@calltelemetry/cucm-mcp 0.1.8 → 0.2.3

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/dist/dime.js CHANGED
@@ -3,308 +3,270 @@ import { extractBoundary, parseMultipartRelated } from "./multipart.js";
3
3
  import { formatCucmDateTime, guessTimezoneString } from "./time.js";
4
4
  import { mkdirSync, writeFileSync } from "node:fs";
5
5
  import { dirname, join } from "node:path";
6
- // Default to accepting self-signed/invalid certs (common on CUCM lab/dev).
7
- // Opt back into strict verification with CUCM_MCP_TLS_MODE=strict.
8
6
  const tlsMode = (process.env.CUCM_MCP_TLS_MODE || process.env.MCP_TLS_MODE || "").toLowerCase();
9
7
  const strictTls = tlsMode === "strict" || tlsMode === "verify";
10
- if (!strictTls)
11
- process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
8
+ if (!strictTls) process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
12
9
  const parser = new XMLParser({
13
- ignoreAttributes: false,
14
- attributeNamePrefix: "@",
15
- removeNSPrefix: true,
16
- trimValues: true,
10
+ ignoreAttributes: false,
11
+ attributeNamePrefix: "@",
12
+ removeNSPrefix: true,
13
+ trimValues: true
17
14
  });
18
15
  function isXmlContentType(contentType) {
19
- const ct = String(contentType || "").toLowerCase();
20
- if (!ct)
21
- return false;
22
- return ct === "text/xml" || ct === "application/xml" || ct === "application/soap+xml" || ct.endsWith("+xml") || ct.includes("/xml");
16
+ const ct = String(contentType || "").toLowerCase();
17
+ if (!ct) return false;
18
+ return ct === "text/xml" || ct === "application/xml" || ct === "application/soap+xml" || ct.endsWith("+xml") || ct.includes("/xml");
23
19
  }
24
20
  function dimeXmlBytes(contentType, bytes) {
25
- const boundary = extractBoundary(contentType);
26
- if (!boundary)
27
- return bytes;
28
- const parts = parseMultipartRelated(bytes, boundary);
29
- const xmlParts = parts.filter((p) => isXmlContentType(p.contentType));
30
- if (xmlParts.length === 0) {
31
- // Some CUCM versions have been observed returning multipart bodies where the XML part
32
- // does not advertise a classic XML content-type. Fall back to sniffing the payload.
33
- const sniffed = parts.find((p) => {
34
- const head = p.body.subarray(0, 64).toString("utf8");
35
- return head.includes("<?xml") || head.trimStart().startsWith("<");
36
- });
37
- if (sniffed)
38
- return sniffed.body;
39
- const partTypes = parts.map((p) => p.contentType).join(", ");
40
- throw new Error(`DIME response missing text/xml part (boundary=${boundary}; parts=[${partTypes || "none"}])`);
41
- }
42
- return xmlParts[0].body;
21
+ const boundary = extractBoundary(contentType);
22
+ if (!boundary) return bytes;
23
+ const parts = parseMultipartRelated(bytes, boundary);
24
+ const xmlParts = parts.filter((p) => isXmlContentType(p.contentType));
25
+ if (xmlParts.length === 0) {
26
+ const sniffed = parts.find((p) => {
27
+ const head = p.body.subarray(0, 64).toString("utf8");
28
+ return head.includes("<?xml") || head.trimStart().startsWith("<");
29
+ });
30
+ if (sniffed) return sniffed.body;
31
+ const partTypes = parts.map((p) => p.contentType).join(", ");
32
+ throw new Error(`DIME response missing text/xml part (boundary=${boundary}; parts=[${partTypes || "none"}])`);
33
+ }
34
+ return xmlParts[0].body;
43
35
  }
44
- export function normalizeHost(hostOrUrl) {
45
- const s = String(hostOrUrl || "").trim();
46
- if (!s)
47
- throw new Error("host is required");
48
- if (s.includes("://")) {
49
- const u = new URL(s);
50
- return u.hostname;
51
- }
52
- return s.replace(/^https?:\/\//, "").replace(/\/+$/, "").split("/")[0];
36
+ function normalizeHost(hostOrUrl) {
37
+ const s = String(hostOrUrl || "").trim();
38
+ if (!s) throw new Error("host is required");
39
+ if (s.includes("://")) {
40
+ const u = new URL(s);
41
+ return u.hostname;
42
+ }
43
+ return s.replace(/^https?:\/\//, "").replace(/\/+$/, "").split("/")[0];
53
44
  }
54
- export function resolveAuth(auth) {
55
- const username = auth?.username || process.env.CUCM_DIME_USERNAME || process.env.CUCM_USERNAME;
56
- const password = auth?.password || process.env.CUCM_DIME_PASSWORD || process.env.CUCM_PASSWORD;
57
- if (!username || !password) {
58
- throw new Error("Missing DIME credentials (provide auth or set CUCM_DIME_USERNAME/CUCM_DIME_PASSWORD)");
59
- }
60
- return { username, password };
45
+ function resolveAuth(auth) {
46
+ const username = auth?.username || process.env.CUCM_DIME_USERNAME || process.env.CUCM_USERNAME;
47
+ const password = auth?.password || process.env.CUCM_DIME_PASSWORD || process.env.CUCM_PASSWORD;
48
+ if (!username || !password) {
49
+ throw new Error("Missing DIME credentials (provide auth or set CUCM_DIME_USERNAME/CUCM_DIME_PASSWORD)");
50
+ }
51
+ return { username, password };
61
52
  }
62
- export function resolveTarget(hostOrUrl, port) {
63
- const host = normalizeHost(hostOrUrl);
64
- const envPort = process.env.CUCM_DIME_PORT ? Number.parseInt(process.env.CUCM_DIME_PORT, 10) : undefined;
65
- const resolvedPort = port ?? envPort ?? 8443;
66
- return { host, port: resolvedPort };
53
+ function resolveTarget(hostOrUrl, port) {
54
+ const host = normalizeHost(hostOrUrl);
55
+ const envPort = process.env.CUCM_DIME_PORT ? Number.parseInt(process.env.CUCM_DIME_PORT, 10) : void 0;
56
+ const resolvedPort = port ?? envPort ?? 8443;
57
+ return { host, port: resolvedPort };
67
58
  }
68
59
  function basicAuthHeader(username, password) {
69
- return `Basic ${Buffer.from(`${username}:${password}`, "utf8").toString("base64")}`;
60
+ return `Basic ${Buffer.from(`${username}:${password}`, "utf8").toString("base64")}`;
70
61
  }
71
62
  function escapeXml(s) {
72
- return s
73
- .replaceAll("&", "&amp;")
74
- .replaceAll("<", "&lt;")
75
- .replaceAll(">", "&gt;")
76
- .replaceAll('"', "&quot;")
77
- .replaceAll("'", "&apos;");
63
+ return s.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&apos;");
78
64
  }
79
65
  function soapEnvelopeList() {
80
- return ('<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:soap="http://schemas.cisco.com/ast/soap">' +
81
- "<soapenv:Header/>" +
82
- "<soapenv:Body>" +
83
- "<soap:listNodeServiceLogs>" +
84
- "<soap:ListRequest></soap:ListRequest>" +
85
- "</soap:listNodeServiceLogs>" +
86
- "</soapenv:Body>" +
87
- "</soapenv:Envelope>");
66
+ return '<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:soap="http://schemas.cisco.com/ast/soap"><soapenv:Header/><soapenv:Body><soap:listNodeServiceLogs><soap:ListRequest></soap:ListRequest></soap:listNodeServiceLogs></soapenv:Body></soapenv:Envelope>';
88
67
  }
89
68
  function soapItems(tag, values) {
90
- const vals = (values || []).filter((v) => String(v || "").trim() !== "");
91
- if (vals.length === 0)
92
- return `<soap:${tag}><soap:item></soap:item></soap:${tag}>`;
93
- return `<soap:${tag}>${vals.map((v) => `<soap:item>${escapeXml(v)}</soap:item>`).join("")}</soap:${tag}>`;
69
+ const vals = (values || []).filter((v) => String(v || "").trim() !== "");
70
+ if (vals.length === 0) return `<soap:${tag}><soap:item></soap:item></soap:${tag}>`;
71
+ return `<soap:${tag}>${vals.map((v) => `<soap:item>${escapeXml(v)}</soap:item>`).join("")}</soap:${tag}>`;
94
72
  }
95
73
  function soapEnvelopeSelect(criteria) {
96
- const serviceLogs = soapItems("ServiceLogs", criteria.serviceLogs);
97
- const systemLogs = soapItems("SystemLogs", criteria.systemLogs);
98
- const searchStr = escapeXml(criteria.searchStr || "");
99
- return ('<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:soap="http://schemas.cisco.com/ast/soap">' +
100
- "<soapenv:Header/>" +
101
- "<soapenv:Body>" +
102
- "<soap:selectLogFiles>" +
103
- "<soap:FileSelectionCriteria>" +
104
- serviceLogs +
105
- systemLogs +
106
- `<soap:SearchStr>${searchStr}</soap:SearchStr>` +
107
- "<soap:Frequency>OnDemand</soap:Frequency>" +
108
- "<soap:JobType>DownloadtoClient</soap:JobType>" +
109
- `<soap:ToDate>${escapeXml(criteria.toDate)}</soap:ToDate>` +
110
- `<soap:FromDate>${escapeXml(criteria.fromDate)}</soap:FromDate>` +
111
- `<soap:TimeZone>${escapeXml(criteria.timezone)}</soap:TimeZone>` +
112
- "<soap:RelText>None</soap:RelText>" +
113
- "<soap:RelTime>0</soap:RelTime>" +
114
- "<soap:Port></soap:Port>" +
115
- "<soap:IPAddress></soap:IPAddress>" +
116
- "<soap:UserName></soap:UserName>" +
117
- "<soap:Password></soap:Password>" +
118
- "<soap:ZipInfo></soap:ZipInfo>" +
119
- "<soap:RemoteFolder></soap:RemoteFolder>" +
120
- "</soap:FileSelectionCriteria>" +
121
- "</soap:selectLogFiles>" +
122
- "</soapenv:Body>" +
123
- "</soapenv:Envelope>");
74
+ const serviceLogs = soapItems("ServiceLogs", criteria.serviceLogs);
75
+ const systemLogs = soapItems("SystemLogs", criteria.systemLogs);
76
+ const searchStr = escapeXml(criteria.searchStr || "");
77
+ return '<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:soap="http://schemas.cisco.com/ast/soap"><soapenv:Header/><soapenv:Body><soap:selectLogFiles><soap:FileSelectionCriteria>' + serviceLogs + systemLogs + `<soap:SearchStr>${searchStr}</soap:SearchStr><soap:Frequency>OnDemand</soap:Frequency><soap:JobType>DownloadtoClient</soap:JobType><soap:ToDate>${escapeXml(criteria.toDate)}</soap:ToDate><soap:FromDate>${escapeXml(criteria.fromDate)}</soap:FromDate><soap:TimeZone>${escapeXml(criteria.timezone)}</soap:TimeZone><soap:RelText>None</soap:RelText><soap:RelTime>0</soap:RelTime><soap:Port></soap:Port><soap:IPAddress></soap:IPAddress><soap:UserName></soap:UserName><soap:Password></soap:Password><soap:ZipInfo></soap:ZipInfo><soap:RemoteFolder></soap:RemoteFolder></soap:FileSelectionCriteria></soap:selectLogFiles></soapenv:Body></soapenv:Envelope>`;
124
78
  }
125
79
  function soapEnvelopeGetOneFile(fileName) {
126
- return ('<soapenv:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:soap="http://schemas.cisco.com/ast/soap/">' +
127
- "<soapenv:Header/>" +
128
- "<soapenv:Body>" +
129
- '<soap:GetOneFile soapenv:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">' +
130
- `<FileName xsi:type="get:FileName" xmlns:get="http://cisco.com/ccm/serviceability/soap/LogCollection/GetFile/">${escapeXml(fileName)}</FileName>` +
131
- "</soap:GetOneFile>" +
132
- "</soapenv:Body>" +
133
- "</soapenv:Envelope>");
80
+ return `<soapenv:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:soap="http://schemas.cisco.com/ast/soap/"><soapenv:Header/><soapenv:Body><soap:GetOneFile soapenv:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><FileName xsi:type="get:FileName" xmlns:get="http://cisco.com/ccm/serviceability/soap/LogCollection/GetFile/">${escapeXml(fileName)}</FileName></soap:GetOneFile></soapenv:Body></soapenv:Envelope>`;
134
81
  }
135
- async function fetchSoap(target, auth, path, soapAction, xmlBody, timeoutMs = 30000) {
136
- const url = `https://${target.host}:${target.port}${path}`;
137
- const res = await fetch(url, {
138
- method: "POST",
139
- headers: {
140
- Authorization: basicAuthHeader(auth.username, auth.password),
141
- SOAPAction: soapAction,
142
- "Content-Type": "text/xml;charset=UTF-8",
143
- Accept: "*/*",
144
- },
145
- body: Buffer.from(xmlBody, "utf8"),
146
- signal: AbortSignal.timeout(timeoutMs),
147
- });
148
- if (!res.ok) {
149
- const text = await res.text().catch(() => "");
150
- throw new Error(`CUCM DIME HTTP ${res.status}: ${text || res.statusText}`);
151
- }
152
- const contentType = res.headers.get("content-type");
153
- const ab = await res.arrayBuffer();
154
- return { contentType, bytes: Buffer.from(ab) };
82
+ async function fetchSoap(target, auth, path, soapAction, xmlBody, timeoutMs = 3e4) {
83
+ const url = `https://${target.host}:${target.port}${path}`;
84
+ const res = await fetch(url, {
85
+ method: "POST",
86
+ headers: {
87
+ Authorization: basicAuthHeader(auth.username, auth.password),
88
+ SOAPAction: soapAction,
89
+ "Content-Type": "text/xml;charset=UTF-8",
90
+ Accept: "*/*"
91
+ },
92
+ body: Buffer.from(xmlBody, "utf8"),
93
+ signal: AbortSignal.timeout(timeoutMs)
94
+ });
95
+ if (!res.ok) {
96
+ const text = await res.text().catch(() => "");
97
+ throw new Error(`CUCM DIME HTTP ${res.status}: ${text || res.statusText}`);
98
+ }
99
+ const contentType = res.headers.get("content-type");
100
+ const ab = await res.arrayBuffer();
101
+ return { contentType, bytes: Buffer.from(ab) };
155
102
  }
156
- export async function listNodeServiceLogs(hostOrUrl, auth, port) {
157
- const target = resolveTarget(hostOrUrl, port);
158
- const resolvedAuth = resolveAuth(auth);
159
- const { contentType, bytes } = await fetchSoap(target, resolvedAuth, "/logcollectionservice2/services/LogCollectionPortTypeService", "listNodeServiceLogs", soapEnvelopeList());
160
- const xml = dimeXmlBytes(contentType, bytes);
161
- const parsed = parser.parse(xml.toString("utf8"));
162
- const env = parsed.Envelope || parsed;
163
- const body = env.Body || env;
164
- const resp = body.listNodeServiceLogsResponse;
165
- const ret = resp?.listNodeServiceLogsReturn;
166
- if (!ret)
167
- throw new Error("Unexpected listNodeServiceLogs response shape");
168
- const items = Array.isArray(ret) ? ret : [ret];
169
- return items.map((it) => {
170
- const serviceLogsRaw = it?.ServiceLog?.item;
171
- const serviceLogs = Array.isArray(serviceLogsRaw)
172
- ? serviceLogsRaw
173
- : typeof serviceLogsRaw === "string"
174
- ? [serviceLogsRaw]
175
- : [];
176
- return {
177
- server: String(it?.name || ""),
178
- serviceLogs,
179
- count: serviceLogs.length,
180
- };
181
- });
103
+ async function listNodeServiceLogs(hostOrUrl, auth, port) {
104
+ const target = resolveTarget(hostOrUrl, port);
105
+ const resolvedAuth = resolveAuth(auth);
106
+ const { contentType, bytes } = await fetchSoap(
107
+ target,
108
+ resolvedAuth,
109
+ "/logcollectionservice2/services/LogCollectionPortTypeService",
110
+ "listNodeServiceLogs",
111
+ soapEnvelopeList()
112
+ );
113
+ const xml = dimeXmlBytes(contentType, bytes);
114
+ const parsed = parser.parse(xml.toString("utf8"));
115
+ const env = parsed.Envelope || parsed;
116
+ const body = env.Body || env;
117
+ const resp = body.listNodeServiceLogsResponse;
118
+ const ret = resp?.listNodeServiceLogsReturn;
119
+ if (!ret) throw new Error("Unexpected listNodeServiceLogs response shape");
120
+ const items = Array.isArray(ret) ? ret : [ret];
121
+ return items.map((it) => {
122
+ const serviceLogsRaw = it?.ServiceLog?.item;
123
+ const serviceLogs = Array.isArray(serviceLogsRaw) ? serviceLogsRaw : typeof serviceLogsRaw === "string" ? [serviceLogsRaw] : [];
124
+ return {
125
+ server: String(it?.name || ""),
126
+ serviceLogs,
127
+ count: serviceLogs.length
128
+ };
129
+ });
182
130
  }
183
- export async function selectLogs(hostOrUrl, criteria, auth, port) {
184
- const target = resolveTarget(hostOrUrl, port);
185
- const resolvedAuth = resolveAuth(auth);
186
- const hasService = (criteria.serviceLogs || []).some((x) => String(x || "").trim() !== "");
187
- const hasSystem = (criteria.systemLogs || []).some((x) => String(x || "").trim() !== "");
188
- if (!hasService && !hasSystem) {
189
- throw new Error("selectLogs requires at least one of serviceLogs or systemLogs");
190
- }
191
- const { contentType, bytes } = await fetchSoap(target, resolvedAuth, "/logcollectionservice2/services/LogCollectionPortTypeService", "selectLogFiles", soapEnvelopeSelect(criteria));
192
- const xml = dimeXmlBytes(contentType, bytes);
193
- const parsed = parser.parse(xml.toString("utf8"));
194
- const env = parsed.Envelope || parsed;
195
- const body = env.Body || env;
196
- const resp = body.selectLogFilesResponse;
197
- const resultSet = resp?.ResultSet;
198
- const serviceFileList = resultSet?.SchemaFileSelectionResult?.Node?.ServiceList?.ServiceLogs?.SetOfFiles?.File;
199
- const systemFileList = resultSet?.SchemaFileSelectionResult?.Node?.ServiceList?.SystemLogs?.SetOfFiles?.File;
200
- const toArray = (x) => (x == null ? [] : Array.isArray(x) ? x : [x]);
201
- const combined = [...toArray(serviceFileList), ...toArray(systemFileList)];
202
- if (combined.length === 0)
203
- throw new Error("No files found (missing ServiceLogs/SystemLogs SetOfFiles/File)");
204
- return combined.map((f) => {
205
- const absolutePath = f?.absolutepath || f?.AbsolutePath || f?.Absolutepath;
206
- const fileName = f?.filename || f?.FileName || f?.Filename;
207
- return {
208
- server: target.host,
209
- absolutePath: absolutePath ? String(absolutePath) : undefined,
210
- fileName: fileName ? String(fileName) : undefined,
211
- ...f,
212
- };
213
- });
131
+ async function selectLogs(hostOrUrl, criteria, auth, port) {
132
+ const target = resolveTarget(hostOrUrl, port);
133
+ const resolvedAuth = resolveAuth(auth);
134
+ const hasService = (criteria.serviceLogs || []).some((x) => String(x || "").trim() !== "");
135
+ const hasSystem = (criteria.systemLogs || []).some((x) => String(x || "").trim() !== "");
136
+ if (!hasService && !hasSystem) {
137
+ throw new Error("selectLogs requires at least one of serviceLogs or systemLogs");
138
+ }
139
+ const { contentType, bytes } = await fetchSoap(
140
+ target,
141
+ resolvedAuth,
142
+ "/logcollectionservice2/services/LogCollectionPortTypeService",
143
+ "selectLogFiles",
144
+ soapEnvelopeSelect(criteria)
145
+ );
146
+ const xml = dimeXmlBytes(contentType, bytes);
147
+ const parsed = parser.parse(xml.toString("utf8"));
148
+ const env = parsed.Envelope || parsed;
149
+ const body = env.Body || env;
150
+ const resp = body.selectLogFilesResponse;
151
+ const resultSet = resp?.ResultSet;
152
+ const serviceFileList = resultSet?.SchemaFileSelectionResult?.Node?.ServiceList?.ServiceLogs?.SetOfFiles?.File;
153
+ const systemFileList = resultSet?.SchemaFileSelectionResult?.Node?.ServiceList?.SystemLogs?.SetOfFiles?.File;
154
+ const toArray = (x) => x == null ? [] : Array.isArray(x) ? x : [x];
155
+ const combined = [...toArray(serviceFileList), ...toArray(systemFileList)];
156
+ if (combined.length === 0) throw new Error("No files found (missing ServiceLogs/SystemLogs SetOfFiles/File)");
157
+ return combined.map((f) => {
158
+ const absolutePath = f?.absolutepath || f?.AbsolutePath || f?.Absolutepath;
159
+ const fileName = f?.filename || f?.FileName || f?.Filename;
160
+ return {
161
+ server: target.host,
162
+ absolutePath: absolutePath ? String(absolutePath) : void 0,
163
+ fileName: fileName ? String(fileName) : void 0,
164
+ ...f
165
+ };
166
+ });
214
167
  }
215
- export async function selectLogsMinutes(hostOrUrl, minutesBack, select, timezone, auth, port) {
216
- const now = new Date();
217
- const past = new Date(now.getTime() - minutesBack * 60_000);
218
- const fromDate = formatCucmDateTime(past);
219
- const toDate = formatCucmDateTime(now);
220
- const tz = timezone || guessTimezoneString(now);
221
- const files = await selectLogs(hostOrUrl, { ...select, fromDate, toDate, timezone: tz }, auth, port);
222
- return { fromDate, toDate, timezone: tz, files };
168
+ async function selectLogsMinutes(hostOrUrl, minutesBack, select, timezone, auth, port) {
169
+ const now = /* @__PURE__ */ new Date();
170
+ const past = new Date(now.getTime() - minutesBack * 6e4);
171
+ const fromDate = formatCucmDateTime(past);
172
+ const toDate = formatCucmDateTime(now);
173
+ const tz = timezone || guessTimezoneString(now);
174
+ const files = await selectLogs(
175
+ hostOrUrl,
176
+ { ...select, fromDate, toDate, timezone: tz },
177
+ auth,
178
+ port
179
+ );
180
+ return { fromDate, toDate, timezone: tz, files };
223
181
  }
224
- export async function getOneFile(hostOrUrl, filePath, auth, port) {
225
- const target = resolveTarget(hostOrUrl, port);
226
- const resolvedAuth = resolveAuth(auth);
227
- const { contentType, bytes } = await fetchSoap(target, resolvedAuth, "/logcollectionservice/services/DimeGetFileService", "http://schemas.cisco.com/ast/soap/action/#LogCollectionPort#GetOneFile", soapEnvelopeGetOneFile(filePath));
228
- const boundary = extractBoundary(contentType);
229
- if (!boundary) {
230
- // Some environments respond with a non-multipart body. Prefer treating non-XML bodies as raw file bytes.
231
- const ct = (contentType || "").toLowerCase();
232
- if (ct.includes("text/xml") || ct.includes("application/soap+xml")) {
233
- // Best-effort fault detection
234
- const asText = bytes.toString("utf8");
235
- if (/\bFault\b/i.test(asText))
236
- throw new Error(`CUCM DIME GetOneFile returned SOAP fault: ${asText}`);
237
- // If it's XML but not a fault, still return raw bytes.
238
- }
239
- return { server: target.host, filename: filePath, data: bytes };
182
+ async function getOneFile(hostOrUrl, filePath, auth, port) {
183
+ const target = resolveTarget(hostOrUrl, port);
184
+ const resolvedAuth = resolveAuth(auth);
185
+ const { contentType, bytes } = await fetchSoap(
186
+ target,
187
+ resolvedAuth,
188
+ "/logcollectionservice/services/DimeGetFileService",
189
+ "http://schemas.cisco.com/ast/soap/action/#LogCollectionPort#GetOneFile",
190
+ soapEnvelopeGetOneFile(filePath)
191
+ );
192
+ const boundary = extractBoundary(contentType);
193
+ if (!boundary) {
194
+ const ct = (contentType || "").toLowerCase();
195
+ if (ct.includes("text/xml") || ct.includes("application/soap+xml")) {
196
+ const asText = bytes.toString("utf8");
197
+ if (/\bFault\b/i.test(asText)) throw new Error(`CUCM DIME GetOneFile returned SOAP fault: ${asText}`);
240
198
  }
241
- const parts = parseMultipartRelated(bytes, boundary);
242
- if (parts.length === 0)
243
- throw new Error("DIME GetOneFile returned no multipart parts");
244
- const nonXml = parts.find((p) => !isXmlContentType(p.contentType));
245
- if (!nonXml)
246
- throw new Error("DIME GetOneFile response missing non-XML file part");
247
- return { server: target.host, filename: filePath, data: nonXml.body };
199
+ return { server: target.host, filename: filePath, data: bytes };
200
+ }
201
+ const parts = parseMultipartRelated(bytes, boundary);
202
+ if (parts.length === 0) throw new Error("DIME GetOneFile returned no multipart parts");
203
+ const nonXml = parts.find((p) => !isXmlContentType(p.contentType));
204
+ if (!nonXml) throw new Error("DIME GetOneFile response missing non-XML file part");
205
+ return { server: target.host, filename: filePath, data: nonXml.body };
248
206
  }
249
207
  function isDimeMissingFileError(e) {
250
- const msg = e instanceof Error ? e.message : String(e || "");
251
- // CUCM DIME commonly responds with HTTP 500 and a SOAP fault like:
252
- // "FileName ... do not exist"
253
- return /DIME HTTP 500/i.test(msg) && /do not exist/i.test(msg);
208
+ const msg = e instanceof Error ? e.message : String(e || "");
209
+ return /DIME HTTP 500/i.test(msg) && /do not exist/i.test(msg);
254
210
  }
255
- export async function getOneFileWithRetry(hostOrUrl, filePath, opts) {
256
- const timeoutMs = Math.max(1000, opts?.timeoutMs ?? 120_000);
257
- const pollIntervalMs = Math.max(250, opts?.pollIntervalMs ?? 2000);
258
- const start = Date.now();
259
- let attempts = 0;
260
- // Simple fixed-interval poll; CUCM can take time to flush capture files to disk.
261
- while (true) {
262
- attempts++;
263
- try {
264
- const r = await getOneFile(hostOrUrl, filePath, opts?.auth, opts?.port);
265
- return { ...r, attempts, waitedMs: Date.now() - start };
266
- }
267
- catch (e) {
268
- const elapsed = Date.now() - start;
269
- if (!isDimeMissingFileError(e) || elapsed >= timeoutMs)
270
- throw e;
271
- await new Promise((r) => setTimeout(r, pollIntervalMs));
272
- }
211
+ async function getOneFileWithRetry(hostOrUrl, filePath, opts) {
212
+ const timeoutMs = Math.max(1e3, opts?.timeoutMs ?? 12e4);
213
+ const pollIntervalMs = Math.max(250, opts?.pollIntervalMs ?? 2e3);
214
+ const start = Date.now();
215
+ let attempts = 0;
216
+ while (true) {
217
+ attempts++;
218
+ try {
219
+ const r = await getOneFile(hostOrUrl, filePath, opts?.auth, opts?.port);
220
+ return { ...r, attempts, waitedMs: Date.now() - start };
221
+ } catch (e) {
222
+ const elapsed = Date.now() - start;
223
+ if (!isDimeMissingFileError(e) || elapsed >= timeoutMs) throw e;
224
+ await new Promise((r) => setTimeout(r, pollIntervalMs));
273
225
  }
226
+ }
274
227
  }
275
- export async function getOneFileAnyWithRetry(hostOrUrl, filePaths, opts) {
276
- const paths = (filePaths || []).map(String).filter((p) => p.trim() !== "");
277
- if (paths.length === 0)
278
- throw new Error("getOneFileAnyWithRetry requires at least one filePath");
279
- const timeoutMs = Math.max(1000, opts?.timeoutMs ?? 120_000);
280
- const pollIntervalMs = Math.max(250, opts?.pollIntervalMs ?? 2000);
281
- const start = Date.now();
282
- let attempts = 0;
283
- while (true) {
284
- attempts++;
285
- for (const p of paths) {
286
- try {
287
- const r = await getOneFile(hostOrUrl, p, opts?.auth, opts?.port);
288
- return { ...r, attempts, waitedMs: Date.now() - start };
289
- }
290
- catch (e) {
291
- if (!isDimeMissingFileError(e))
292
- throw e;
293
- }
294
- }
295
- const elapsed = Date.now() - start;
296
- if (elapsed >= timeoutMs) {
297
- // If timeout, throw the same missing-file style error for the first candidate.
298
- throw new Error(`DIME HTTP 500: FileName ${paths[0]} do not exist (timed out after ${elapsed}ms)`);
299
- }
300
- await new Promise((r) => setTimeout(r, pollIntervalMs));
228
+ async function getOneFileAnyWithRetry(hostOrUrl, filePaths, opts) {
229
+ const paths = (filePaths || []).map(String).filter((p) => p.trim() !== "");
230
+ if (paths.length === 0) throw new Error("getOneFileAnyWithRetry requires at least one filePath");
231
+ const timeoutMs = Math.max(1e3, opts?.timeoutMs ?? 12e4);
232
+ const pollIntervalMs = Math.max(250, opts?.pollIntervalMs ?? 2e3);
233
+ const start = Date.now();
234
+ let attempts = 0;
235
+ while (true) {
236
+ attempts++;
237
+ for (const p of paths) {
238
+ try {
239
+ const r = await getOneFile(hostOrUrl, p, opts?.auth, opts?.port);
240
+ return { ...r, attempts, waitedMs: Date.now() - start };
241
+ } catch (e) {
242
+ if (!isDimeMissingFileError(e)) throw e;
243
+ }
244
+ }
245
+ const elapsed = Date.now() - start;
246
+ if (elapsed >= timeoutMs) {
247
+ throw new Error(`DIME HTTP 500: FileName ${paths[0]} do not exist (timed out after ${elapsed}ms)`);
301
248
  }
249
+ await new Promise((r) => setTimeout(r, pollIntervalMs));
250
+ }
302
251
  }
303
- export function writeDownloadedFile(result, outFile) {
304
- const baseName = result.filename.split("/").filter(Boolean).pop() || "cucm-file.bin";
305
- const defaultDir = join("/tmp", "cucm-mcp");
306
- const filePath = outFile || join(defaultDir, baseName);
307
- mkdirSync(dirname(filePath), { recursive: true });
308
- writeFileSync(filePath, result.data);
309
- return { filePath, bytes: result.data.length, baseName };
252
+ function writeDownloadedFile(result, outFile) {
253
+ const baseName = result.filename.split("/").filter(Boolean).pop() || "cucm-file.bin";
254
+ const defaultDir = join("/tmp", "cucm-mcp");
255
+ const filePath = outFile || join(defaultDir, baseName);
256
+ mkdirSync(dirname(filePath), { recursive: true });
257
+ writeFileSync(filePath, result.data);
258
+ return { filePath, bytes: result.data.length, baseName };
310
259
  }
260
+ export {
261
+ getOneFile,
262
+ getOneFileAnyWithRetry,
263
+ getOneFileWithRetry,
264
+ listNodeServiceLogs,
265
+ normalizeHost,
266
+ resolveAuth,
267
+ resolveTarget,
268
+ selectLogs,
269
+ selectLogsMinutes,
270
+ writeDownloadedFile
271
+ };
272
+ //# sourceMappingURL=dime.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/dime.ts"],
4
+ "sourcesContent": ["import { XMLParser } from \"fast-xml-parser\";\nimport { extractBoundary, parseMultipartRelated } from \"./multipart.js\";\nimport { formatCucmDateTime, guessTimezoneString } from \"./time.js\";\nimport { mkdirSync, writeFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\n\n// Default to accepting self-signed/invalid certs (common on CUCM lab/dev).\n// Opt back into strict verification with CUCM_MCP_TLS_MODE=strict.\nconst tlsMode = (process.env.CUCM_MCP_TLS_MODE || process.env.MCP_TLS_MODE || \"\").toLowerCase();\nconst strictTls = tlsMode === \"strict\" || tlsMode === \"verify\";\nif (!strictTls) process.env.NODE_TLS_REJECT_UNAUTHORIZED = \"0\";\n\nexport type DimeAuth = { username?: string; password?: string };\n\nexport type DimeTarget = {\n host: string;\n port?: number;\n};\n\nexport type NodeServiceLogs = {\n server: string;\n serviceLogs: string[];\n count: number;\n};\n\nexport type SelectedLogFile = {\n server: string;\n absolutePath?: string;\n fileName?: string;\n [k: string]: unknown;\n};\n\nexport type SelectLogsCriteria = {\n serviceLogs?: string[];\n systemLogs?: string[];\n searchStr?: string;\n fromDate: string;\n toDate: string;\n timezone: string;\n};\n\nconst parser = new XMLParser({\n ignoreAttributes: false,\n attributeNamePrefix: \"@\",\n removeNSPrefix: true,\n trimValues: true,\n});\n\nfunction isXmlContentType(contentType: string): boolean {\n const ct = String(contentType || \"\").toLowerCase();\n if (!ct) return false;\n return ct === \"text/xml\" || ct === \"application/xml\" || ct === \"application/soap+xml\" || ct.endsWith(\"+xml\") || ct.includes(\"/xml\");\n}\n\nfunction dimeXmlBytes(contentType: string | null, bytes: Buffer): Buffer {\n const boundary = extractBoundary(contentType);\n if (!boundary) return bytes;\n const parts = parseMultipartRelated(bytes, boundary);\n const xmlParts = parts.filter((p) => isXmlContentType(p.contentType));\n if (xmlParts.length === 0) {\n // Some CUCM versions have been observed returning multipart bodies where the XML part\n // does not advertise a classic XML content-type. Fall back to sniffing the payload.\n const sniffed = parts.find((p) => {\n const head = p.body.subarray(0, 64).toString(\"utf8\");\n return head.includes(\"<?xml\") || head.trimStart().startsWith(\"<\");\n });\n if (sniffed) return sniffed.body;\n\n const partTypes = parts.map((p) => p.contentType).join(\", \");\n throw new Error(`DIME response missing text/xml part (boundary=${boundary}; parts=[${partTypes || \"none\"}])`);\n }\n return xmlParts[0].body;\n}\n\nexport function normalizeHost(hostOrUrl: string): string {\n const s = String(hostOrUrl || \"\").trim();\n if (!s) throw new Error(\"host is required\");\n if (s.includes(\"://\")) {\n const u = new URL(s);\n return u.hostname;\n }\n return s.replace(/^https?:\\/\\//, \"\").replace(/\\/+$/, \"\").split(\"/\")[0];\n}\n\nexport function resolveAuth(auth?: DimeAuth): Required<DimeAuth> {\n const username = auth?.username || process.env.CUCM_DIME_USERNAME || process.env.CUCM_USERNAME;\n const password = auth?.password || process.env.CUCM_DIME_PASSWORD || process.env.CUCM_PASSWORD;\n if (!username || !password) {\n throw new Error(\"Missing DIME credentials (provide auth or set CUCM_DIME_USERNAME/CUCM_DIME_PASSWORD)\");\n }\n return { username, password };\n}\n\nexport function resolveTarget(hostOrUrl: string, port?: number): DimeTarget {\n const host = normalizeHost(hostOrUrl);\n const envPort = process.env.CUCM_DIME_PORT ? Number.parseInt(process.env.CUCM_DIME_PORT, 10) : undefined;\n const resolvedPort = port ?? envPort ?? 8443;\n return { host, port: resolvedPort };\n}\n\nfunction basicAuthHeader(username: string, password: string): string {\n return `Basic ${Buffer.from(`${username}:${password}`, \"utf8\").toString(\"base64\")}`;\n}\n\nfunction escapeXml(s: string): string {\n return s\n .replaceAll(\"&\", \"&amp;\")\n .replaceAll(\"<\", \"&lt;\")\n .replaceAll(\">\", \"&gt;\")\n .replaceAll('\"', \"&quot;\")\n .replaceAll(\"'\", \"&apos;\");\n}\n\nfunction soapEnvelopeList(): string {\n return (\n '<soapenv:Envelope xmlns:soapenv=\"http://schemas.xmlsoap.org/soap/envelope/\" xmlns:soap=\"http://schemas.cisco.com/ast/soap\">' +\n \"<soapenv:Header/>\" +\n \"<soapenv:Body>\" +\n \"<soap:listNodeServiceLogs>\" +\n \"<soap:ListRequest></soap:ListRequest>\" +\n \"</soap:listNodeServiceLogs>\" +\n \"</soapenv:Body>\" +\n \"</soapenv:Envelope>\"\n );\n}\n\nfunction soapItems(tag: string, values?: string[]): string {\n const vals = (values || []).filter((v) => String(v || \"\").trim() !== \"\");\n if (vals.length === 0) return `<soap:${tag}><soap:item></soap:item></soap:${tag}>`;\n return `<soap:${tag}>${vals.map((v) => `<soap:item>${escapeXml(v)}</soap:item>`).join(\"\")}</soap:${tag}>`;\n}\n\nfunction soapEnvelopeSelect(criteria: SelectLogsCriteria): string {\n const serviceLogs = soapItems(\"ServiceLogs\", criteria.serviceLogs);\n const systemLogs = soapItems(\"SystemLogs\", criteria.systemLogs);\n const searchStr = escapeXml(criteria.searchStr || \"\");\n\n return (\n '<soapenv:Envelope xmlns:soapenv=\"http://schemas.xmlsoap.org/soap/envelope/\" xmlns:soap=\"http://schemas.cisco.com/ast/soap\">' +\n \"<soapenv:Header/>\" +\n \"<soapenv:Body>\" +\n \"<soap:selectLogFiles>\" +\n \"<soap:FileSelectionCriteria>\" +\n serviceLogs +\n systemLogs +\n `<soap:SearchStr>${searchStr}</soap:SearchStr>` +\n \"<soap:Frequency>OnDemand</soap:Frequency>\" +\n \"<soap:JobType>DownloadtoClient</soap:JobType>\" +\n `<soap:ToDate>${escapeXml(criteria.toDate)}</soap:ToDate>` +\n `<soap:FromDate>${escapeXml(criteria.fromDate)}</soap:FromDate>` +\n `<soap:TimeZone>${escapeXml(criteria.timezone)}</soap:TimeZone>` +\n \"<soap:RelText>None</soap:RelText>\" +\n \"<soap:RelTime>0</soap:RelTime>\" +\n \"<soap:Port></soap:Port>\" +\n \"<soap:IPAddress></soap:IPAddress>\" +\n \"<soap:UserName></soap:UserName>\" +\n \"<soap:Password></soap:Password>\" +\n \"<soap:ZipInfo></soap:ZipInfo>\" +\n \"<soap:RemoteFolder></soap:RemoteFolder>\" +\n \"</soap:FileSelectionCriteria>\" +\n \"</soap:selectLogFiles>\" +\n \"</soapenv:Body>\" +\n \"</soapenv:Envelope>\"\n );\n}\n\nfunction soapEnvelopeGetOneFile(fileName: string): string {\n return (\n '<soapenv:Envelope xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:soapenv=\"http://schemas.xmlsoap.org/soap/envelope/\" xmlns:soap=\"http://schemas.cisco.com/ast/soap/\">' +\n \"<soapenv:Header/>\" +\n \"<soapenv:Body>\" +\n '<soap:GetOneFile soapenv:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">' +\n `<FileName xsi:type=\"get:FileName\" xmlns:get=\"http://cisco.com/ccm/serviceability/soap/LogCollection/GetFile/\">${escapeXml(fileName)}</FileName>` +\n \"</soap:GetOneFile>\" +\n \"</soapenv:Body>\" +\n \"</soapenv:Envelope>\"\n );\n}\n\nasync function fetchSoap(\n target: DimeTarget,\n auth: Required<DimeAuth>,\n path: string,\n soapAction: string,\n xmlBody: string,\n timeoutMs = 30000\n): Promise<{ contentType: string | null; bytes: Buffer }> {\n const url = `https://${target.host}:${target.port}${path}`;\n const res = await fetch(url, {\n method: \"POST\",\n headers: {\n Authorization: basicAuthHeader(auth.username, auth.password),\n SOAPAction: soapAction,\n \"Content-Type\": \"text/xml;charset=UTF-8\",\n Accept: \"*/*\",\n },\n body: Buffer.from(xmlBody, \"utf8\"),\n signal: AbortSignal.timeout(timeoutMs),\n });\n\n if (!res.ok) {\n const text = await res.text().catch(() => \"\");\n throw new Error(`CUCM DIME HTTP ${res.status}: ${text || res.statusText}`);\n }\n\n const contentType = res.headers.get(\"content-type\");\n const ab = await res.arrayBuffer();\n return { contentType, bytes: Buffer.from(ab) };\n}\n\nexport async function listNodeServiceLogs(hostOrUrl: string, auth?: DimeAuth, port?: number): Promise<NodeServiceLogs[]> {\n const target = resolveTarget(hostOrUrl, port);\n const resolvedAuth = resolveAuth(auth);\n\n const { contentType, bytes } = await fetchSoap(\n target,\n resolvedAuth,\n \"/logcollectionservice2/services/LogCollectionPortTypeService\",\n \"listNodeServiceLogs\",\n soapEnvelopeList()\n );\n\n const xml = dimeXmlBytes(contentType, bytes);\n const parsed = parser.parse(xml.toString(\"utf8\"));\n const env = parsed.Envelope || parsed;\n const body = env.Body || env;\n const resp = body.listNodeServiceLogsResponse;\n const ret = resp?.listNodeServiceLogsReturn;\n if (!ret) throw new Error(\"Unexpected listNodeServiceLogs response shape\");\n\n const items = Array.isArray(ret) ? ret : [ret];\n return items.map((it: any) => {\n const serviceLogsRaw = it?.ServiceLog?.item;\n const serviceLogs = Array.isArray(serviceLogsRaw)\n ? serviceLogsRaw\n : typeof serviceLogsRaw === \"string\"\n ? [serviceLogsRaw]\n : [];\n return {\n server: String(it?.name || \"\"),\n serviceLogs,\n count: serviceLogs.length,\n };\n });\n}\n\nexport async function selectLogs(\n hostOrUrl: string,\n criteria: SelectLogsCriteria,\n auth?: DimeAuth,\n port?: number\n): Promise<SelectedLogFile[]> {\n const target = resolveTarget(hostOrUrl, port);\n const resolvedAuth = resolveAuth(auth);\n\n const hasService = (criteria.serviceLogs || []).some((x) => String(x || \"\").trim() !== \"\");\n const hasSystem = (criteria.systemLogs || []).some((x) => String(x || \"\").trim() !== \"\");\n if (!hasService && !hasSystem) {\n throw new Error(\"selectLogs requires at least one of serviceLogs or systemLogs\");\n }\n\n const { contentType, bytes } = await fetchSoap(\n target,\n resolvedAuth,\n \"/logcollectionservice2/services/LogCollectionPortTypeService\",\n \"selectLogFiles\",\n soapEnvelopeSelect(criteria)\n );\n\n const xml = dimeXmlBytes(contentType, bytes);\n const parsed = parser.parse(xml.toString(\"utf8\"));\n const env = parsed.Envelope || parsed;\n const body = env.Body || env;\n const resp = body.selectLogFilesResponse;\n const resultSet = resp?.ResultSet;\n const serviceFileList =\n resultSet?.SchemaFileSelectionResult?.Node?.ServiceList?.ServiceLogs?.SetOfFiles?.File;\n const systemFileList =\n resultSet?.SchemaFileSelectionResult?.Node?.ServiceList?.SystemLogs?.SetOfFiles?.File;\n\n const toArray = (x: any) => (x == null ? [] : Array.isArray(x) ? x : [x]);\n const combined = [...toArray(serviceFileList), ...toArray(systemFileList)];\n\n if (combined.length === 0) throw new Error(\"No files found (missing ServiceLogs/SystemLogs SetOfFiles/File)\");\n\n return combined.map((f: any) => {\n const absolutePath = f?.absolutepath || f?.AbsolutePath || f?.Absolutepath;\n const fileName = f?.filename || f?.FileName || f?.Filename;\n return {\n server: target.host,\n absolutePath: absolutePath ? String(absolutePath) : undefined,\n fileName: fileName ? String(fileName) : undefined,\n ...f,\n };\n });\n}\n\nexport async function selectLogsMinutes(\n hostOrUrl: string,\n minutesBack: number,\n select: Pick<SelectLogsCriteria, \"serviceLogs\" | \"systemLogs\" | \"searchStr\">,\n timezone?: string,\n auth?: DimeAuth,\n port?: number\n): Promise<{ fromDate: string; toDate: string; timezone: string; files: SelectedLogFile[] }> {\n const now = new Date();\n const past = new Date(now.getTime() - minutesBack * 60_000);\n const fromDate = formatCucmDateTime(past);\n const toDate = formatCucmDateTime(now);\n const tz = timezone || guessTimezoneString(now);\n const files = await selectLogs(\n hostOrUrl,\n { ...select, fromDate, toDate, timezone: tz },\n auth,\n port\n );\n return { fromDate, toDate, timezone: tz, files };\n}\n\nexport async function getOneFile(\n hostOrUrl: string,\n filePath: string,\n auth?: DimeAuth,\n port?: number\n): Promise<{ server: string; filename: string; data: Buffer }> {\n const target = resolveTarget(hostOrUrl, port);\n const resolvedAuth = resolveAuth(auth);\n\n const { contentType, bytes } = await fetchSoap(\n target,\n resolvedAuth,\n \"/logcollectionservice/services/DimeGetFileService\",\n \"http://schemas.cisco.com/ast/soap/action/#LogCollectionPort#GetOneFile\",\n soapEnvelopeGetOneFile(filePath)\n );\n\n const boundary = extractBoundary(contentType);\n if (!boundary) {\n // Some environments respond with a non-multipart body. Prefer treating non-XML bodies as raw file bytes.\n const ct = (contentType || \"\").toLowerCase();\n if (ct.includes(\"text/xml\") || ct.includes(\"application/soap+xml\")) {\n // Best-effort fault detection\n const asText = bytes.toString(\"utf8\");\n if (/\\bFault\\b/i.test(asText)) throw new Error(`CUCM DIME GetOneFile returned SOAP fault: ${asText}`);\n // If it's XML but not a fault, still return raw bytes.\n }\n return { server: target.host, filename: filePath, data: bytes };\n }\n\n const parts = parseMultipartRelated(bytes, boundary);\n if (parts.length === 0) throw new Error(\"DIME GetOneFile returned no multipart parts\");\n\n const nonXml = parts.find((p) => !isXmlContentType(p.contentType));\n if (!nonXml) throw new Error(\"DIME GetOneFile response missing non-XML file part\");\n\n return { server: target.host, filename: filePath, data: nonXml.body };\n}\n\nfunction isDimeMissingFileError(e: unknown): boolean {\n const msg = e instanceof Error ? e.message : String(e || \"\");\n // CUCM DIME commonly responds with HTTP 500 and a SOAP fault like:\n // \"FileName ... do not exist\"\n return /DIME HTTP 500/i.test(msg) && /do not exist/i.test(msg);\n}\n\nexport async function getOneFileWithRetry(\n hostOrUrl: string,\n filePath: string,\n opts?: {\n auth?: DimeAuth;\n port?: number;\n timeoutMs?: number;\n pollIntervalMs?: number;\n }\n): Promise<{ server: string; filename: string; data: Buffer; attempts: number; waitedMs: number }> {\n const timeoutMs = Math.max(1000, opts?.timeoutMs ?? 120_000);\n const pollIntervalMs = Math.max(250, opts?.pollIntervalMs ?? 2000);\n\n const start = Date.now();\n let attempts = 0;\n // Simple fixed-interval poll; CUCM can take time to flush capture files to disk.\n while (true) {\n attempts++;\n try {\n const r = await getOneFile(hostOrUrl, filePath, opts?.auth, opts?.port);\n return { ...r, attempts, waitedMs: Date.now() - start };\n } catch (e) {\n const elapsed = Date.now() - start;\n if (!isDimeMissingFileError(e) || elapsed >= timeoutMs) throw e;\n await new Promise((r) => setTimeout(r, pollIntervalMs));\n }\n }\n}\n\nexport async function getOneFileAnyWithRetry(\n hostOrUrl: string,\n filePaths: string[],\n opts?: {\n auth?: DimeAuth;\n port?: number;\n timeoutMs?: number;\n pollIntervalMs?: number;\n }\n): Promise<{ server: string; filename: string; data: Buffer; attempts: number; waitedMs: number }> {\n const paths = (filePaths || []).map(String).filter((p) => p.trim() !== \"\");\n if (paths.length === 0) throw new Error(\"getOneFileAnyWithRetry requires at least one filePath\");\n\n const timeoutMs = Math.max(1000, opts?.timeoutMs ?? 120_000);\n const pollIntervalMs = Math.max(250, opts?.pollIntervalMs ?? 2000);\n\n const start = Date.now();\n let attempts = 0;\n\n while (true) {\n attempts++;\n for (const p of paths) {\n try {\n const r = await getOneFile(hostOrUrl, p, opts?.auth, opts?.port);\n return { ...r, attempts, waitedMs: Date.now() - start };\n } catch (e) {\n if (!isDimeMissingFileError(e)) throw e;\n }\n }\n\n const elapsed = Date.now() - start;\n if (elapsed >= timeoutMs) {\n // If timeout, throw the same missing-file style error for the first candidate.\n throw new Error(`DIME HTTP 500: FileName ${paths[0]} do not exist (timed out after ${elapsed}ms)`);\n }\n\n await new Promise((r) => setTimeout(r, pollIntervalMs));\n }\n}\n\nexport function writeDownloadedFile(result: { server: string; filename: string; data: Buffer }, outFile?: string) {\n const baseName = result.filename.split(\"/\").filter(Boolean).pop() || \"cucm-file.bin\";\n const defaultDir = join(\"/tmp\", \"cucm-mcp\");\n const filePath = outFile || join(defaultDir, baseName);\n mkdirSync(dirname(filePath), { recursive: true });\n writeFileSync(filePath, result.data);\n return { filePath, bytes: result.data.length, baseName };\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }