@tplog/zendcli 1.1.5 → 1.2.0

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 +33 -12
  2. package/dist/cli.js +47 -8
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,6 +1,13 @@
1
1
  # zendcli
2
2
 
3
- Minimal Zendesk CLI for listing tickets and reading ticket comment threads.
3
+ Minimal Zendesk CLI for listing tickets and reading ticket comment timelines.
4
+
5
+ `zend comments` returns a slim timeline optimized for terminal review and LLM summarization. By default it includes both public and private comments and emits only:
6
+
7
+ - `author`
8
+ - `time`
9
+ - `visibility` (`public` or `private`)
10
+ - `body`
4
11
 
5
12
  ## Install
6
13
 
@@ -30,7 +37,7 @@ This is the recommended option for temporary or CI usage.
30
37
 
31
38
  ```bash
32
39
  export ZENDESK_SUBDOMAIN="your-subdomain"
33
- export ZENDESK_EMAIL="foo@example.com"
40
+ export ZENDESK_EMAIL="user@example.com"
34
41
  export ZENDESK_API_TOKEN="your_zendesk_api_token"
35
42
  ```
36
43
 
@@ -40,22 +47,36 @@ Environment variables take precedence over the config file.
40
47
 
41
48
  ```bash
42
49
  zend --help
43
- zend tickets --help
50
+ zend ticket --help
44
51
  zend email --help
45
52
  zend follower --help
46
53
  zend comments --help
47
- zend tickets --limit 10
48
- zend tickets --status open --limit 20
49
- zend email foo@example.com
50
- zend email foo@example.com --status unresolved
51
- zend email foo@example.com --status open,pending
52
- zend follower
53
- zend follower foo@example.com --limit 3
54
+ zend 12345
55
+ zend ticket 12345 --raw
56
+ zend email user@example.com
57
+ zend email user@example.com --status unresolved
58
+ zend email user@example.com --status open,pending
59
+ zend follower user@example.com --limit 3
54
60
  zend comments 12345
55
- zend comments 12345 --type public
56
- zend comments 12345 --json
61
+ zend comments 12345 --visibility public
62
+ zend comments 12345 --visibility private --sort desc
57
63
  ```
58
64
 
65
+ ### Comments output shape
66
+
67
+ ```json
68
+ [
69
+ {
70
+ "author": "Support Agent",
71
+ "time": "2026-03-13T06:19:57Z",
72
+ "visibility": "public",
73
+ "body": "Reply text..."
74
+ }
75
+ ]
76
+ ```
77
+
78
+ Use `--visibility public` or `--visibility private` to filter the timeline.
79
+
59
80
  ## Development workflow
60
81
 
61
82
  ### Daily development
package/dist/cli.js CHANGED
@@ -3674,6 +3674,12 @@ function parseSort(input = "desc") {
3674
3674
  }
3675
3675
  return input;
3676
3676
  }
3677
+ function parseVisibility(input = "all") {
3678
+ if (input !== "all" && input !== "public" && input !== "private") {
3679
+ throw new CliError("invalid_args", "visibility must be all, public, or private", { visibility: input });
3680
+ }
3681
+ return input;
3682
+ }
3677
3683
  function buildSearchQuery(base, rawStatus, statusFilter) {
3678
3684
  if (rawStatus === "unresolved") return `${base} status<solved`;
3679
3685
  if (statusFilter.length === 1) return `${base} status:${statusFilter[0]}`;
@@ -3714,6 +3720,41 @@ async function fetchFollowerTickets(email, rawStatus, limit, sort) {
3714
3720
  }
3715
3721
  return matches;
3716
3722
  }
3723
+ async function fetchUsersByIds(ids) {
3724
+ const userMap = /* @__PURE__ */ new Map();
3725
+ const uniqueIds = [...new Set(ids)].filter((id) => Number.isFinite(id));
3726
+ for (let index = 0; index < uniqueIds.length; index += 100) {
3727
+ const chunk = uniqueIds.slice(index, index + 100);
3728
+ try {
3729
+ const data = await apiGet("/api/v2/users/show_many.json", { ids: chunk.join(",") });
3730
+ for (const user of data.users || []) {
3731
+ userMap.set(user.id, user);
3732
+ }
3733
+ } catch (error) {
3734
+ if (error instanceof ApiError && error.status === 404) continue;
3735
+ throw error;
3736
+ }
3737
+ }
3738
+ return userMap;
3739
+ }
3740
+ function filterCommentsByVisibility(comments, visibility) {
3741
+ if (visibility === "public") return comments.filter((comment) => comment.public === true);
3742
+ if (visibility === "private") return comments.filter((comment) => comment.public === false);
3743
+ return comments;
3744
+ }
3745
+ function normalizeCommentBody(comment) {
3746
+ return String(comment.plain_body || comment.body || "").replace(/&nbsp;/g, " ").replace(/\r/g, "").trim();
3747
+ }
3748
+ function toSlimComment(comment, userMap) {
3749
+ const authorId = typeof comment.author_id === "number" ? comment.author_id : null;
3750
+ const author = authorId ? userMap.get(authorId)?.name || `user:${authorId}` : "unknown";
3751
+ return {
3752
+ author,
3753
+ time: typeof comment.created_at === "string" ? comment.created_at : null,
3754
+ visibility: comment.public === true ? "public" : "private",
3755
+ body: normalizeCommentBody(comment)
3756
+ };
3757
+ }
3717
3758
  function apiBaseUrl() {
3718
3759
  const { subdomain } = getConfig();
3719
3760
  return `https://${subdomain}.zendesk.com`;
@@ -3793,22 +3834,20 @@ program2.command("follower <email>").description("Find tickets where the user is
3793
3834
  const tickets = await fetchFollowerTickets(email, opts.status, limit, sort);
3794
3835
  printJson(tickets);
3795
3836
  }));
3796
- program2.command("comments <ticketId>").description("List comments for a ticket").option("--type <type>", "all|public|internal", "all").option("--sort <order>", "Sort order (asc|desc)", "asc").action(run(async (ticketId, opts) => {
3837
+ program2.command("comments <ticketId>").description("List slim comment timeline for a ticket").option("--visibility <visibility>", "all|public|private", "all").option("--sort <order>", "Sort order (asc|desc)", "asc").action(run(async (ticketId, opts) => {
3797
3838
  if (!DIGITS_RE.test(ticketId)) {
3798
3839
  throw new CliError("invalid_args", "ticket id must be numeric", { ticketId });
3799
3840
  }
3800
3841
  const sort = parseSort(opts.sort);
3801
- if (!["all", "public", "internal"].includes(opts.type)) {
3802
- throw new CliError("invalid_args", "type must be all, public, or internal", { type: opts.type });
3803
- }
3842
+ const visibility = parseVisibility(opts.visibility);
3804
3843
  const data = await apiGet(
3805
3844
  `/api/v2/tickets/${ticketId}/comments.json`,
3806
3845
  { sort_order: sort, per_page: 100 }
3807
3846
  );
3808
- let comments = data.comments || [];
3809
- if (opts.type === "public") comments = comments.filter((comment) => comment.public === true);
3810
- if (opts.type === "internal") comments = comments.filter((comment) => comment.public === false);
3811
- printJson(comments);
3847
+ const comments = filterCommentsByVisibility(data.comments || [], visibility);
3848
+ const authorIds = comments.map((comment) => comment.author_id).filter((authorId) => typeof authorId === "number");
3849
+ const userMap = await fetchUsersByIds(authorIds);
3850
+ printJson(comments.map((comment) => toSlimComment(comment, userMap)));
3812
3851
  }));
3813
3852
  function preprocessArgv(argv) {
3814
3853
  const firstArg = argv[2];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tplog/zendcli",
3
- "version": "1.1.5",
3
+ "version": "1.2.0",
4
4
  "description": "Minimal Zendesk CLI for tickets and comments",
5
5
  "repository": {
6
6
  "type": "git",