@stephendolan/helpscout-cli 1.0.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/dist/cli.js ADDED
@@ -0,0 +1,918 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { Command as Command7 } from "commander";
5
+
6
+ // src/lib/output.ts
7
+ import { convert } from "html-to-text";
8
+ var globalOutputOptions = {};
9
+ function setOutputOptions(options) {
10
+ globalOutputOptions = options;
11
+ }
12
+ function isObject(value) {
13
+ return typeof value === "object" && value !== null && !Array.isArray(value);
14
+ }
15
+ function htmlToPlainText(html) {
16
+ const text = convert(html, {
17
+ wordwrap: false,
18
+ preserveNewlines: false,
19
+ selectors: [
20
+ { selector: "a", options: { ignoreHref: true } },
21
+ { selector: "img", format: "skip" }
22
+ ]
23
+ });
24
+ return text.replace(/\n{3,}/g, "\n\n").replace(/[ \t]+/g, " ").trim();
25
+ }
26
+ function convertBodiesToPlainText(data) {
27
+ if (Array.isArray(data)) {
28
+ return data.map(convertBodiesToPlainText);
29
+ }
30
+ if (isObject(data)) {
31
+ const result = {};
32
+ for (const [key, value] of Object.entries(data)) {
33
+ if (key === "body" && typeof value === "string") {
34
+ result[key] = htmlToPlainText(value);
35
+ } else {
36
+ result[key] = convertBodiesToPlainText(value);
37
+ }
38
+ }
39
+ return result;
40
+ }
41
+ return data;
42
+ }
43
+ function stripMetadata(data) {
44
+ if (Array.isArray(data)) {
45
+ return data.map(stripMetadata);
46
+ }
47
+ if (isObject(data)) {
48
+ const result = {};
49
+ for (const [key, value] of Object.entries(data)) {
50
+ if (key === "_links" || key === "_embedded") {
51
+ continue;
52
+ }
53
+ result[key] = stripMetadata(value);
54
+ }
55
+ return result;
56
+ }
57
+ return data;
58
+ }
59
+ function stripTagStyles(data) {
60
+ if (Array.isArray(data)) {
61
+ return data.map(stripTagStyles);
62
+ }
63
+ if (isObject(data)) {
64
+ const result = {};
65
+ const isTag = "id" in data && "name" in data && "slug" in data;
66
+ for (const [key, value] of Object.entries(data)) {
67
+ if (isTag && (key === "color" || key === "styles")) {
68
+ continue;
69
+ }
70
+ result[key] = stripTagStyles(value);
71
+ }
72
+ return result;
73
+ }
74
+ return data;
75
+ }
76
+ function selectFields(data, fields) {
77
+ if (Array.isArray(data)) {
78
+ return data.map((item) => selectFields(item, fields));
79
+ }
80
+ if (isObject(data)) {
81
+ const hasRequestedFields = fields.some((f) => f in data);
82
+ if (hasRequestedFields) {
83
+ const result2 = {};
84
+ for (const field of fields) {
85
+ if (field in data) {
86
+ result2[field] = data[field];
87
+ }
88
+ }
89
+ return result2;
90
+ }
91
+ const result = {};
92
+ for (const [key, value] of Object.entries(data)) {
93
+ result[key] = selectFields(value, fields);
94
+ }
95
+ return result;
96
+ }
97
+ return data;
98
+ }
99
+ function outputJson(data, options = {}) {
100
+ const mergedOptions = { ...globalOutputOptions, ...options };
101
+ let processed = data;
102
+ if (mergedOptions.slim) {
103
+ processed = stripMetadata(processed);
104
+ }
105
+ if (mergedOptions.plain) {
106
+ processed = convertBodiesToPlainText(processed);
107
+ }
108
+ processed = stripTagStyles(processed);
109
+ if (mergedOptions.fields) {
110
+ const fieldList = mergedOptions.fields.split(",").map((f) => f.trim());
111
+ processed = selectFields(processed, fieldList);
112
+ }
113
+ const jsonString = mergedOptions.compact ? JSON.stringify(processed) : JSON.stringify(processed, null, 2);
114
+ console.log(jsonString);
115
+ }
116
+
117
+ // src/commands/auth.ts
118
+ import { Command } from "commander";
119
+
120
+ // src/lib/auth.ts
121
+ import { Entry } from "@napi-rs/keyring";
122
+
123
+ // src/lib/config.ts
124
+ import Conf from "conf";
125
+ var store = new Conf({
126
+ projectName: "helpscout-cli"
127
+ });
128
+ var config = {
129
+ getDefaultMailbox() {
130
+ return store.get("defaultMailbox") || process.env.HELPSCOUT_MAILBOX_ID;
131
+ },
132
+ setDefaultMailbox(mailboxId) {
133
+ store.set("defaultMailbox", mailboxId);
134
+ },
135
+ clearDefaultMailbox() {
136
+ store.delete("defaultMailbox");
137
+ },
138
+ clear() {
139
+ store.clear();
140
+ }
141
+ };
142
+
143
+ // src/lib/auth.ts
144
+ var SERVICE_NAME = "helpscout-cli";
145
+ var ACCESS_TOKEN_ACCOUNT = "access-token";
146
+ var REFRESH_TOKEN_ACCOUNT = "refresh-token";
147
+ var APP_ID_ACCOUNT = "app-id";
148
+ var APP_SECRET_ACCOUNT = "app-secret";
149
+ var KEYRING_UNAVAILABLE_ERROR = "Keychain storage unavailable. Cannot store credentials securely.\nOn Linux, install libsecret: sudo apt-get install libsecret-1-dev\nThen reinstall: npm install -g @stephendolan/helpscout-cli\nAlternatively, use HELPSCOUT_APP_ID and HELPSCOUT_APP_SECRET environment variables.";
150
+ var keyringCache = /* @__PURE__ */ new Map();
151
+ function getKeyring(account) {
152
+ if (keyringCache.has(account)) {
153
+ return keyringCache.get(account);
154
+ }
155
+ try {
156
+ const entry = new Entry(SERVICE_NAME, account);
157
+ keyringCache.set(account, entry);
158
+ return entry;
159
+ } catch {
160
+ keyringCache.set(account, null);
161
+ return null;
162
+ }
163
+ }
164
+ async function getPassword(account) {
165
+ const entry = getKeyring(account);
166
+ if (entry) {
167
+ try {
168
+ return entry.getPassword();
169
+ } catch {
170
+ return null;
171
+ }
172
+ }
173
+ return null;
174
+ }
175
+ async function setPassword(account, value) {
176
+ const entry = getKeyring(account);
177
+ if (!entry) {
178
+ throw new Error(KEYRING_UNAVAILABLE_ERROR);
179
+ }
180
+ entry.setPassword(value);
181
+ }
182
+ async function deletePassword(account) {
183
+ const entry = getKeyring(account);
184
+ if (entry) {
185
+ return entry.deletePassword();
186
+ }
187
+ return false;
188
+ }
189
+ var AuthManager = class {
190
+ async getAccessToken() {
191
+ return getPassword(ACCESS_TOKEN_ACCOUNT);
192
+ }
193
+ async setAccessToken(token) {
194
+ return setPassword(ACCESS_TOKEN_ACCOUNT, token);
195
+ }
196
+ async getRefreshToken() {
197
+ return getPassword(REFRESH_TOKEN_ACCOUNT);
198
+ }
199
+ async setRefreshToken(token) {
200
+ return setPassword(REFRESH_TOKEN_ACCOUNT, token);
201
+ }
202
+ async getAppId() {
203
+ const keychainValue = await getPassword(APP_ID_ACCOUNT);
204
+ return keychainValue || process.env.HELPSCOUT_APP_ID || null;
205
+ }
206
+ async setAppId(appId) {
207
+ return setPassword(APP_ID_ACCOUNT, appId);
208
+ }
209
+ async getAppSecret() {
210
+ const keychainValue = await getPassword(APP_SECRET_ACCOUNT);
211
+ return keychainValue || process.env.HELPSCOUT_APP_SECRET || null;
212
+ }
213
+ async setAppSecret(appSecret) {
214
+ return setPassword(APP_SECRET_ACCOUNT, appSecret);
215
+ }
216
+ async isAuthenticated() {
217
+ return await this.getAccessToken() !== null;
218
+ }
219
+ async logout() {
220
+ await deletePassword(ACCESS_TOKEN_ACCOUNT);
221
+ await deletePassword(REFRESH_TOKEN_ACCOUNT);
222
+ config.clearDefaultMailbox();
223
+ }
224
+ async clearAll() {
225
+ await deletePassword(ACCESS_TOKEN_ACCOUNT);
226
+ await deletePassword(REFRESH_TOKEN_ACCOUNT);
227
+ await deletePassword(APP_ID_ACCOUNT);
228
+ await deletePassword(APP_SECRET_ACCOUNT);
229
+ config.clear();
230
+ }
231
+ };
232
+ var auth = new AuthManager();
233
+
234
+ // src/lib/errors.ts
235
+ var HelpScoutCliError = class extends Error {
236
+ constructor(message, statusCode) {
237
+ super(message);
238
+ this.statusCode = statusCode;
239
+ this.name = "HelpScoutCliError";
240
+ }
241
+ };
242
+ var HelpScoutApiError = class extends Error {
243
+ constructor(message, apiError, statusCode) {
244
+ super(message);
245
+ this.apiError = apiError;
246
+ this.statusCode = statusCode;
247
+ this.name = "HelpScoutApiError";
248
+ }
249
+ };
250
+ var ERROR_STATUS_CODES = {
251
+ bad_request: 400,
252
+ unauthorized: 401,
253
+ forbidden: 403,
254
+ not_found: 404,
255
+ conflict: 409,
256
+ too_many_requests: 429,
257
+ internal_server_error: 500,
258
+ service_unavailable: 503
259
+ };
260
+ function sanitizeErrorMessage(message) {
261
+ const sensitivePatterns = [
262
+ /Bearer\s+[\w\-._~+/]+=*/gi,
263
+ /token[=:]\s*[\w\-._~+/]+=*/gi,
264
+ /client[_-]?secret[=:]\s*[\w\-._~+/]+=*/gi,
265
+ /authorization:\s*bearer\s+[\w\-._~+/]+=*/gi
266
+ ];
267
+ let sanitized = message;
268
+ for (const pattern of sensitivePatterns) {
269
+ sanitized = sanitized.replace(pattern, "[REDACTED]");
270
+ }
271
+ return sanitized.length > 500 ? sanitized.substring(0, 500) + "..." : sanitized;
272
+ }
273
+ function isErrorObject(value) {
274
+ return typeof value === "object" && value !== null;
275
+ }
276
+ function sanitizeApiError(error) {
277
+ if (!isErrorObject(error)) {
278
+ return {
279
+ name: "api_error",
280
+ detail: "An error occurred"
281
+ };
282
+ }
283
+ const apiError = error;
284
+ let detail = "An error occurred";
285
+ if (apiError.error_description) {
286
+ detail = apiError.error_description;
287
+ } else if (apiError.message) {
288
+ detail = apiError.message;
289
+ } else if (apiError._embedded?.errors?.length) {
290
+ detail = apiError._embedded.errors.map((e) => e.message || e.path).filter(Boolean).join("; ");
291
+ }
292
+ return {
293
+ name: apiError.error || "api_error",
294
+ detail: sanitizeErrorMessage(detail)
295
+ };
296
+ }
297
+ function formatErrorResponse(name, detail, statusCode) {
298
+ const hint = name === "too_many_requests" ? "Help Scout API limit: 200 requests/minute. Wait a moment and retry." : void 0;
299
+ const response = {
300
+ error: { name, detail, statusCode }
301
+ };
302
+ if (hint) {
303
+ response.hint = hint;
304
+ }
305
+ outputJson(response);
306
+ process.exit(1);
307
+ }
308
+ function handleHelpScoutError(error) {
309
+ if (error instanceof HelpScoutCliError) {
310
+ const sanitized = sanitizeErrorMessage(error.message);
311
+ formatErrorResponse("cli_error", sanitized, error.statusCode || 1);
312
+ }
313
+ if (error instanceof HelpScoutApiError) {
314
+ const hsError = sanitizeApiError(error.apiError);
315
+ formatErrorResponse(
316
+ hsError.name,
317
+ hsError.detail,
318
+ error.statusCode || ERROR_STATUS_CODES[hsError.name] || 500
319
+ );
320
+ }
321
+ if (error instanceof Error) {
322
+ const sanitized = sanitizeErrorMessage(error.message);
323
+ formatErrorResponse("unknown_error", sanitized, 1);
324
+ }
325
+ formatErrorResponse("unknown_error", "An unexpected error occurred", 1);
326
+ }
327
+
328
+ // src/lib/api-client.ts
329
+ import dotenv from "dotenv";
330
+ dotenv.config();
331
+ var API_BASE = "https://api.helpscout.net/v2";
332
+ var TOKEN_URL = "https://api.helpscout.net/v2/oauth2/token";
333
+ var HelpScoutClient = class {
334
+ accessToken = null;
335
+ clearToken() {
336
+ this.accessToken = null;
337
+ }
338
+ async refreshAccessToken() {
339
+ const appId = await auth.getAppId();
340
+ const appSecret = await auth.getAppSecret();
341
+ const refreshToken = await auth.getRefreshToken();
342
+ if (!appId || !appSecret) {
343
+ throw new HelpScoutCliError(
344
+ "Not configured. Please run: helpscout auth login",
345
+ 401
346
+ );
347
+ }
348
+ if (refreshToken) {
349
+ try {
350
+ const response2 = await fetch(TOKEN_URL, {
351
+ method: "POST",
352
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
353
+ body: new URLSearchParams({
354
+ grant_type: "refresh_token",
355
+ client_id: appId,
356
+ client_secret: appSecret,
357
+ refresh_token: refreshToken
358
+ })
359
+ });
360
+ if (response2.ok) {
361
+ const data2 = await response2.json();
362
+ await auth.setAccessToken(data2.access_token);
363
+ if (data2.refresh_token) {
364
+ await auth.setRefreshToken(data2.refresh_token);
365
+ }
366
+ this.accessToken = data2.access_token;
367
+ return data2.access_token;
368
+ }
369
+ } catch (error) {
370
+ const message = error instanceof Error ? error.message : "Unknown error";
371
+ console.error(JSON.stringify({ warning: "Refresh token failed, using client credentials", reason: message }));
372
+ }
373
+ }
374
+ let response;
375
+ try {
376
+ response = await fetch(TOKEN_URL, {
377
+ method: "POST",
378
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
379
+ body: new URLSearchParams({
380
+ grant_type: "client_credentials",
381
+ client_id: appId,
382
+ client_secret: appSecret
383
+ })
384
+ });
385
+ } catch (error) {
386
+ const message = error instanceof Error ? error.message : "Unknown network error";
387
+ throw new HelpScoutCliError(`Network request failed during authentication: ${message}`, 0);
388
+ }
389
+ if (!response.ok) {
390
+ const error = await response.json();
391
+ throw new HelpScoutApiError("OAuth token request failed", error, response.status);
392
+ }
393
+ const data = await response.json();
394
+ await auth.setAccessToken(data.access_token);
395
+ this.accessToken = data.access_token;
396
+ return data.access_token;
397
+ }
398
+ async getAccessToken() {
399
+ if (this.accessToken) {
400
+ return this.accessToken;
401
+ }
402
+ const storedToken = await auth.getAccessToken();
403
+ if (storedToken) {
404
+ this.accessToken = storedToken;
405
+ return storedToken;
406
+ }
407
+ return this.refreshAccessToken();
408
+ }
409
+ async request(method, path, options = {}) {
410
+ const { params, body, retry = true, rateLimitRetry = true } = options;
411
+ const url = new URL(`${API_BASE}${path}`);
412
+ if (params) {
413
+ Object.entries(params).forEach(([key, value]) => {
414
+ if (value !== void 0) {
415
+ url.searchParams.set(key, String(value));
416
+ }
417
+ });
418
+ }
419
+ const token = await this.getAccessToken();
420
+ const fetchOptions = {
421
+ method,
422
+ headers: {
423
+ Authorization: `Bearer ${token}`,
424
+ "Content-Type": "application/json"
425
+ }
426
+ };
427
+ if (body) {
428
+ fetchOptions.body = JSON.stringify(body);
429
+ }
430
+ let response;
431
+ try {
432
+ response = await fetch(url.toString(), fetchOptions);
433
+ } catch (error) {
434
+ const message = error instanceof Error ? error.message : "Unknown network error";
435
+ throw new HelpScoutCliError(`Network request failed: ${message}`, 0);
436
+ }
437
+ if (response.status === 401 && retry) {
438
+ this.accessToken = null;
439
+ await this.refreshAccessToken();
440
+ return this.request(method, path, { ...options, retry: false });
441
+ }
442
+ if (response.status === 429 && rateLimitRetry) {
443
+ const retryAfter = parseInt(response.headers.get("Retry-After") || "60", 10);
444
+ const waitSeconds = Math.min(retryAfter, 120);
445
+ console.error(JSON.stringify({ warning: `Rate limited. Waiting ${waitSeconds}s before retry...` }));
446
+ await new Promise((resolve) => setTimeout(resolve, waitSeconds * 1e3));
447
+ return this.request(method, path, { ...options, rateLimitRetry: false });
448
+ }
449
+ if (response.status === 204) {
450
+ return {};
451
+ }
452
+ if (!response.ok) {
453
+ const error = await response.json().catch(() => ({}));
454
+ throw new HelpScoutApiError("API request failed", error, response.status);
455
+ }
456
+ return response.json();
457
+ }
458
+ // Conversations
459
+ async listConversations(params = {}) {
460
+ const response = await this.request(
461
+ "GET",
462
+ "/conversations",
463
+ { params }
464
+ );
465
+ return {
466
+ conversations: response._embedded?.conversations || [],
467
+ page: response.page
468
+ };
469
+ }
470
+ async listAllConversations(params = {}) {
471
+ const allConversations = [];
472
+ let page = 1;
473
+ let totalPages = 1;
474
+ do {
475
+ const result = await this.listConversations({ ...params, page });
476
+ allConversations.push(...result.conversations);
477
+ totalPages = result.page.totalPages;
478
+ page++;
479
+ } while (page <= totalPages);
480
+ return allConversations;
481
+ }
482
+ async getConversation(conversationId, embed) {
483
+ const params = embed ? { embed } : void 0;
484
+ return this.request("GET", `/conversations/${conversationId}`, { params });
485
+ }
486
+ async getConversationThreads(conversationId) {
487
+ const response = await this.request(
488
+ "GET",
489
+ `/conversations/${conversationId}/threads`
490
+ );
491
+ return response._embedded?.threads || [];
492
+ }
493
+ async updateConversation(conversationId, data) {
494
+ await this.request("PATCH", `/conversations/${conversationId}`, { body: data });
495
+ }
496
+ async deleteConversation(conversationId) {
497
+ await this.request("DELETE", `/conversations/${conversationId}`);
498
+ }
499
+ async addConversationTag(conversationId, tag) {
500
+ await this.request("PUT", `/conversations/${conversationId}/tags`, {
501
+ body: { tags: [tag] }
502
+ });
503
+ }
504
+ async removeConversationTag(conversationId, tag) {
505
+ const conversation = await this.getConversation(conversationId);
506
+ const existingTags = conversation?.tags?.map((t) => t.name) || [];
507
+ const newTags = existingTags.filter((t) => t !== tag);
508
+ await this.request("PUT", `/conversations/${conversationId}/tags`, {
509
+ body: { tags: newTags }
510
+ });
511
+ }
512
+ async createReply(conversationId, data) {
513
+ await this.request("POST", `/conversations/${conversationId}/reply`, { body: data });
514
+ }
515
+ async createNote(conversationId, data) {
516
+ await this.request("POST", `/conversations/${conversationId}/notes`, { body: data });
517
+ }
518
+ // Customers
519
+ async listCustomers(params = {}) {
520
+ const response = await this.request(
521
+ "GET",
522
+ "/customers",
523
+ { params }
524
+ );
525
+ return {
526
+ customers: response._embedded?.customers || [],
527
+ page: response.page
528
+ };
529
+ }
530
+ async getCustomer(customerId) {
531
+ return this.request("GET", `/customers/${customerId}`);
532
+ }
533
+ async createCustomer(data) {
534
+ await this.request("POST", "/customers", { body: data });
535
+ }
536
+ async updateCustomer(customerId, data) {
537
+ await this.request("PUT", `/customers/${customerId}`, { body: data });
538
+ }
539
+ async deleteCustomer(customerId) {
540
+ await this.request("DELETE", `/customers/${customerId}`);
541
+ }
542
+ // Tags
543
+ async listTags(page) {
544
+ const response = await this.request(
545
+ "GET",
546
+ "/tags",
547
+ { params: page ? { page } : void 0 }
548
+ );
549
+ return {
550
+ tags: response._embedded?.tags || [],
551
+ page: response.page
552
+ };
553
+ }
554
+ async getTag(tagId) {
555
+ return this.request("GET", `/tags/${tagId}`);
556
+ }
557
+ // Workflows
558
+ async listWorkflows(params = {}) {
559
+ const response = await this.request(
560
+ "GET",
561
+ "/workflows",
562
+ { params: { mailboxId: params.mailbox, type: params.type, page: params.page } }
563
+ );
564
+ return {
565
+ workflows: response._embedded?.workflows || [],
566
+ page: response.page
567
+ };
568
+ }
569
+ async runWorkflow(workflowId, conversationIds) {
570
+ await this.request("POST", `/workflows/${workflowId}/run`, {
571
+ body: { conversationIds }
572
+ });
573
+ }
574
+ async updateWorkflowStatus(workflowId, status) {
575
+ await this.request("PATCH", `/workflows/${workflowId}`, {
576
+ body: { op: "replace", path: "/status", value: status }
577
+ });
578
+ }
579
+ // Mailboxes
580
+ async listMailboxes(page) {
581
+ const response = await this.request(
582
+ "GET",
583
+ "/mailboxes",
584
+ { params: page ? { page } : void 0 }
585
+ );
586
+ return {
587
+ mailboxes: response._embedded?.mailboxes || [],
588
+ page: response.page
589
+ };
590
+ }
591
+ async getMailbox(mailboxId) {
592
+ return this.request("GET", `/mailboxes/${mailboxId}`);
593
+ }
594
+ };
595
+ var client = new HelpScoutClient();
596
+
597
+ // src/lib/command-utils.ts
598
+ function withErrorHandling(fn) {
599
+ return async (...args) => {
600
+ try {
601
+ await fn(...args);
602
+ } catch (error) {
603
+ handleHelpScoutError(error);
604
+ }
605
+ };
606
+ }
607
+ function parseIdArg(value, resourceType = "resource") {
608
+ const parsed = parseInt(value, 10);
609
+ if (isNaN(parsed) || parsed <= 0) {
610
+ throw new HelpScoutCliError(`Invalid ${resourceType} ID: "${value}"`, 400);
611
+ }
612
+ return parsed;
613
+ }
614
+ function requireAtLeastOneField(data, operation) {
615
+ const hasFields = Object.values(data).some((v) => v !== void 0);
616
+ if (!hasFields) {
617
+ throw new HelpScoutCliError(`${operation} requires at least one field to update`, 400);
618
+ }
619
+ }
620
+ async function confirmDelete(resourceType, skipConfirmation) {
621
+ if (skipConfirmation) {
622
+ return true;
623
+ }
624
+ const readline = await import("readline");
625
+ const rl = readline.createInterface({
626
+ input: process.stdin,
627
+ output: process.stderr
628
+ });
629
+ return new Promise((resolve) => {
630
+ rl.question(`Delete ${resourceType}? (y/N) `, (answer) => {
631
+ rl.close();
632
+ if (answer.toLowerCase() !== "y") {
633
+ outputJson({ cancelled: true, message: "Deletion cancelled" });
634
+ process.exit(0);
635
+ }
636
+ resolve(true);
637
+ });
638
+ });
639
+ }
640
+
641
+ // src/commands/auth.ts
642
+ function createAuthCommand() {
643
+ const cmd = new Command("auth").description("Authentication operations");
644
+ cmd.command("login").description("Configure Help Scout API credentials").requiredOption("--app-id <id>", "Help Scout App ID").requiredOption("--app-secret <secret>", "Help Scout App Secret").action(withErrorHandling(async (options) => {
645
+ await auth.setAppId(options.appId);
646
+ await auth.setAppSecret(options.appSecret);
647
+ client.clearToken();
648
+ await client.refreshAccessToken();
649
+ outputJson({ message: "Successfully authenticated with Help Scout" });
650
+ }));
651
+ cmd.command("logout").description("Remove stored credentials").action(withErrorHandling(async () => {
652
+ await auth.logout();
653
+ client.clearToken();
654
+ outputJson({ message: "Logged out successfully" });
655
+ }));
656
+ cmd.command("status").description("Check authentication status").action(withErrorHandling(async () => {
657
+ const hasToken = await auth.isAuthenticated();
658
+ const hasAppId = !!await auth.getAppId();
659
+ const hasAppSecret = !!await auth.getAppSecret();
660
+ outputJson({
661
+ authenticated: hasToken,
662
+ configured: hasAppId && hasAppSecret
663
+ });
664
+ }));
665
+ cmd.command("refresh").description("Refresh access token").action(withErrorHandling(async () => {
666
+ client.clearToken();
667
+ await client.refreshAccessToken();
668
+ outputJson({ message: "Access token refreshed" });
669
+ }));
670
+ return cmd;
671
+ }
672
+
673
+ // src/commands/mailboxes.ts
674
+ import { Command as Command2 } from "commander";
675
+ function createMailboxesCommand() {
676
+ const cmd = new Command2("mailboxes").description("Mailbox operations");
677
+ cmd.command("list").description("List mailboxes").option("--page <number>", "Page number").action(withErrorHandling(async (options) => {
678
+ const result = await client.listMailboxes(options.page ? parseInt(options.page, 10) : void 0);
679
+ outputJson(result);
680
+ }));
681
+ cmd.command("view").description("View a mailbox").argument("<id>", "Mailbox ID").action(withErrorHandling(async (id) => {
682
+ const mailbox = await client.getMailbox(parseIdArg(id, "mailbox"));
683
+ outputJson(mailbox);
684
+ }));
685
+ cmd.command("set-default").description("Set default mailbox").argument("<id>", "Mailbox ID").action(withErrorHandling(async (id) => {
686
+ const mailboxId = parseIdArg(id, "mailbox");
687
+ config.setDefaultMailbox(String(mailboxId));
688
+ outputJson({ message: `Default mailbox set to ${mailboxId}` });
689
+ }));
690
+ cmd.command("get-default").description("Get default mailbox").action(withErrorHandling(async () => {
691
+ const mailboxId = config.getDefaultMailbox();
692
+ outputJson({ defaultMailbox: mailboxId || null });
693
+ }));
694
+ cmd.command("clear-default").description("Clear default mailbox").action(withErrorHandling(async () => {
695
+ config.clearDefaultMailbox();
696
+ outputJson({ message: "Default mailbox cleared" });
697
+ }));
698
+ return cmd;
699
+ }
700
+
701
+ // src/commands/conversations.ts
702
+ import { Command as Command3 } from "commander";
703
+ function summarizeConversations(conversations) {
704
+ const byStatus = {};
705
+ const byTag = {};
706
+ for (const conv of conversations) {
707
+ byStatus[conv.status] = (byStatus[conv.status] || 0) + 1;
708
+ for (const tag of conv.tags || []) {
709
+ byTag[tag.name] = (byTag[tag.name] || 0) + 1;
710
+ }
711
+ }
712
+ return {
713
+ total: conversations.length,
714
+ byStatus,
715
+ byTag,
716
+ conversations: conversations.map((c) => ({
717
+ id: c.id,
718
+ subject: c.subject,
719
+ status: c.status,
720
+ tags: (c.tags || []).map((t) => t.name),
721
+ preview: c.preview
722
+ }))
723
+ };
724
+ }
725
+ function createConversationsCommand() {
726
+ const cmd = new Command3("conversations").description("Conversation operations");
727
+ cmd.command("list").description("List conversations").option("-m, --mailbox <id>", "Filter by mailbox ID").option("-s, --status <status>", "Filter by status (active, all, closed, open, pending, spam)").option("-t, --tag <tags>", "Filter by tag(s), comma-separated").option("--assigned-to <id>", "Filter by assignee user ID").option("--modified-since <date>", "Filter by modified date (ISO 8601)").option("--sort-field <field>", "Sort by field (createdAt, modifiedAt, number, status, subject)").option("--sort-order <order>", "Sort order (asc, desc)").option("--page <number>", "Page number").option("--embed <resources>", "Embed resources (threads)").option("-q, --query <query>", "Advanced search query (see https://docs.helpscout.com/article/47-search-filters-with-operators)").option("--summary", "Output aggregated summary instead of full conversation list").action(withErrorHandling(async (options) => {
728
+ if (options.summary) {
729
+ const allConversations = await client.listAllConversations({
730
+ mailbox: options.mailbox,
731
+ status: options.status,
732
+ tag: options.tag,
733
+ assignedTo: options.assignedTo,
734
+ modifiedSince: options.modifiedSince,
735
+ query: options.query
736
+ });
737
+ const summary = summarizeConversations(allConversations);
738
+ outputJson(summary);
739
+ return;
740
+ }
741
+ const result = await client.listConversations({
742
+ mailbox: options.mailbox,
743
+ status: options.status,
744
+ tag: options.tag,
745
+ assignedTo: options.assignedTo,
746
+ modifiedSince: options.modifiedSince,
747
+ sortField: options.sortField,
748
+ sortOrder: options.sortOrder,
749
+ page: options.page ? parseInt(options.page, 10) : void 0,
750
+ embed: options.embed,
751
+ query: options.query
752
+ });
753
+ outputJson(result);
754
+ }));
755
+ cmd.command("view").description("View a conversation").argument("<id>", "Conversation ID").option("--embed <resources>", "Embed resources (threads)").action(withErrorHandling(async (id, options) => {
756
+ const conversation = await client.getConversation(parseIdArg(id, "conversation"), options.embed);
757
+ outputJson(conversation);
758
+ }));
759
+ cmd.command("threads").description("List threads for a conversation (defaults to email communications only)").argument("<id>", "Conversation ID").option("--include-notes", "Include internal notes").option("--all", "Show all thread types including lineitems, workflows, etc.").option("-t, --type <types>", "Filter by specific thread type(s), comma-separated (customer, message, note, lineitem, chat, phone, forwardchild, forwardparent, beaconchat)").action(withErrorHandling(async (id, options) => {
760
+ let threads = await client.getConversationThreads(parseIdArg(id, "conversation"));
761
+ if (options.type) {
762
+ const types = options.type.split(",").map((t) => t.trim().toLowerCase());
763
+ threads = threads.filter((t) => types.includes(t.type));
764
+ } else if (!options.all) {
765
+ const allowedTypes = options.includeNotes ? ["customer", "message", "note", "chat", "phone"] : ["customer", "message", "chat", "phone"];
766
+ threads = threads.filter((t) => allowedTypes.includes(t.type));
767
+ }
768
+ outputJson(threads);
769
+ }));
770
+ cmd.command("delete").description("Delete a conversation").argument("<id>", "Conversation ID").option("-y, --yes", "Skip confirmation").action(withErrorHandling(async (id, options) => {
771
+ await confirmDelete("conversation", options.yes);
772
+ await client.deleteConversation(parseIdArg(id, "conversation"));
773
+ outputJson({ message: "Conversation deleted" });
774
+ }));
775
+ cmd.command("add-tag").description("Add a tag to a conversation").argument("<id>", "Conversation ID").argument("<tag>", "Tag name").action(withErrorHandling(async (id, tag) => {
776
+ await client.addConversationTag(parseIdArg(id, "conversation"), tag);
777
+ outputJson({ message: `Tag "${tag}" added` });
778
+ }));
779
+ cmd.command("remove-tag").description("Remove a tag from a conversation").argument("<id>", "Conversation ID").argument("<tag>", "Tag name").action(withErrorHandling(async (id, tag) => {
780
+ await client.removeConversationTag(parseIdArg(id, "conversation"), tag);
781
+ outputJson({ message: `Tag "${tag}" removed` });
782
+ }));
783
+ cmd.command("reply").description("Reply to a conversation").argument("<id>", "Conversation ID").requiredOption("--text <text>", "Reply text").option("--user <id>", "User ID sending the reply").option("--draft", "Save as draft").option("--status <status>", "Set conversation status after reply (active, closed, pending)").action(withErrorHandling(async (id, options) => {
784
+ await client.createReply(parseIdArg(id, "conversation"), {
785
+ text: options.text,
786
+ user: options.user ? parseIdArg(options.user, "user") : void 0,
787
+ draft: options.draft,
788
+ status: options.status
789
+ });
790
+ outputJson({ message: "Reply sent" });
791
+ }));
792
+ cmd.command("note").description("Add a note to a conversation").argument("<id>", "Conversation ID").requiredOption("--text <text>", "Note text").option("--user <id>", "User ID adding the note").action(withErrorHandling(async (id, options) => {
793
+ await client.createNote(parseIdArg(id, "conversation"), {
794
+ text: options.text,
795
+ user: options.user ? parseIdArg(options.user, "user") : void 0
796
+ });
797
+ outputJson({ message: "Note added" });
798
+ }));
799
+ return cmd;
800
+ }
801
+
802
+ // src/commands/customers.ts
803
+ import { Command as Command4 } from "commander";
804
+ function createCustomersCommand() {
805
+ const cmd = new Command4("customers").description("Customer operations");
806
+ cmd.command("list").description("List customers").option("-m, --mailbox <id>", "Filter by mailbox ID").option("--first-name <name>", "Filter by first name").option("--last-name <name>", "Filter by last name").option("--modified-since <date>", "Filter by modified date (ISO 8601)").option("--sort-field <field>", "Sort by field (createdAt, firstName, lastName, modifiedAt)").option("--sort-order <order>", "Sort order (asc, desc)").option("--page <number>", "Page number").option("-q, --query <query>", "Advanced search query").action(withErrorHandling(async (options) => {
807
+ const result = await client.listCustomers({
808
+ mailbox: options.mailbox,
809
+ firstName: options.firstName,
810
+ lastName: options.lastName,
811
+ modifiedSince: options.modifiedSince,
812
+ sortField: options.sortField,
813
+ sortOrder: options.sortOrder,
814
+ page: options.page ? parseInt(options.page, 10) : void 0,
815
+ query: options.query
816
+ });
817
+ outputJson(result);
818
+ }));
819
+ cmd.command("view").description("View a customer").argument("<id>", "Customer ID").action(withErrorHandling(async (id) => {
820
+ const customer = await client.getCustomer(parseIdArg(id, "customer"));
821
+ outputJson(customer);
822
+ }));
823
+ cmd.command("create").description("Create a customer").option("--first-name <name>", "First name").option("--last-name <name>", "Last name").option("--email <email>", "Email address").option("--phone <phone>", "Phone number").action(withErrorHandling(async (options) => {
824
+ const data = {
825
+ ...options.firstName && { firstName: options.firstName },
826
+ ...options.lastName && { lastName: options.lastName },
827
+ ...options.email && { emails: [{ type: "work", value: options.email }] },
828
+ ...options.phone && { phones: [{ type: "work", value: options.phone }] }
829
+ };
830
+ requireAtLeastOneField(data, "Customer create");
831
+ await client.createCustomer(data);
832
+ outputJson({ message: "Customer created" });
833
+ }));
834
+ cmd.command("update").description("Update a customer").argument("<id>", "Customer ID").option("--first-name <name>", "First name").option("--last-name <name>", "Last name").option("--job-title <title>", "Job title").option("--location <location>", "Location").option("--organization <org>", "Organization").option("--background <text>", "Background notes").action(withErrorHandling(async (id, options) => {
835
+ const data = {
836
+ ...options.firstName && { firstName: options.firstName },
837
+ ...options.lastName && { lastName: options.lastName },
838
+ ...options.jobTitle && { jobTitle: options.jobTitle },
839
+ ...options.location && { location: options.location },
840
+ ...options.organization && { organization: options.organization },
841
+ ...options.background && { background: options.background }
842
+ };
843
+ requireAtLeastOneField(data, "Customer update");
844
+ await client.updateCustomer(parseIdArg(id, "customer"), data);
845
+ outputJson({ message: "Customer updated" });
846
+ }));
847
+ cmd.command("delete").description("Delete a customer").argument("<id>", "Customer ID").option("-y, --yes", "Skip confirmation").action(withErrorHandling(async (id, options) => {
848
+ await confirmDelete("customer", options.yes);
849
+ await client.deleteCustomer(parseIdArg(id, "customer"));
850
+ outputJson({ message: "Customer deleted" });
851
+ }));
852
+ return cmd;
853
+ }
854
+
855
+ // src/commands/tags.ts
856
+ import { Command as Command5 } from "commander";
857
+ function createTagsCommand() {
858
+ const cmd = new Command5("tags").description("Tag operations");
859
+ cmd.command("list").description("List all tags").option("--page <number>", "Page number").action(withErrorHandling(async (options) => {
860
+ const result = await client.listTags(options.page ? parseInt(options.page, 10) : void 0);
861
+ outputJson(result);
862
+ }));
863
+ cmd.command("view").description("View a tag").argument("<id>", "Tag ID").action(withErrorHandling(async (id) => {
864
+ const tag = await client.getTag(parseIdArg(id, "tag"));
865
+ outputJson(tag);
866
+ }));
867
+ return cmd;
868
+ }
869
+
870
+ // src/commands/workflows.ts
871
+ import { Command as Command6 } from "commander";
872
+ function createWorkflowsCommand() {
873
+ const cmd = new Command6("workflows").description("Workflow operations");
874
+ cmd.command("list").description("List workflows").option("-m, --mailbox <id>", "Filter by mailbox ID").option("-t, --type <type>", "Filter by type (manual, automatic)").option("--page <number>", "Page number").action(withErrorHandling(async (options) => {
875
+ const result = await client.listWorkflows({
876
+ mailbox: options.mailbox ? parseIdArg(options.mailbox, "mailbox") : void 0,
877
+ type: options.type,
878
+ page: options.page ? parseInt(options.page, 10) : void 0
879
+ });
880
+ outputJson(result);
881
+ }));
882
+ cmd.command("run").description("Run a manual workflow on conversations").argument("<workflow-id>", "Workflow ID").requiredOption("--conversations <ids>", "Comma-separated conversation IDs").action(withErrorHandling(async (workflowId, options) => {
883
+ const conversationIds = options.conversations.split(",").map((id) => parseIdArg(id.trim(), "conversation"));
884
+ await client.runWorkflow(parseIdArg(workflowId, "workflow"), conversationIds);
885
+ outputJson({ message: "Workflow executed" });
886
+ }));
887
+ cmd.command("activate").description("Activate a workflow").argument("<id>", "Workflow ID").action(withErrorHandling(async (id) => {
888
+ await client.updateWorkflowStatus(parseIdArg(id, "workflow"), "active");
889
+ outputJson({ message: "Workflow activated" });
890
+ }));
891
+ cmd.command("deactivate").description("Deactivate a workflow").argument("<id>", "Workflow ID").action(withErrorHandling(async (id) => {
892
+ await client.updateWorkflowStatus(parseIdArg(id, "workflow"), "inactive");
893
+ outputJson({ message: "Workflow deactivated" });
894
+ }));
895
+ return cmd;
896
+ }
897
+
898
+ // src/cli.ts
899
+ var program = new Command7();
900
+ program.name("helpscout").description("A command-line interface for Help Scout").version("1.0.0").option("-c, --compact", "Minified JSON output (single line)").option("-p, --plain", "Strip HTML from body fields, output plain text").option("--include-metadata", "Include _links and _embedded in responses (stripped by default)").option("-f, --fields <fields>", "Comma-separated list of fields to include in output").hook("preAction", (thisCommand) => {
901
+ const options = thisCommand.opts();
902
+ setOutputOptions({
903
+ compact: options.compact,
904
+ slim: !options.includeMetadata,
905
+ plain: options.plain,
906
+ fields: options.fields
907
+ });
908
+ });
909
+ program.addCommand(createAuthCommand());
910
+ program.addCommand(createMailboxesCommand());
911
+ program.addCommand(createConversationsCommand());
912
+ program.addCommand(createCustomersCommand());
913
+ program.addCommand(createTagsCommand());
914
+ program.addCommand(createWorkflowsCommand());
915
+ program.parseAsync().catch(() => {
916
+ process.exit(1);
917
+ });
918
+ //# sourceMappingURL=cli.js.map