@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.
Files changed (3) hide show
  1. package/README.md +7 -0
  2. package/dist/cli.js +99 -29
  3. 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
- async function apiGet(path, params) {
3516
- const { subdomain, email, api_token } = getConfig();
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
- const resp = await fetch(url.toString(), {
3525
- headers: { Authorization: `Basic ${credentials}` }
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
- for (const t of tickets.slice(0, limit)) {
3626
- const status = (t.status || "").padEnd(8);
3627
- const subject = t.subject || "(no subject)";
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
- let query = `type:ticket requester:${email}`;
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
- for (const t of tickets) {
3659
- const status = (t.status || "").padEnd(8);
3660
- const subject = t.subject || "(no subject)";
3661
- console.log(`[${t.id}] ${status} ${subject}`);
3662
- console.log(` priority=${t.priority ?? "-"} type=${t.type ?? "-"} requester=${email} updated=${t.updated_at}`);
3663
- console.log();
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(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tplog/zendcli",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "Minimal Zendesk CLI for tickets and comments",
5
5
  "repository": {
6
6
  "type": "git",