@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.
- package/README.md +33 -12
- package/dist/cli.js +47 -8
- 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
|
|
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="
|
|
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
|
|
50
|
+
zend ticket --help
|
|
44
51
|
zend email --help
|
|
45
52
|
zend follower --help
|
|
46
53
|
zend comments --help
|
|
47
|
-
zend
|
|
48
|
-
zend
|
|
49
|
-
zend email
|
|
50
|
-
zend email
|
|
51
|
-
zend email
|
|
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 --
|
|
56
|
-
zend comments 12345 --
|
|
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(/ /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
|
|
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
|
-
|
|
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
|
-
|
|
3809
|
-
|
|
3810
|
-
|
|
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];
|