zd-agent-cli 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,360 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getZendeskPage = getZendeskPage;
4
+ exports.waitForAgentReady = waitForAgentReady;
5
+ exports.openTicketByIdDom = openTicketByIdDom;
6
+ exports.readCurrentTicketDom = readCurrentTicketDom;
7
+ exports.openQueueByNameDom = openQueueByNameDom;
8
+ exports.openQueueByPathDom = openQueueByPathDom;
9
+ exports.readQueueTicketsDom = readQueueTicketsDom;
10
+ exports.runTicketSearchDom = runTicketSearchDom;
11
+ const browser_cdp_1 = require("./browser-cdp");
12
+ const constants_1 = require("./constants");
13
+ const util_1 = require("./util");
14
+ function extractZendeskHost(urlText = '') {
15
+ try {
16
+ const parsed = new URL(urlText);
17
+ return /zendesk\.com$/i.test(parsed.hostname) ? parsed.hostname : '';
18
+ }
19
+ catch (_) {
20
+ return '';
21
+ }
22
+ }
23
+ function baseUrlFromPage(page) {
24
+ try {
25
+ const parsed = new URL(page.url());
26
+ return `${parsed.protocol}//${parsed.host}`;
27
+ }
28
+ catch (_) {
29
+ return '';
30
+ }
31
+ }
32
+ async function getZendeskPage(context, startUrl, background) {
33
+ const targetHost = extractZendeskHost(startUrl);
34
+ const targetPattern = targetHost
35
+ ? new RegExp(`https?://${targetHost.replace(/\./g, '\\.')}/agent`, 'i')
36
+ : /zendesk\.com\/agent/i;
37
+ let page = context.pages().find((p) => targetPattern.test(p.url()));
38
+ if (!page) {
39
+ if (!startUrl) {
40
+ throw new Error('No Zendesk agent tab found. Provide --start-path or set domain/startPath in zendesk config.');
41
+ }
42
+ page = await context.newPage();
43
+ await page.goto(startUrl, { waitUntil: 'domcontentloaded', timeout: 45000 });
44
+ }
45
+ else if (!/\/agent\//i.test(page.url())) {
46
+ if (!startUrl) {
47
+ throw new Error('Found Zendesk tab but not an agent page. Provide --start-path or set domain/startPath in zendesk config.');
48
+ }
49
+ await page.goto(startUrl, { waitUntil: 'domcontentloaded', timeout: 45000 });
50
+ }
51
+ if (!background) {
52
+ await page.bringToFront().catch(() => undefined);
53
+ }
54
+ await (0, util_1.sleep)(500);
55
+ return page;
56
+ }
57
+ async function waitForAgentReady(page, uiWaitMs = constants_1.DEFAULT_UI_WAIT_MS) {
58
+ await (0, browser_cdp_1.prepareInteractionContext)(page, uiWaitMs);
59
+ await page.waitForLoadState('domcontentloaded', { timeout: 30000 }).catch(() => undefined);
60
+ await page
61
+ .locator('a[href*="/agent/filters/"], a[href*="/agent/tickets/"], main')
62
+ .first()
63
+ .waitFor({ timeout: 15000 })
64
+ .catch(() => undefined);
65
+ await (0, util_1.sleep)(Math.max(500, Math.floor(uiWaitMs / 2)));
66
+ }
67
+ async function openTicketByIdDom(page, ticketId, uiWaitMs = constants_1.DEFAULT_UI_WAIT_MS) {
68
+ const id = String(ticketId || '').replace(/\D+/g, '');
69
+ if (!id) {
70
+ throw new Error('Ticket id is required.');
71
+ }
72
+ const url = `${baseUrlFromPage(page)}/agent/tickets/${id}`;
73
+ await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 45000 });
74
+ await waitForAgentReady(page, uiWaitMs);
75
+ if (!new RegExp(`/agent/tickets/${id}(?:$|[/?#])`, 'i').test(page.url())) {
76
+ throw new Error(`Ticket ${id} was not opened. Current URL: ${page.url()}`);
77
+ }
78
+ }
79
+ async function readCurrentTicketDom(page, options = {}) {
80
+ const count = Math.max(1, Number(options.count) || 10);
81
+ return page.evaluate((maxComments) => {
82
+ function cleanText(text) {
83
+ return (text || '').replace(/\s+/g, ' ').trim();
84
+ }
85
+ const pageUrl = location.href;
86
+ const ticketId = (pageUrl.match(/\/agent\/tickets\/(\d+)/i) || [null, null])[1];
87
+ const subject = cleanText(document.querySelector('[data-test-id="ticket-pane-subject"], [data-garden-id="forms.input"], h1, [title]')?.textContent || '') || null;
88
+ function readField(labelNeedle) {
89
+ const rows = Array.from(document.querySelectorAll('label, dt, div, span'));
90
+ const row = rows.find((n) => cleanText(n.textContent || '').toLowerCase() === labelNeedle.toLowerCase());
91
+ if (!row)
92
+ return null;
93
+ const holder = row.closest('div, dl') || row.parentElement;
94
+ if (!holder)
95
+ return null;
96
+ const value = cleanText(holder.querySelector('button, [data-garden-id="dropdowns.menu_wrapper"], [aria-haspopup="listbox"], dd, [title]')?.textContent || '');
97
+ return value || null;
98
+ }
99
+ const status = readField('Status');
100
+ const priority = readField('Priority');
101
+ const assignee = readField('Assignee');
102
+ const requester = readField('Requester');
103
+ const tags = Array.from(document.querySelectorAll('a[href*="tags"], [data-test-id="ticket-tags"] span, [data-garden-id="tags.item"]'))
104
+ .map((n) => cleanText(n.textContent || ''))
105
+ .filter(Boolean)
106
+ .slice(0, 40);
107
+ const commentNodes = Array.from(document.querySelectorAll('[data-test-id="omni-log-comment-item"], article, [role="article"], [data-test-id="ticket-pane-comment"]'));
108
+ const comments = [];
109
+ for (const node of commentNodes) {
110
+ const text = cleanText(node.querySelector('[data-test-id="rich-text"], [data-test-id="comment-body"], .zd-comment, p, div')?.textContent ||
111
+ node.textContent ||
112
+ '');
113
+ if (!text)
114
+ continue;
115
+ const author = cleanText(node.querySelector('[data-test-id="omni-log-comment-author"], [data-test-id="author"], h4, strong')?.textContent || '') || null;
116
+ const time = cleanText(node.querySelector('time, [data-test-id="omni-log-comment-time"]')?.textContent || '') || null;
117
+ comments.push({ author, time, text: text.slice(0, 4000) });
118
+ if (comments.length >= maxComments) {
119
+ break;
120
+ }
121
+ }
122
+ return {
123
+ pageUrl,
124
+ pageTitle: document.title,
125
+ ticketId,
126
+ subject,
127
+ status,
128
+ priority,
129
+ assignee,
130
+ requester,
131
+ tags,
132
+ comments,
133
+ source: 'dom'
134
+ };
135
+ }, count);
136
+ }
137
+ async function openQueueByNameDom(page, queueName, uiWaitMs = constants_1.DEFAULT_UI_WAIT_MS) {
138
+ const target = (0, util_1.clean)(queueName).toLowerCase();
139
+ if (!target) {
140
+ throw new Error('Queue name is required.');
141
+ }
142
+ await waitForAgentReady(page, uiWaitMs);
143
+ if (/^\d+$/.test(target)) {
144
+ const directUrl = `${baseUrlFromPage(page)}/agent/filters/${target}`;
145
+ await page.goto(directUrl, { waitUntil: 'domcontentloaded', timeout: 45000 });
146
+ await waitForAgentReady(page, uiWaitMs);
147
+ return { score: 100, name: queueName, href: directUrl, source: 'id-dom' };
148
+ }
149
+ const queue = await page.evaluate((targetName) => {
150
+ function cleanText(text) {
151
+ return (text || '').replace(/\s+/g, ' ').trim();
152
+ }
153
+ const links = Array.from(document.querySelectorAll('a[href*="/agent/filters/"]'));
154
+ let best = null;
155
+ for (const link of links) {
156
+ const name = cleanText(link.textContent || '');
157
+ if (!name)
158
+ continue;
159
+ const low = name.toLowerCase();
160
+ const href = link.getAttribute('href') || '';
161
+ let score = -1;
162
+ if (low === targetName)
163
+ score = 100;
164
+ else if (low.includes(targetName))
165
+ score = 80;
166
+ else if (targetName.includes(low))
167
+ score = 60;
168
+ if (score >= 0 && (!best || score > best.score)) {
169
+ best = {
170
+ score,
171
+ name,
172
+ href: new URL(href, location.origin).toString(),
173
+ source: 'dom'
174
+ };
175
+ }
176
+ }
177
+ return best;
178
+ }, target);
179
+ if (!queue || !queue.href) {
180
+ throw new Error(`Could not find queue: ${queueName}`);
181
+ }
182
+ await page.goto(queue.href, { waitUntil: 'domcontentloaded', timeout: 45000 });
183
+ await waitForAgentReady(page, uiWaitMs);
184
+ return queue;
185
+ }
186
+ async function openQueueByPathDom(page, queuePath, uiWaitMs = constants_1.DEFAULT_UI_WAIT_MS) {
187
+ const targetPath = (0, util_1.clean)(queuePath);
188
+ if (!/^\/agent\//i.test(targetPath)) {
189
+ throw new Error(`Invalid queue path "${targetPath}". Queue paths must begin with "/agent/".`);
190
+ }
191
+ const baseUrl = baseUrlFromPage(page);
192
+ const target = targetPath ? `${baseUrl}${targetPath}` : '';
193
+ if (!target) {
194
+ throw new Error('Queue path is required.');
195
+ }
196
+ await page.goto(target, { waitUntil: 'domcontentloaded', timeout: 45000 });
197
+ await waitForAgentReady(page, uiWaitMs);
198
+ return { score: 100, name: null, href: target, source: 'config-path' };
199
+ }
200
+ async function readQueueTicketsDom(page, options = {}) {
201
+ const count = Math.max(1, Number(options.count) || 20);
202
+ return page.evaluate((maxCount) => {
203
+ function cleanText(text) {
204
+ return (text || '').replace(/\s+/g, ' ').trim();
205
+ }
206
+ const queueName = cleanText(document.querySelector('h1, [data-test-id="views_table_header"]')?.textContent || '') || null;
207
+ const anchors = Array.from(document.querySelectorAll('a[href*="/agent/tickets/"]'));
208
+ const tickets = [];
209
+ const seen = new Set();
210
+ for (const a of anchors) {
211
+ const href = a.getAttribute('href') || '';
212
+ const m = href.match(/\/agent\/tickets\/(\d+)/i);
213
+ if (!m)
214
+ continue;
215
+ const ticketId = m[1];
216
+ if (seen.has(ticketId))
217
+ continue;
218
+ seen.add(ticketId);
219
+ const row = a.closest('tr, li, article, div') || a;
220
+ const title = cleanText(a.textContent || '') || null;
221
+ const status = cleanText(row.querySelector('[data-test-id*="status"], [title*="status" i], [aria-label*="status" i]')?.textContent || '') || null;
222
+ const requester = cleanText(row.querySelector('[data-test-id*="requester"], [title*="requester" i]')?.textContent || '') || null;
223
+ tickets.push({
224
+ ticketId,
225
+ subject: title,
226
+ status,
227
+ requester,
228
+ url: new URL(href, location.origin).toString()
229
+ });
230
+ if (tickets.length >= maxCount) {
231
+ break;
232
+ }
233
+ }
234
+ return {
235
+ pageUrl: location.href,
236
+ pageTitle: document.title,
237
+ queueName,
238
+ source: 'dom',
239
+ resultCount: tickets.length,
240
+ tickets
241
+ };
242
+ }, count);
243
+ }
244
+ async function submitSearchFromUi(page, query, uiWaitMs = constants_1.DEFAULT_UI_WAIT_MS) {
245
+ await waitForAgentReady(page, uiWaitMs);
246
+ await (0, browser_cdp_1.prepareInteractionContext)(page, uiWaitMs);
247
+ const triggerSelectors = [
248
+ 'button[aria-label*="Search" i]',
249
+ '[data-test-id*="search-launcher"]',
250
+ '[data-test-id*="search"] button'
251
+ ];
252
+ for (const selector of triggerSelectors) {
253
+ const trigger = page.locator(selector).first();
254
+ const count = await trigger.count();
255
+ if (!count) {
256
+ continue;
257
+ }
258
+ await trigger.click({ timeout: 2000 }).catch(() => undefined);
259
+ await (0, util_1.sleep)(Math.max(200, Math.floor(uiWaitMs / 3)));
260
+ }
261
+ await page.keyboard.press('Meta+K').catch(async () => page.keyboard.press('Control+K')).catch(() => undefined);
262
+ await (0, util_1.sleep)(Math.max(200, Math.floor(uiWaitMs / 3)));
263
+ const focused = await page.evaluate(() => {
264
+ function isVisible(el) {
265
+ if (!el || !(el instanceof HTMLElement))
266
+ return false;
267
+ const rect = el.getBoundingClientRect();
268
+ const style = window.getComputedStyle(el);
269
+ return rect.width > 0 && rect.height > 0 && style.visibility !== 'hidden' && style.display !== 'none';
270
+ }
271
+ const selectors = [
272
+ 'input[type="search"]',
273
+ 'input[role="combobox"]',
274
+ 'input[placeholder*="Search" i]',
275
+ 'input[aria-label*="Search" i]',
276
+ 'textarea[aria-label*="Search" i]',
277
+ '[role="searchbox"]',
278
+ '[role="combobox"][contenteditable="true"]',
279
+ '[contenteditable="true"][aria-label*="Search" i]',
280
+ '[data-test-id*="search"] input',
281
+ '[data-test-id*="search"] [contenteditable="true"]'
282
+ ];
283
+ let target = null;
284
+ for (const selector of selectors) {
285
+ const nodes = Array.from(document.querySelectorAll(selector));
286
+ const visibleNode = nodes.find((n) => isVisible(n));
287
+ if (visibleNode) {
288
+ target = visibleNode;
289
+ break;
290
+ }
291
+ }
292
+ if (!target) {
293
+ return false;
294
+ }
295
+ target.focus();
296
+ target.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
297
+ return true;
298
+ });
299
+ if (!focused) {
300
+ return false;
301
+ }
302
+ await page.keyboard.press('Meta+A').catch(async () => page.keyboard.press('Control+A')).catch(() => undefined);
303
+ await page.keyboard.press('Backspace').catch(() => undefined);
304
+ await page.keyboard.type(query, { delay: 25 });
305
+ await page.keyboard.press('Enter');
306
+ await page.waitForLoadState('domcontentloaded', { timeout: 15000 }).catch(() => undefined);
307
+ await (0, util_1.sleep)(Math.max(700, Math.floor(uiWaitMs / 2)));
308
+ return true;
309
+ }
310
+ async function runTicketSearchDom(page, query, count = 20, uiWaitMs = constants_1.DEFAULT_UI_WAIT_MS) {
311
+ const q = (0, util_1.clean)(query);
312
+ if (!q) {
313
+ throw new Error('Search query is required.');
314
+ }
315
+ const uiSubmitted = await submitSearchFromUi(page, q, uiWaitMs);
316
+ if (!uiSubmitted) {
317
+ const url = `${baseUrlFromPage(page)}/agent/search/1?query=${encodeURIComponent(q)}`;
318
+ await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 45000 });
319
+ await waitForAgentReady(page, uiWaitMs);
320
+ }
321
+ const maxCount = Math.max(1, Number(count) || 20);
322
+ return page.evaluate(({ maxCount, searchQuery }) => {
323
+ function cleanText(text) {
324
+ return (text || '').replace(/\s+/g, ' ').trim();
325
+ }
326
+ const rows = Array.from(document.querySelectorAll('a[href*="/agent/tickets/"]'));
327
+ const seen = new Set();
328
+ const results = [];
329
+ for (const row of rows) {
330
+ const href = row.getAttribute('href') || '';
331
+ const m = href.match(/\/agent\/tickets\/(\d+)/i);
332
+ if (!m)
333
+ continue;
334
+ const ticketId = m[1];
335
+ if (seen.has(ticketId))
336
+ continue;
337
+ seen.add(ticketId);
338
+ const container = row.closest('article, li, tr, div') || row;
339
+ const title = cleanText(row.textContent || '') || null;
340
+ const snippet = cleanText(container.querySelector('p, [data-test-id*="snippet"], [data-test-id*="description"]')?.textContent || '') || null;
341
+ results.push({
342
+ ticketId,
343
+ title,
344
+ snippet,
345
+ url: new URL(href, location.origin).toString()
346
+ });
347
+ if (results.length >= maxCount) {
348
+ break;
349
+ }
350
+ }
351
+ return {
352
+ pageUrl: location.href,
353
+ pageTitle: document.title,
354
+ query: searchQuery,
355
+ source: 'dom',
356
+ resultCount: results.length,
357
+ results
358
+ };
359
+ }, { maxCount, searchQuery: q });
360
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ const constants = __importStar(require("./constants"));
37
+ const runtime = __importStar(require("./runtime"));
38
+ const config = __importStar(require("./config"));
39
+ const automation = __importStar(require("./automation"));
40
+ const browserCdp = __importStar(require("./browser-cdp"));
41
+ const storage = __importStar(require("./storage"));
42
+ const core = {
43
+ ...constants,
44
+ resolveGlobalOpts: runtime.resolveGlobalOpts,
45
+ resolveQueueInput: config.resolveQueueInput,
46
+ withZendeskBrowser: runtime.withZendeskBrowser,
47
+ emitResult: runtime.emitResult,
48
+ openTicketById: automation.openTicketById,
49
+ readCurrentTicket: automation.readCurrentTicket,
50
+ openQueueByName: automation.openQueueByName,
51
+ readQueueTickets: automation.readQueueTickets,
52
+ runTicketSearch: automation.runTicketSearch,
53
+ readCurrentUser: automation.readCurrentUser,
54
+ isCdpReachable: browserCdp.isCdpReachable,
55
+ checkCdpOwnership: browserCdp.checkCdpOwnership,
56
+ readCachedTicket: storage.readCachedTicket
57
+ };
58
+ exports.default = core;