@tplog/zendcli 0.2.2 → 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/README.md +7 -0
- package/dist/cli.js +99 -29
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -39,11 +39,18 @@ Environment variables take precedence over the config file.
|
|
|
39
39
|
## Usage
|
|
40
40
|
|
|
41
41
|
```bash
|
|
42
|
+
zend --help
|
|
43
|
+
zend tickets --help
|
|
44
|
+
zend email --help
|
|
45
|
+
zend follower --help
|
|
46
|
+
zend comments --help
|
|
42
47
|
zend tickets --limit 10
|
|
43
48
|
zend tickets --status open --limit 20
|
|
44
49
|
zend email user@example.com
|
|
45
50
|
zend email user@example.com --status unresolved
|
|
46
51
|
zend email user@example.com --status open,pending
|
|
52
|
+
zend follower
|
|
53
|
+
zend follower tp@dify.ai --limit 3
|
|
47
54
|
zend comments 12345
|
|
48
55
|
zend comments 12345 --type public
|
|
49
56
|
zend comments 12345 --json
|
package/dist/cli.js
CHANGED
|
@@ -3512,17 +3512,14 @@ function getConfig() {
|
|
|
3512
3512
|
}
|
|
3513
3513
|
|
|
3514
3514
|
// src/api.ts
|
|
3515
|
-
|
|
3516
|
-
const {
|
|
3517
|
-
const url = new URL(`https://${subdomain}.zendesk.com${path}`);
|
|
3518
|
-
if (params) {
|
|
3519
|
-
for (const [key, value] of Object.entries(params)) {
|
|
3520
|
-
url.searchParams.set(key, String(value));
|
|
3521
|
-
}
|
|
3522
|
-
}
|
|
3515
|
+
function getAuthHeader() {
|
|
3516
|
+
const { email, api_token } = getConfig();
|
|
3523
3517
|
const credentials = btoa(`${email}/token:${api_token}`);
|
|
3524
|
-
|
|
3525
|
-
|
|
3518
|
+
return `Basic ${credentials}`;
|
|
3519
|
+
}
|
|
3520
|
+
async function fetchJson(url) {
|
|
3521
|
+
const resp = await fetch(url, {
|
|
3522
|
+
headers: { Authorization: getAuthHeader() }
|
|
3526
3523
|
});
|
|
3527
3524
|
if (!resp.ok) {
|
|
3528
3525
|
const body = await resp.text();
|
|
@@ -3531,6 +3528,19 @@ async function apiGet(path, params) {
|
|
|
3531
3528
|
}
|
|
3532
3529
|
return resp.json();
|
|
3533
3530
|
}
|
|
3531
|
+
async function apiGet(path, params) {
|
|
3532
|
+
const { subdomain } = getConfig();
|
|
3533
|
+
const url = new URL(`https://${subdomain}.zendesk.com${path}`);
|
|
3534
|
+
if (params) {
|
|
3535
|
+
for (const [key, value] of Object.entries(params)) {
|
|
3536
|
+
url.searchParams.set(key, String(value));
|
|
3537
|
+
}
|
|
3538
|
+
}
|
|
3539
|
+
return fetchJson(url.toString());
|
|
3540
|
+
}
|
|
3541
|
+
async function apiGetUrl(url) {
|
|
3542
|
+
return fetchJson(url);
|
|
3543
|
+
}
|
|
3534
3544
|
|
|
3535
3545
|
// src/cli.ts
|
|
3536
3546
|
var program2 = new Command();
|
|
@@ -3599,6 +3609,67 @@ function parseStatusFilter(input) {
|
|
|
3599
3609
|
if (statuses.includes("unresolved")) return ["new", "open", "pending", "hold"];
|
|
3600
3610
|
return statuses;
|
|
3601
3611
|
}
|
|
3612
|
+
function buildSearchQuery(base, rawStatus, statusFilter, canUseSearchStatus) {
|
|
3613
|
+
let query = base;
|
|
3614
|
+
if (rawStatus === "unresolved") {
|
|
3615
|
+
query += " status<solved";
|
|
3616
|
+
} else if (canUseSearchStatus && statusFilter.length === 1) {
|
|
3617
|
+
query += ` status:${statusFilter[0]}`;
|
|
3618
|
+
}
|
|
3619
|
+
return query;
|
|
3620
|
+
}
|
|
3621
|
+
function printTickets(tickets, meta) {
|
|
3622
|
+
for (const t of tickets) {
|
|
3623
|
+
const status = (t.status || "").padEnd(8);
|
|
3624
|
+
const subject = t.subject || "(no subject)";
|
|
3625
|
+
console.log(`[${t.id}] ${status} ${subject}`);
|
|
3626
|
+
console.log(meta(t));
|
|
3627
|
+
console.log();
|
|
3628
|
+
}
|
|
3629
|
+
}
|
|
3630
|
+
async function findUserByEmail(email) {
|
|
3631
|
+
const data = await apiGet("/api/v2/users/search.json", {
|
|
3632
|
+
query: email
|
|
3633
|
+
});
|
|
3634
|
+
const user = (data.users || []).find((item) => item.email?.toLowerCase() === email.toLowerCase()) || data.users?.[0];
|
|
3635
|
+
if (!user) {
|
|
3636
|
+
console.error(`No Zendesk user found for email: ${email}`);
|
|
3637
|
+
process.exit(1);
|
|
3638
|
+
}
|
|
3639
|
+
return user;
|
|
3640
|
+
}
|
|
3641
|
+
async function findFollowerTickets(email, rawStatus, limit, sort) {
|
|
3642
|
+
const user = await findUserByEmail(email);
|
|
3643
|
+
const statusFilter = parseStatusFilter(rawStatus);
|
|
3644
|
+
const canUseSearchStatus = rawStatus === "unresolved" || statusFilter.length === 1;
|
|
3645
|
+
let url = new URL(`https://${getConfig().subdomain}.zendesk.com/api/v2/search.json`).toString();
|
|
3646
|
+
const params = new URLSearchParams({
|
|
3647
|
+
query: buildSearchQuery("type:ticket", rawStatus, statusFilter, canUseSearchStatus),
|
|
3648
|
+
sort_by: "updated_at",
|
|
3649
|
+
sort_order: sort,
|
|
3650
|
+
per_page: "100"
|
|
3651
|
+
});
|
|
3652
|
+
url += `?${params.toString()}`;
|
|
3653
|
+
const matches = [];
|
|
3654
|
+
while (url && matches.length < limit) {
|
|
3655
|
+
const data = await apiGetUrl(url);
|
|
3656
|
+
let tickets = data.results || [];
|
|
3657
|
+
if (statusFilter.length > 0 && rawStatus !== "unresolved" && !canUseSearchStatus) {
|
|
3658
|
+
tickets = tickets.filter((ticket) => statusFilter.includes((ticket.status || "").toLowerCase()));
|
|
3659
|
+
}
|
|
3660
|
+
for (const ticket of tickets) {
|
|
3661
|
+
const followers = ticket.follower_ids || [];
|
|
3662
|
+
if (followers.includes(user.id) && ticket.assignee_id !== user.id) {
|
|
3663
|
+
matches.push(ticket);
|
|
3664
|
+
}
|
|
3665
|
+
if (matches.length >= limit) {
|
|
3666
|
+
break;
|
|
3667
|
+
}
|
|
3668
|
+
}
|
|
3669
|
+
url = data.next_page || null;
|
|
3670
|
+
}
|
|
3671
|
+
return matches;
|
|
3672
|
+
}
|
|
3602
3673
|
program2.command("configure").description("Set up Zendesk credentials interactively").action(async () => {
|
|
3603
3674
|
const existing = loadConfig();
|
|
3604
3675
|
console.error("Zendesk CLI Configuration");
|
|
@@ -3622,24 +3693,15 @@ program2.command("tickets").description("List tickets from Zendesk, sorted by up
|
|
|
3622
3693
|
if (opts.status) {
|
|
3623
3694
|
tickets = tickets.filter((t) => t.status === opts.status);
|
|
3624
3695
|
}
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
console.log(`[${t.id}] ${status} ${subject}`);
|
|
3629
|
-
console.log(` priority=${t.priority ?? "-"} type=${t.type ?? "-"} created=${t.created_at}`);
|
|
3630
|
-
console.log();
|
|
3631
|
-
}
|
|
3696
|
+
printTickets(tickets.slice(0, limit), (ticket) => {
|
|
3697
|
+
return ` priority=${ticket.priority ?? "-"} type=${ticket.type ?? "-"} created=${ticket.created_at}`;
|
|
3698
|
+
});
|
|
3632
3699
|
});
|
|
3633
3700
|
program2.command("email <email>").description("Find tickets for a requester email").option("--status <status>", "Filter status: unresolved|all|new|open|pending|hold|solved|closed or comma-separated list", "unresolved").option("--limit <n>", "Max tickets to return", "20").option("--sort <order>", "Sort order (asc|desc)", "desc").option("--json", "Output raw JSON").action(async (email, opts) => {
|
|
3634
3701
|
const limit = parseInt(opts.limit, 10);
|
|
3635
3702
|
const statusFilter = parseStatusFilter(opts.status);
|
|
3636
3703
|
const canUseSearchStatus = opts.status === "unresolved" || statusFilter.length === 1;
|
|
3637
|
-
|
|
3638
|
-
if (opts.status === "unresolved") {
|
|
3639
|
-
query += " status<solved";
|
|
3640
|
-
} else if (canUseSearchStatus && statusFilter.length === 1) {
|
|
3641
|
-
query += ` status:${statusFilter[0]}`;
|
|
3642
|
-
}
|
|
3704
|
+
const query = buildSearchQuery(`type:ticket requester:${email}`, opts.status, statusFilter, canUseSearchStatus);
|
|
3643
3705
|
const data = await apiGet("/api/v2/search.json", {
|
|
3644
3706
|
query,
|
|
3645
3707
|
sort_by: "updated_at",
|
|
@@ -3655,13 +3717,21 @@ program2.command("email <email>").description("Find tickets for a requester emai
|
|
|
3655
3717
|
console.log(JSON.stringify(tickets, null, 2));
|
|
3656
3718
|
return;
|
|
3657
3719
|
}
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3720
|
+
printTickets(tickets, (ticket) => {
|
|
3721
|
+
return ` priority=${ticket.priority ?? "-"} type=${ticket.type ?? "-"} requester=${email} updated=${ticket.updated_at}`;
|
|
3722
|
+
});
|
|
3723
|
+
});
|
|
3724
|
+
program2.command("follower [email]").description("Find tickets where the user is a follower but not the assignee").option("--status <status>", "Filter status: unresolved|all|new|open|pending|hold|solved|closed or comma-separated list", "unresolved").option("--limit <n>", "Max tickets to return", "20").option("--sort <order>", "Sort order (asc|desc)", "desc").option("--json", "Output raw JSON").action(async (email, opts) => {
|
|
3725
|
+
const targetEmail = email || getConfig().email;
|
|
3726
|
+
const limit = parseInt(opts.limit, 10);
|
|
3727
|
+
const tickets = await findFollowerTickets(targetEmail, opts.status, limit, opts.sort);
|
|
3728
|
+
if (opts.json) {
|
|
3729
|
+
console.log(JSON.stringify(tickets, null, 2));
|
|
3730
|
+
return;
|
|
3664
3731
|
}
|
|
3732
|
+
printTickets(tickets, (ticket) => {
|
|
3733
|
+
return ` priority=${ticket.priority ?? "-"} type=${ticket.type ?? "-"} follower=${targetEmail} assignee=${ticket.assignee_id ?? "-"} updated=${ticket.updated_at}`;
|
|
3734
|
+
});
|
|
3665
3735
|
});
|
|
3666
3736
|
program2.command("comments <ticketId>").description("List comments/thread for a ticket (public=customer-facing, internal=agents only)").option("--type <type>", "Filter: all|public|internal", "all").option("--sort <order>", "Sort order (asc|desc)", "asc").option("--json", "Output raw JSON").action(async (ticketId, opts) => {
|
|
3667
3737
|
const data = await apiGet(
|