@intercom/cli 0.5.0 → 0.6.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 (2) hide show
  1. package/dist/index.js +443 -24
  2. package/package.json +3 -2
package/dist/index.js CHANGED
@@ -1024,7 +1024,12 @@ async function fetchAccessToken(baseUrl, cookies, appPackageId, appId, timeoutMs
1024
1024
  }
1025
1025
  async function getToken(options) {
1026
1026
  const { baseUrl, email, password, appId, appPackageId, timeoutMs } = options;
1027
- const cookies = await deviseSignIn(baseUrl, email, password, timeoutMs);
1027
+ let cookies = await deviseSignIn(baseUrl, email, password, timeoutMs);
1028
+ const appResponse = await fetchWithTimeout(`${baseUrl}/a/apps/${appId}`, {
1029
+ headers: { Cookie: cookies }
1030
+ }, { timeoutMs: timeoutMs ?? API_TIMEOUT_MS });
1031
+ const appCookies = extractCookies(appResponse);
1032
+ if (appCookies) cookies = mergeCookies(cookies, appCookies);
1028
1033
  const appPackage = await findOrCreateAppPackage(
1029
1034
  baseUrl,
1030
1035
  cookies,
@@ -1805,6 +1810,38 @@ Examples:
1805
1810
  }
1806
1811
 
1807
1812
  // src/commands/conversations.ts
1813
+ var BLOCKED_FIELDS = [
1814
+ "topics",
1815
+ "ai_topics",
1816
+ "monitor_evaluations",
1817
+ "scorecards"
1818
+ ];
1819
+ var BLOCKED_CUSTOM_ATTRIBUTE_KEYS = [
1820
+ "AI Topic",
1821
+ "AI Subtopic",
1822
+ "AI Topic and Subtopic",
1823
+ "AI Issue summary",
1824
+ "Query Type",
1825
+ "CX Score rating",
1826
+ "CX Score explanation",
1827
+ "CX Score reasons",
1828
+ "Review status",
1829
+ "Reviewer"
1830
+ ];
1831
+ function sanitizeConversation(conversation) {
1832
+ const sanitized = { ...conversation };
1833
+ for (const field of BLOCKED_FIELDS) {
1834
+ delete sanitized[field];
1835
+ }
1836
+ if (sanitized.custom_attributes && typeof sanitized.custom_attributes === "object") {
1837
+ const filtered = { ...sanitized.custom_attributes };
1838
+ for (const key of BLOCKED_CUSTOM_ATTRIBUTE_KEYS) {
1839
+ delete filtered[key];
1840
+ }
1841
+ sanitized.custom_attributes = filtered;
1842
+ }
1843
+ return sanitized;
1844
+ }
1808
1845
  function formatDatetime(ts) {
1809
1846
  if (!ts) return "";
1810
1847
  const n = typeof ts === "number" ? ts : Number(ts);
@@ -1902,9 +1939,10 @@ Examples:
1902
1939
  result = await client.get("/conversations", { per_page: limit });
1903
1940
  }
1904
1941
  const response = result;
1905
- const items = response.conversations ?? [];
1942
+ const items = (response.conversations ?? []).map(sanitizeConversation);
1943
+ const sanitizedResult = { ...response, conversations: items };
1906
1944
  render(
1907
- result,
1945
+ sanitizedResult,
1908
1946
  opts,
1909
1947
  () => {
1910
1948
  const rows = items.map((c2) => ({
@@ -1925,10 +1963,11 @@ Examples:
1925
1963
  conversations.command("get <id>").description("Get a conversation by ID").option("--json", "Output as JSON").option("--jq <expr>", "Filter JSON output with jq expression").option("--verbose", "Show HTTP request/response details").action(async function(id) {
1926
1964
  const opts = this.optsWithGlobals();
1927
1965
  const client = await resolveClient(opts);
1928
- const result = await client.get(`/conversations/${id}`);
1966
+ const result = sanitizeConversation(
1967
+ await client.get(`/conversations/${id}`)
1968
+ );
1929
1969
  render(result, opts, () => {
1930
- const r = result;
1931
- return renderConversationStream(r);
1970
+ return renderConversationStream(result);
1932
1971
  });
1933
1972
  });
1934
1973
  conversations.command("reply <id>").description("Reply to a conversation").option(
@@ -1978,9 +2017,10 @@ Required fields: body, type (admin or user). Also: admin_id, message_type.`
1978
2017
  };
1979
2018
  const result = await client.post("/conversations/search", body);
1980
2019
  const response = result;
1981
- const items = response.conversations ?? [];
2020
+ const items = (response.conversations ?? []).map(sanitizeConversation);
2021
+ const sanitizedResult = { ...response, conversations: items };
1982
2022
  render(
1983
- result,
2023
+ sanitizedResult,
1984
2024
  opts,
1985
2025
  () => {
1986
2026
  const rows = items.map((c2) => ({
@@ -2404,7 +2444,7 @@ Examples:
2404
2444
  }
2405
2445
 
2406
2446
  // src/commands/fin.ts
2407
- import { writeFile as writeFile2 } from "fs/promises";
2447
+ import { writeFile as writeFile3 } from "fs/promises";
2408
2448
 
2409
2449
  // src/commands/fin/procedures.ts
2410
2450
  import { readFile as readFile2 } from "fs/promises";
@@ -2613,6 +2653,11 @@ function registerAudiencesCommand(fin) {
2613
2653
  }
2614
2654
 
2615
2655
  // src/commands/fin/workflows.ts
2656
+ import { readFile as readFile3 } from "fs/promises";
2657
+ import { basename as basename2 } from "path";
2658
+ import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
2659
+ var STATE_LIVE = 1;
2660
+ var STATE_PAUSED = 2;
2616
2661
  function registerWorkflowsCommand(fin) {
2617
2662
  const workflows = fin.command("workflows").description("Manage Fin SDK workflows");
2618
2663
  workflows.command("list").description("List Fin SDK workflows").option("--json", "Output as JSON").option("--verbose", "Show HTTP request/response details").action(async function() {
@@ -2633,19 +2678,122 @@ function registerWorkflowsCommand(fin) {
2633
2678
  return table({ columns: ["ID", "TITLE", "STATE", "TRIGGER"], rows });
2634
2679
  }, items);
2635
2680
  });
2636
- workflows.command("get <id>").description("Get a workflow by ID").option("--json", "Output as JSON").option("--verbose", "Show HTTP request/response details").action(async function(id) {
2681
+ workflows.command("get <id>").description("Get a workflow by ID").option("--json", "Output as JSON").option("--jq <expr>", "Filter JSON output with jq expression").option("--verbose", "Show HTTP request/response details").action(async function(id) {
2637
2682
  const opts = this.optsWithGlobals();
2638
2683
  const client = await resolveClient(opts);
2639
- const result = await client.get(`/fin-sdk/workflows/${id}`);
2640
- const r = result;
2684
+ const text = await client.getText(`/fin-sdk/workflows/${id}`);
2685
+ const parsed = parseYaml(text);
2686
+ render(
2687
+ parsed,
2688
+ opts,
2689
+ () => detail({
2690
+ "ID": String(parsed.id ?? ""),
2691
+ "Title": String(parsed.title ?? ""),
2692
+ "State": String(parsed.state ?? ""),
2693
+ "Version": parsed.version != null ? String(parsed.version) : void 0,
2694
+ "Trigger": parsed.trigger_type != null ? String(parsed.trigger_type) : void 0
2695
+ })
2696
+ );
2697
+ });
2698
+ workflows.command("upload <file>").description('Upload a workflow YAML file (creates if id is "new", updates if numeric)').option("--json", "Output as JSON").option("--jq <expr>", "Filter JSON output with jq expression").option("--verbose", "Show HTTP request/response details").addHelpText(
2699
+ "after",
2700
+ `
2701
+ Examples:
2702
+ $ intercom fin workflows upload ./welcome.yaml
2703
+ $ intercom fin workflows get wf-42 > local.yaml; vim local.yaml; intercom fin workflows upload local.yaml
2704
+ $ intercom fin workflows upload ./new.yaml --json | jq .visual_builder_url
2705
+
2706
+ The YAML file's id field determines whether this creates or updates:
2707
+ id: "new" \u2192 creates a new workflow
2708
+ id: <int> \u2192 updates the existing workflow with that id`
2709
+ ).action(async function(filePath) {
2710
+ const opts = this.optsWithGlobals();
2711
+ const client = await resolveClient(opts);
2712
+ const fileContent = await readFile3(filePath);
2713
+ const formData = new FormData();
2714
+ const blob = new Blob([fileContent], { type: "application/x-yaml" });
2715
+ formData.append("file_data", blob, basename2(filePath));
2716
+ const result = await client.postFormData("/fin-sdk/upload", formData);
2641
2717
  render(
2642
2718
  result,
2643
2719
  opts,
2644
2720
  () => detail({
2645
- "ID": String(r.id ?? ""),
2646
- "Title": String(r.title ?? ""),
2647
- "State": String(r.state ?? ""),
2648
- "Trigger": r.trigger_type != null ? String(r.trigger_type) : void 0
2721
+ "ID": String(result.id ?? ""),
2722
+ "Type": result.type != null ? String(result.type) : void 0,
2723
+ "Message": result.message != null ? String(result.message) : void 0,
2724
+ "Visual Builder URL": result.visual_builder_url != null ? String(result.visual_builder_url) : void 0
2725
+ })
2726
+ );
2727
+ });
2728
+ workflows.command("enable <id>").description("Set a workflow's state to live").option("--json", "Output as JSON").option("--jq <expr>", "Filter JSON output with jq expression").option("--verbose", "Show HTTP request/response details").addHelpText(
2729
+ "after",
2730
+ `
2731
+ Examples:
2732
+ $ intercom fin workflows enable 42
2733
+ $ intercom fin workflows enable 42 --json
2734
+
2735
+ This is a convenience for GET \u2192 set state \u2192 POST. If another operator
2736
+ edits the workflow between the GET and POST, those edits will be
2737
+ overwritten (the server does not expose a concurrency token).`
2738
+ ).action(async function(id) {
2739
+ const opts = this.optsWithGlobals();
2740
+ const client = await resolveClient(opts);
2741
+ const text = await client.getText(`/fin-sdk/workflows/${id}`);
2742
+ let parsed;
2743
+ try {
2744
+ parsed = parseYaml(text);
2745
+ } catch (err) {
2746
+ throw new Error(`server returned unparseable YAML for workflow ${id}: ${err.message}`);
2747
+ }
2748
+ parsed.state = STATE_LIVE;
2749
+ const yamlBody = stringifyYaml(parsed);
2750
+ const formData = new FormData();
2751
+ const blob = new Blob([yamlBody], { type: "application/x-yaml" });
2752
+ formData.append("file_data", blob, `workflow-${id}.yaml`);
2753
+ const result = await client.postFormData("/fin-sdk/upload", formData);
2754
+ render(
2755
+ result,
2756
+ opts,
2757
+ () => detail({
2758
+ "ID": String(result.id ?? ""),
2759
+ "Type": result.type != null ? String(result.type) : void 0,
2760
+ "Message": result.message != null ? String(result.message) : void 0
2761
+ })
2762
+ );
2763
+ });
2764
+ workflows.command("disable <id>").description("Set a workflow's state to paused").option("--json", "Output as JSON").option("--jq <expr>", "Filter JSON output with jq expression").option("--verbose", "Show HTTP request/response details").addHelpText(
2765
+ "after",
2766
+ `
2767
+ Examples:
2768
+ $ intercom fin workflows disable 42
2769
+ $ intercom fin workflows disable 42 --json
2770
+
2771
+ This is a convenience for GET \u2192 set state \u2192 POST. If another operator
2772
+ edits the workflow between the GET and POST, those edits will be
2773
+ overwritten (the server does not expose a concurrency token).`
2774
+ ).action(async function(id) {
2775
+ const opts = this.optsWithGlobals();
2776
+ const client = await resolveClient(opts);
2777
+ const text = await client.getText(`/fin-sdk/workflows/${id}`);
2778
+ let parsed;
2779
+ try {
2780
+ parsed = parseYaml(text);
2781
+ } catch (err) {
2782
+ throw new Error(`server returned unparseable YAML for workflow ${id}: ${err.message}`);
2783
+ }
2784
+ parsed.state = STATE_PAUSED;
2785
+ const yamlBody = stringifyYaml(parsed);
2786
+ const formData = new FormData();
2787
+ const blob = new Blob([yamlBody], { type: "application/x-yaml" });
2788
+ formData.append("file_data", blob, `workflow-${id}.yaml`);
2789
+ const result = await client.postFormData("/fin-sdk/upload", formData);
2790
+ render(
2791
+ result,
2792
+ opts,
2793
+ () => detail({
2794
+ "ID": String(result.id ?? ""),
2795
+ "Type": result.type != null ? String(result.type) : void 0,
2796
+ "Message": result.message != null ? String(result.message) : void 0
2649
2797
  })
2650
2798
  );
2651
2799
  });
@@ -2888,6 +3036,275 @@ Fin is live on workspace ${appId}`);
2888
3036
  });
2889
3037
  }
2890
3038
 
3039
+ // src/commands/fin/monitors.ts
3040
+ import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
3041
+ import { join as join3, relative, dirname } from "path";
3042
+ import { stringify } from "yaml";
3043
+ async function resolveMonitorId(client, nameOrId) {
3044
+ if (/^\d+$/.test(nameOrId)) {
3045
+ return parseInt(nameOrId, 10);
3046
+ }
3047
+ const monitors = await client.get("/fin-sdk/qa/monitors");
3048
+ const exact = monitors.find((m) => m.name === nameOrId);
3049
+ if (exact) return parseInt(exact.id, 10);
3050
+ const lower = nameOrId.toLowerCase();
3051
+ const fuzzy = monitors.find((m) => m.name.toLowerCase().includes(lower));
3052
+ if (fuzzy) return parseInt(fuzzy.id, 10);
3053
+ throw new CLIError(
3054
+ `Monitor not found: "${nameOrId}". Check the name or use the numeric ID.`
3055
+ );
3056
+ }
3057
+ function buildWorkingSetYaml(result, since) {
3058
+ const header = [
3059
+ `# Generated by: fin monitor pull-flagged "${result.monitor_name}" --since ${since}`,
3060
+ `# Generated at: ${(/* @__PURE__ */ new Date()).toISOString()}`,
3061
+ `# Monitor: ${result.monitor_name} (ID: ${result.monitor_id})`,
3062
+ `# Showing: ${result.conversations.length} of ${result.total_count} total flagged`,
3063
+ "#",
3064
+ "# Edit this file:",
3065
+ "# - Remove conversations you don't want to act on",
3066
+ "# - Set the action and message below",
3067
+ "# - Then run: fin conversation bulk-send --yes",
3068
+ ""
3069
+ ].join("\n");
3070
+ const doc = {
3071
+ action: "reply",
3072
+ message: "Your message here \u2014 this will be sent to all conversations listed below.\n",
3073
+ close_after_reply: false,
3074
+ conversations: result.conversations.map((c2) => ({
3075
+ id: c2.id,
3076
+ flagged_at: c2.flagged_at,
3077
+ reasoning: c2.reasoning || "No reasoning provided"
3078
+ }))
3079
+ };
3080
+ return header + stringify(doc, { lineWidth: 120 });
3081
+ }
3082
+ function registerMonitorsCommand(fin) {
3083
+ const monitor = fin.command("monitor").description("Manage Fin QA monitors");
3084
+ monitor.command("pull-flagged <monitor>").description(
3085
+ "Pull conversations flagged by a QA monitor into an editable working set file"
3086
+ ).option(
3087
+ "--since <period>",
3088
+ "Time window to look back (e.g. 24h, 7d, 2w)",
3089
+ "24h"
3090
+ ).option("--limit <n>", "Maximum number of conversations to include", "50").option("--output <path>", "Output file path for the working set YAML").option(
3091
+ "--json",
3092
+ "Output flagged conversations as JSON instead of writing a file"
3093
+ ).option("--jq <expr>", "Filter JSON output with jq expression").option("--verbose", "Show HTTP request/response details").addHelpText(
3094
+ "after",
3095
+ `
3096
+ Examples:
3097
+ $ fin monitor pull-flagged "Compliance Violations" --since 7d
3098
+ $ fin monitor pull-flagged 12345 --since 24h --limit 100
3099
+ $ fin monitor pull-flagged "My Monitor" --json`
3100
+ ).action(async function(monitorArg) {
3101
+ const opts = this.optsWithGlobals();
3102
+ const client = await resolveClient(opts);
3103
+ const since = opts.since ?? "24h";
3104
+ const limit = parseInt(opts.limit ?? "50", 10);
3105
+ const monitorId = await resolveMonitorId(client, monitorArg);
3106
+ const result = await client.get(
3107
+ `/fin-sdk/qa/monitors/${monitorId}/flagged-conversations`,
3108
+ { since, limit: String(limit) }
3109
+ );
3110
+ if (opts.json || opts.jq || !process.stdout.isTTY) {
3111
+ render(result, opts, () => {
3112
+ return `${result.conversations.length} conversations flagged by "${result.monitor_name}"`;
3113
+ });
3114
+ return;
3115
+ }
3116
+ if (result.conversations.length === 0) {
3117
+ console.log(
3118
+ `No conversations flagged by "${result.monitor_name}" in the last ${since}.`
3119
+ );
3120
+ return;
3121
+ }
3122
+ const outputPath = opts.output ?? join3(
3123
+ process.cwd(),
3124
+ "src",
3125
+ "qa",
3126
+ "bulk-actions",
3127
+ "flagged-conversations.yaml"
3128
+ );
3129
+ await mkdir2(dirname(outputPath), { recursive: true });
3130
+ await writeFile2(outputPath, buildWorkingSetYaml(result, since), "utf-8");
3131
+ const relPath = relative(process.cwd(), outputPath);
3132
+ console.log(
3133
+ `Wrote ${result.conversations.length} conversations to ${relPath}`
3134
+ );
3135
+ console.log("");
3136
+ console.log("Next steps:");
3137
+ console.log(` 1. Edit ${relPath}`);
3138
+ console.log(" - Remove conversations you don't want to act on");
3139
+ console.log(" - Set action: reply, note, or close");
3140
+ console.log(" - Write your message");
3141
+ console.log(" 2. Preview: fin conversation bulk-send --dry-run");
3142
+ console.log(" 3. Send: fin conversation bulk-send --yes");
3143
+ });
3144
+ }
3145
+
3146
+ // src/commands/fin/conversation.ts
3147
+ import { readFile as readFile4 } from "fs/promises";
3148
+ import { join as join4 } from "path";
3149
+ import { parse } from "yaml";
3150
+ var PLACEHOLDER_MESSAGE = "Your message here \u2014 this will be sent to all conversations listed below.";
3151
+ function validateWorkingSet(ws) {
3152
+ const errors = [];
3153
+ if (!ws.action) {
3154
+ errors.push('Missing "action" field. Must be: reply, note, close, tag, or run_procedure.');
3155
+ } else if (!["reply", "note", "close", "tag", "run_procedure"].includes(ws.action)) {
3156
+ errors.push(
3157
+ `Invalid action "${ws.action}". Must be one of: reply, note, close, tag, run_procedure.`
3158
+ );
3159
+ }
3160
+ if (ws.action === "reply" || ws.action === "note") {
3161
+ if (!ws.message || ws.message.trim().startsWith(PLACEHOLDER_MESSAGE.slice(0, 20))) {
3162
+ errors.push(
3163
+ `Message is required for action "${ws.action}". Edit the message field.`
3164
+ );
3165
+ }
3166
+ }
3167
+ if (ws.action === "run_procedure" && !ws.procedure_id && !ws.procedure_name) {
3168
+ errors.push("procedure_id or procedure_name is required for run_procedure actions.");
3169
+ } else if (ws.action === "run_procedure" && ws.procedure_id && typeof ws.procedure_id !== "number") {
3170
+ errors.push("procedure_id must be a number.");
3171
+ }
3172
+ if (ws.action === "tag" && !ws.tag_id && !ws.tag_name) {
3173
+ errors.push("tag_id or tag_name is required for tag actions.");
3174
+ } else if (ws.action === "tag" && ws.tag_id && typeof ws.tag_id !== "number") {
3175
+ errors.push("tag_id must be a number.");
3176
+ }
3177
+ if (!ws.conversations || ws.conversations.length === 0) {
3178
+ errors.push("conversations list is empty. Add at least one conversation.");
3179
+ } else {
3180
+ const invalid = ws.conversations.filter(
3181
+ (c2) => typeof c2.id !== "number" || isNaN(c2.id)
3182
+ );
3183
+ if (invalid.length > 0) {
3184
+ errors.push(
3185
+ `${invalid.length} conversation(s) have invalid IDs. Each entry must have a numeric id.`
3186
+ );
3187
+ }
3188
+ }
3189
+ return errors;
3190
+ }
3191
+ function registerConversationCommand(fin) {
3192
+ const conversation = fin.command("conversation").description("Manage conversations in bulk");
3193
+ conversation.command("bulk-send").description(
3194
+ "Send bulk actions (reply, note, close, tag, run_procedure) from a working set file"
3195
+ ).option(
3196
+ "--file <path>",
3197
+ "Path to the working set YAML file",
3198
+ join4(process.cwd(), "src", "qa", "bulk-actions", "flagged-conversations.yaml")
3199
+ ).option("--dry-run", "Preview actions without sending").option("--yes", "Skip confirmation prompt (required in TTY mode)").option(
3200
+ "--json",
3201
+ "Output result as JSON"
3202
+ ).option("--jq <expr>", "Filter JSON output with jq expression").option("--verbose", "Show HTTP request/response details").addHelpText(
3203
+ "after",
3204
+ `
3205
+ Examples:
3206
+ $ fin conversation bulk-send --dry-run
3207
+ $ fin conversation bulk-send --yes
3208
+ $ fin conversation bulk-send --file ./my-working-set.yaml --yes
3209
+ $ fin conversation bulk-send --json`
3210
+ ).action(async function() {
3211
+ const opts = this.optsWithGlobals();
3212
+ const filePath = opts.file ?? join4(
3213
+ process.cwd(),
3214
+ "src",
3215
+ "qa",
3216
+ "bulk-actions",
3217
+ "flagged-conversations.yaml"
3218
+ );
3219
+ let raw;
3220
+ try {
3221
+ raw = await readFile4(filePath, "utf-8");
3222
+ } catch (err) {
3223
+ const e = err;
3224
+ if (e.code === "ENOENT") {
3225
+ throw new CLIError(
3226
+ `Working set file not found: ${filePath}
3227
+ Run "fin monitor pull-flagged <monitor>" to create one.`
3228
+ );
3229
+ }
3230
+ throw new CLIError(`Failed to read working set file: ${e.message}`);
3231
+ }
3232
+ let ws;
3233
+ try {
3234
+ ws = parse(raw);
3235
+ } catch (err) {
3236
+ const e = err;
3237
+ throw new CLIError(`Invalid YAML in working set file: ${e.message}`);
3238
+ }
3239
+ const errors = validateWorkingSet(ws);
3240
+ if (errors.length > 0) {
3241
+ throw new CLIError(
3242
+ `Working set validation failed:
3243
+ ${errors.map((e) => ` - ${e}`).join("\n")}`
3244
+ );
3245
+ }
3246
+ const conversationIds = ws.conversations.map((c2) => c2.id);
3247
+ const count = conversationIds.length;
3248
+ if (opts.dryRun) {
3249
+ const dryResult = {
3250
+ dry_run: true,
3251
+ action_type: ws.action,
3252
+ conversation_count: count,
3253
+ ...ws.message ? { message: ws.message } : {},
3254
+ ...ws.close_after_reply ? { close_after_reply: true } : {},
3255
+ ...ws.procedure_id ? { procedure_id: ws.procedure_id } : {},
3256
+ ...ws.procedure_name ? { procedure_name: ws.procedure_name } : {},
3257
+ ...ws.tag_id ? { tag_id: ws.tag_id } : {},
3258
+ ...ws.tag_name ? { tag_name: ws.tag_name } : {}
3259
+ };
3260
+ render(dryResult, opts, () => {
3261
+ const lines = [
3262
+ `Dry run \u2014 no changes will be made.`,
3263
+ `Action: ${ws.action}`,
3264
+ `Conversations: ${count}`
3265
+ ];
3266
+ if (ws.message) lines.push(`Message: ${ws.message.trim()}`);
3267
+ if (ws.close_after_reply) lines.push(`Close after: yes`);
3268
+ if (ws.procedure_id) lines.push(`Procedure ID: ${ws.procedure_id}`);
3269
+ if (ws.procedure_name) lines.push(`Procedure: ${ws.procedure_name}`);
3270
+ if (ws.tag_id) lines.push(`Tag ID: ${ws.tag_id}`);
3271
+ if (ws.tag_name) lines.push(`Tag: ${ws.tag_name}`);
3272
+ return lines.join("\n");
3273
+ });
3274
+ return;
3275
+ }
3276
+ if (process.stdout.isTTY && !opts.yes) {
3277
+ console.log(
3278
+ `About to send "${ws.action}" to ${count} conversation(s).`
3279
+ );
3280
+ if (ws.message) {
3281
+ console.log(`Message: ${ws.message.trim()}`);
3282
+ }
3283
+ throw new CLIError(
3284
+ `Add --yes to confirm, or use --dry-run to preview.`
3285
+ );
3286
+ }
3287
+ const client = await resolveClient(opts);
3288
+ const body = {
3289
+ conversation_ids: conversationIds,
3290
+ action_type: ws.action
3291
+ };
3292
+ if (ws.message) body.message = ws.message;
3293
+ if (ws.close_after_reply != null) body.close_after_reply = ws.close_after_reply;
3294
+ if (ws.procedure_id != null) body.procedure_id = ws.procedure_id;
3295
+ if (ws.procedure_name != null) body.procedure_name = ws.procedure_name;
3296
+ if (ws.tag_id != null) body.tag_id = ws.tag_id;
3297
+ if (ws.tag_name != null) body.tag_name = ws.tag_name;
3298
+ const result = await client.post(
3299
+ "/fin-sdk/bulk-actions",
3300
+ body
3301
+ );
3302
+ render(result, opts, () => {
3303
+ return `Sent "${result.action_type}" to ${result.conversation_count} conversation(s). Status: ${result.status}`;
3304
+ });
3305
+ });
3306
+ }
3307
+
2891
3308
  // src/commands/fin.ts
2892
3309
  function renderResourceTable(label, items) {
2893
3310
  if (items.length === 0) {
@@ -2948,19 +3365,21 @@ Examples:
2948
3365
  registerAudiencesCommand(fin);
2949
3366
  registerWorkflowsCommand(fin);
2950
3367
  registerSimulationsCommand(fin);
3368
+ registerMonitorsCommand(fin);
3369
+ registerConversationCommand(fin);
2951
3370
  fin.command("download").description("Download Fin SDK resources as a ZIP file").option("-o, --output <path>", "Output file path", "./fin-resources.zip").option("--verbose", "Show HTTP request/response details").action(async function() {
2952
3371
  const opts = this.optsWithGlobals();
2953
3372
  const outputPath = opts.output ?? "./fin-resources.zip";
2954
3373
  const client = await resolveClient(opts);
2955
3374
  const buffer = await client.getBuffer("/fin-sdk/download");
2956
- await writeFile2(outputPath, buffer);
3375
+ await writeFile3(outputPath, buffer);
2957
3376
  console.log(`Downloaded ${buffer.length} bytes to ${outputPath}`);
2958
3377
  });
2959
3378
  }
2960
3379
 
2961
3380
  // src/commands/setup.ts
2962
3381
  import { readdirSync, readFileSync as readFileSync4, statSync } from "fs";
2963
- import { join as join3, basename as basename2 } from "path";
3382
+ import { join as join5, basename as basename3 } from "path";
2964
3383
  function isGreenfield(opts) {
2965
3384
  return !!(opts.email && opts.name && (process.env.INTERCOM_PASSWORD || opts.passwordStdin));
2966
3385
  }
@@ -3018,8 +3437,8 @@ function buildPlan(opts) {
3018
3437
  }
3019
3438
  if (mdFiles.length > 0) {
3020
3439
  const articleCommands = mdFiles.map((file) => {
3021
- const title = basename2(file, ".md").replace(/-/g, " ").replace(/\b\w/g, (c2) => c2.toUpperCase());
3022
- return `intercom articles create -f title="${title}" -F body=@${join3(opts.articlesFrom, file)} -f author_id=$(intercom me --json | jq -r .id)`;
3440
+ const title = basename3(file, ".md").replace(/-/g, " ").replace(/\b\w/g, (c2) => c2.toUpperCase());
3441
+ return `intercom articles create -f title="${title}" -F body=@${join5(opts.articlesFrom, file)} -f author_id=$(intercom me --json | jq -r .id)`;
3023
3442
  });
3024
3443
  steps.push({
3025
3444
  step: stepNum++,
@@ -3085,8 +3504,8 @@ async function runPostAuthSteps(client, opts, results, appId) {
3085
3504
  let created = 0;
3086
3505
  let skipped = 0;
3087
3506
  for (const file of mdFiles) {
3088
- const title = basename2(file, ".md").replace(/-/g, " ").replace(/\b\w/g, (c2) => c2.toUpperCase());
3089
- const body = readFileSync4(join3(opts.articlesFrom, file), "utf-8");
3507
+ const title = basename3(file, ".md").replace(/-/g, " ").replace(/\b\w/g, (c2) => c2.toUpperCase());
3508
+ const body = readFileSync4(join5(opts.articlesFrom, file), "utf-8");
3090
3509
  if (existingTitles.has(title)) {
3091
3510
  skipped++;
3092
3511
  } else {
@@ -3563,7 +3982,7 @@ function registerPricingCommand(program) {
3563
3982
  }
3564
3983
 
3565
3984
  // src/commands/config.ts
3566
- import { join as join4 } from "path";
3985
+ import { join as join6 } from "path";
3567
3986
  var SUPPORTED_KEYS = ["default_workspace", "timeout"];
3568
3987
  function configDir() {
3569
3988
  return process.env.INTERCOM_CONFIG_DIR ?? defaultConfigDir();
@@ -3652,7 +4071,7 @@ function registerConfigCommand(program) {
3652
4071
  console.log(`Set ${key} = ${value}`);
3653
4072
  });
3654
4073
  cmd.command("path").description("Show configuration file path").action(async function() {
3655
- console.log(join4(configDir(), "config.json"));
4074
+ console.log(join6(configDir(), "config.json"));
3656
4075
  });
3657
4076
  }
3658
4077
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intercom/cli",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "CLI tool for managing Intercom workspaces",
5
5
  "type": "module",
6
6
  "bin": {
@@ -28,7 +28,8 @@
28
28
  "adm-zip": "^0.5.16",
29
29
  "commander": "^14.0.3",
30
30
  "picocolors": "^1.1.1",
31
- "turndown": "^7.2.2"
31
+ "turndown": "^7.2.2",
32
+ "yaml": "^2.8.3"
32
33
  },
33
34
  "devDependencies": {
34
35
  "@types/adm-zip": "^0.5.8",