@intercom/cli 0.14.1 → 0.15.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/CHANGELOG.md +14 -0
  2. package/dist/index.js +135 -49
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  View it from the CLI with `intercom changelog`.
9
9
 
10
+ ## [0.15.0] - 2026-11-06
11
+
12
+ ### Added
13
+
14
+ - `intercom fin workflows list`, `get` and `upload` now return a `web_url` that opens the workflow in the visual builder. The builder URL uses a different id from the workflow's `id`, so use this field instead of building the URL yourself.
15
+ - `intercom articles list`, `search`, `get`, `create` and `update` add a `web_url` that opens each article in the Intercom editor, in the workspace's own region.
16
+ - `intercom fin workflows upload --note <text>` labels the saved version, as `fin procedures upload --note` already does. The note shows in `intercom fin workflows versions <id>`.
17
+
18
+ ### Fixed
19
+
20
+ - `intercom fin workflows upload --verify` no longer reports drift on `web_url` or `workflow_instance_id`. The server assigns those itself, so a file copied from another workflow no longer fails verification.
21
+ - `intercom fin workflows get <id> --version <version_id>` and `intercom fin procedures get <id> --version <version_id>` now download that version. Before, a space between `--version` and the id printed the CLI's own version instead; only `--version=<version_id>` worked.
22
+ - `intercom fin workflows upload --verify` no longer reports drift when an uploaded audio file, such as a phone greeting or hold music, comes back with a fresh download signature. The link still points at the same file.
23
+
10
24
  ## [0.14.1] - 2026-10-27
11
25
 
12
26
  ### Fixed
package/dist/index.js CHANGED
@@ -59,6 +59,51 @@ async function fetchWithTimeout(url, init, options) {
59
59
  return response;
60
60
  }
61
61
 
62
+ // src/api/regions.ts
63
+ var REGION_BASE_URLS = {
64
+ us: "https://api.intercom.io",
65
+ eu: "https://api.eu.intercom.io",
66
+ au: "https://api.au.intercom.io"
67
+ };
68
+ var APP_BASE_URLS = {
69
+ [REGION_BASE_URLS.us]: "https://app.intercom.com",
70
+ [REGION_BASE_URLS.eu]: "https://app.eu.intercom.com",
71
+ [REGION_BASE_URLS.au]: "https://app.au.intercom.com"
72
+ };
73
+ function appBaseUrlFor(apiBaseUrl) {
74
+ return process.env.INTERCOM_APP_BASE_URL || APP_BASE_URLS[apiBaseUrl.replace(/\/+$/, "")];
75
+ }
76
+ function regionBaseUrl(region) {
77
+ if (!region) return void 0;
78
+ return REGION_BASE_URLS[region];
79
+ }
80
+ function normalizeRegion(value) {
81
+ if (typeof value !== "string") return void 0;
82
+ switch (value.trim().toLowerCase()) {
83
+ case "us":
84
+ case "usa":
85
+ case "united states":
86
+ return "us";
87
+ case "eu":
88
+ case "europe":
89
+ return "eu";
90
+ case "au":
91
+ case "australia":
92
+ return "au";
93
+ default:
94
+ return void 0;
95
+ }
96
+ }
97
+ function detectRegion(me) {
98
+ if (!me || typeof me !== "object") return void 0;
99
+ const m = me;
100
+ const app = m.app;
101
+ return normalizeRegion(app?.region ?? m.region);
102
+ }
103
+ function resolveBaseUrl(region) {
104
+ return process.env.INTERCOM_API_BASE_URL || regionBaseUrl(region) || void 0;
105
+ }
106
+
62
107
  // src/api/client.ts
63
108
  var DEFAULT_BASE_URL = "https://api.intercom.io";
64
109
  var MAX_RAW_ERROR_LEN = 500;
@@ -104,6 +149,9 @@ var IntercomClient = class _IntercomClient {
104
149
  this.verbose = options.verbose ?? false;
105
150
  this.timeoutMs = options.timeoutMs ?? API_TIMEOUT_MS;
106
151
  }
152
+ get appBaseUrl() {
153
+ return appBaseUrlFor(this.baseUrl);
154
+ }
107
155
  authHeader() {
108
156
  if (this.authType === "api_key") {
109
157
  return `Basic ${Buffer.from(`${this.appId}:${this.token}`).toString("base64")}`;
@@ -363,43 +411,6 @@ ${await value.text()}`);
363
411
  }
364
412
  };
365
413
 
366
- // src/api/regions.ts
367
- var REGION_BASE_URLS = {
368
- us: "https://api.intercom.io",
369
- eu: "https://api.eu.intercom.io",
370
- au: "https://api.au.intercom.io"
371
- };
372
- function regionBaseUrl(region) {
373
- if (!region) return void 0;
374
- return REGION_BASE_URLS[region];
375
- }
376
- function normalizeRegion(value) {
377
- if (typeof value !== "string") return void 0;
378
- switch (value.trim().toLowerCase()) {
379
- case "us":
380
- case "usa":
381
- case "united states":
382
- return "us";
383
- case "eu":
384
- case "europe":
385
- return "eu";
386
- case "au":
387
- case "australia":
388
- return "au";
389
- default:
390
- return void 0;
391
- }
392
- }
393
- function detectRegion(me) {
394
- if (!me || typeof me !== "object") return void 0;
395
- const m = me;
396
- const app = m.app;
397
- return normalizeRegion(app?.region ?? m.region);
398
- }
399
- function resolveBaseUrl(region) {
400
- return process.env.INTERCOM_API_BASE_URL || regionBaseUrl(region) || void 0;
401
- }
402
-
403
414
  // src/auth/store.ts
404
415
  import { readFileSync, mkdirSync } from "fs";
405
416
 
@@ -1604,6 +1615,13 @@ function htmlToText(html) {
1604
1615
  function stripBody(items) {
1605
1616
  return items.map(({ body: _body, ...rest }) => rest);
1606
1617
  }
1618
+ function withWebUrl(article, appBaseUrl) {
1619
+ if (!appBaseUrl || article.id == null || article.workspace_id == null) return article;
1620
+ return {
1621
+ ...article,
1622
+ web_url: `${appBaseUrl}/a/apps/${String(article.workspace_id)}/knowledge-hub/article/${String(article.id)}`
1623
+ };
1624
+ }
1607
1625
  function registerArticlesCommand(program) {
1608
1626
  const articles = program.command("articles").description("Manage help center articles (list, get, create, update, delete, search)").addHelpText(
1609
1627
  "after",
@@ -1645,6 +1663,7 @@ Examples:
1645
1663
  }
1646
1664
  result = raw;
1647
1665
  }
1666
+ result.data = result.data?.map((a) => withWebUrl(a, client.appBaseUrl));
1648
1667
  const items = result.data ?? [];
1649
1668
  render(
1650
1669
  result,
@@ -1688,6 +1707,11 @@ Examples:
1688
1707
  data.articles = stripBody(data.articles);
1689
1708
  }
1690
1709
  }
1710
+ if (Array.isArray(result.data)) {
1711
+ result.data = result.data.map((a) => withWebUrl(a, client.appBaseUrl));
1712
+ } else if (result.data && Array.isArray(result.data.articles)) {
1713
+ result.data.articles = result.data.articles.map((a) => withWebUrl(a, client.appBaseUrl));
1714
+ }
1691
1715
  const dataAfter = result.data;
1692
1716
  const items = Array.isArray(dataAfter) ? dataAfter : dataAfter?.articles ?? [];
1693
1717
  render(
@@ -1709,12 +1733,13 @@ Examples:
1709
1733
  articles.command("get <id>").description("Get a help center article 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) {
1710
1734
  const opts = this.optsWithGlobals();
1711
1735
  const client = await resolveClient(opts);
1712
- const result = await client.get(`/articles/${id}`);
1736
+ const result = withWebUrl(await client.get(`/articles/${id}`), client.appBaseUrl);
1713
1737
  render(result, opts, () => {
1714
1738
  const meta = detail({
1715
1739
  ID: String(result.id ?? ""),
1716
1740
  Title: String(result.title ?? ""),
1717
1741
  State: String(result.state ?? ""),
1742
+ "Web URL": result.web_url != null ? String(result.web_url) : void 0,
1718
1743
  "Author ID": result.author_id != null ? String(result.author_id) : void 0,
1719
1744
  Updated: formatTimestamp(result.updated_at),
1720
1745
  Created: formatTimestamp(result.created_at)
@@ -1755,14 +1780,15 @@ Use -F for typed values (numbers, booleans) and @file reads.`
1755
1780
  const parsed = parseInt(body.author_id, 10);
1756
1781
  if (!isNaN(parsed)) body.author_id = parsed;
1757
1782
  }
1758
- const result = await client.post("/articles", body);
1783
+ const result = withWebUrl(await client.post("/articles", body), client.appBaseUrl);
1759
1784
  render(
1760
1785
  result,
1761
1786
  opts,
1762
1787
  () => detail({
1763
1788
  ID: String(result.id ?? ""),
1764
1789
  Title: String(result.title ?? ""),
1765
- State: String(result.state ?? "")
1790
+ State: String(result.state ?? ""),
1791
+ "Web URL": result.web_url != null ? String(result.web_url) : void 0
1766
1792
  })
1767
1793
  );
1768
1794
  });
@@ -1787,14 +1813,15 @@ Use -F for typed values (numbers, booleans) and @file reads.`
1787
1813
  const parsed = parseInt(body.author_id, 10);
1788
1814
  if (!isNaN(parsed)) body.author_id = parsed;
1789
1815
  }
1790
- const result = await client.put(`/articles/${id}`, body);
1816
+ const result = withWebUrl(await client.put(`/articles/${id}`, body), client.appBaseUrl);
1791
1817
  render(
1792
1818
  result,
1793
1819
  opts,
1794
1820
  () => detail({
1795
1821
  ID: String(result.id ?? ""),
1796
1822
  Title: String(result.title ?? ""),
1797
- State: String(result.state ?? "")
1823
+ State: String(result.state ?? ""),
1824
+ "Web URL": result.web_url != null ? String(result.web_url) : void 0
1798
1825
  })
1799
1826
  );
1800
1827
  });
@@ -2966,7 +2993,36 @@ function scalarsMatch(sent, saved) {
2966
2993
  if (typeof saved === "object") {
2967
2994
  return false;
2968
2995
  }
2969
- return String(sent) === String(saved);
2996
+ if (String(sent) === String(saved)) {
2997
+ return true;
2998
+ }
2999
+ const unsignedSent = withoutSignature(String(sent));
3000
+ return unsignedSent !== null && unsignedSent === withoutSignature(String(saved));
3001
+ }
3002
+ var SIGNATURE_PARAMS = ["expires", "signature", "req"];
3003
+ var UPLOAD_HOSTS = [
3004
+ /^downloads\.intercomcdn\.eu$/,
3005
+ /^downloads(\.[a-z]+)?\.intercomcdn\.com$/,
3006
+ /\.intercom-attachments(-[0-9]+)?\.(com|eu)$/,
3007
+ /^video-messages\.intercomcdn\.com$/,
3008
+ /\.intercom-mail\.(com|eu)$/
3009
+ ];
3010
+ function withoutSignature(value) {
3011
+ if (!/^https?:\/\//i.test(value)) {
3012
+ return null;
3013
+ }
3014
+ try {
3015
+ const url = new URL(value);
3016
+ if (!UPLOAD_HOSTS.some((host) => host.test(url.hostname))) {
3017
+ return null;
3018
+ }
3019
+ for (const param of SIGNATURE_PARAMS) {
3020
+ url.searchParams.delete(param);
3021
+ }
3022
+ return url.toString();
3023
+ } catch {
3024
+ return null;
3025
+ }
2970
3026
  }
2971
3027
  function findDrift(sent, saved, path = "") {
2972
3028
  const here = path || ".";
@@ -3532,7 +3588,16 @@ function ensureWorkflowType(raw) {
3532
3588
  }
3533
3589
  function registerWorkflowsCommand(fin) {
3534
3590
  const workflows = fin.command("workflows").description("Manage Fin SDK workflows");
3535
- workflows.command("list").description("List Fin SDK workflows").option("--json", "Output as JSON").option("--verbose", "Show HTTP request/response details").action(async function() {
3591
+ workflows.command("list").description("List Fin SDK workflows").option("--json", "Output as JSON").option("--verbose", "Show HTTP request/response details").addHelpText(
3592
+ "after",
3593
+ `
3594
+ Examples:
3595
+ $ intercom fin workflows list
3596
+ $ intercom fin workflows list --jq '.web_url'
3597
+
3598
+ Each workflow's web_url opens it in the visual builder. Use it rather than
3599
+ building a URL from id: the builder uses a different id.`
3600
+ ).action(async function() {
3536
3601
  const opts = this.optsWithGlobals();
3537
3602
  const client = await resolveClient(opts);
3538
3603
  const result = await client.get("/fin-sdk/workflows");
@@ -3561,7 +3626,8 @@ Examples:
3561
3626
  $ intercom fin workflows get 42 --version 4821 # print that version's YAML
3562
3627
 
3563
3628
  The YAML it prints is ready to edit and re-upload with
3564
- \`intercom fin workflows upload\`.
3629
+ \`intercom fin workflows upload\`. Its web_url field links to the workflow in
3630
+ the visual builder, whose URL uses a different id from this one.
3565
3631
 
3566
3632
  --version downloads the re-uploadable YAML of a historical version, so you can
3567
3633
  diff a local file against what is live (\`intercom fin workflows versions <id>\`
@@ -3582,19 +3648,22 @@ shows which version that is).`
3582
3648
  console.log(text.replace(/\n+$/, ""));
3583
3649
  }
3584
3650
  });
3585
- workflows.command("upload <file>").description('Upload a workflow YAML file (creates if id is "new", updates if numeric)').option("--verify", "Fail if the saved workflow differs from the file uploaded").option("--json", "Output as JSON").option("--jq <expr>", "Filter JSON output with jq expression").option("--verbose", "Show HTTP request/response details").addHelpText(
3651
+ workflows.command("upload <file>").description('Upload a workflow YAML file (creates if id is "new", updates if numeric)').option("--note <text>", "Version note to record against this save").option("--verify", "Fail if the saved workflow differs from the file uploaded").option("--json", "Output as JSON").option("--jq <expr>", "Filter JSON output with jq expression").option("--verbose", "Show HTTP request/response details").addHelpText(
3586
3652
  "after",
3587
3653
  `
3588
3654
  Examples:
3589
3655
  $ intercom fin workflows upload ./welcome.yaml
3590
3656
  $ intercom fin workflows get wf-42 > local.yaml; vim local.yaml; intercom fin workflows upload local.yaml
3591
- $ intercom fin workflows upload ./new.yaml --json | jq .visual_builder_url
3657
+ $ intercom fin workflows upload ./new.yaml --json | jq .web_url
3592
3658
  $ intercom fin workflows upload ./welcome.yaml --verify
3659
+ $ intercom fin workflows upload ./welcome.yaml --note "added VIP routing"
3593
3660
 
3594
3661
  The YAML file's id field determines whether this creates or updates:
3595
3662
  id: "new" \u2192 creates a new workflow
3596
3663
  id: <int> \u2192 updates the existing workflow with that id
3597
3664
 
3665
+ --note labels this save in \`intercom fin workflows versions <id>\`.
3666
+
3598
3667
  --verify compares the workflow the server saved against the file just uploaded
3599
3668
  and exits non-zero listing any field that did not survive the round-trip. Fields
3600
3669
  the server adds (ids, defaults) are ignored; only content that was sent and came
@@ -3607,6 +3676,9 @@ back missing or changed is reported.`
3607
3676
  const formData = new FormData();
3608
3677
  const blob = new Blob([yamlBody], { type: "application/x-yaml" });
3609
3678
  formData.append("file_data", blob, basename3(filePath));
3679
+ if (opts.note) {
3680
+ formData.append("version_note", opts.note);
3681
+ }
3610
3682
  const result = await client.postFormData("/fin-sdk/upload", formData);
3611
3683
  render(
3612
3684
  result,
@@ -3615,7 +3687,7 @@ back missing or changed is reported.`
3615
3687
  "ID": String(result.id ?? ""),
3616
3688
  "Type": result.type != null ? String(result.type) : void 0,
3617
3689
  "Message": result.message != null ? String(result.message) : void 0,
3618
- "Visual Builder URL": result.visual_builder_url != null ? String(result.visual_builder_url) : void 0
3690
+ "Web URL": result.web_url != null || result.visual_builder_url != null ? String(result.web_url ?? result.visual_builder_url) : void 0
3619
3691
  })
3620
3692
  );
3621
3693
  if (opts.verify && !isDryRun()) {
@@ -3743,6 +3815,8 @@ function withoutExpectedChanges(sent) {
3743
3815
  const rest = { ...sent };
3744
3816
  delete rest.type;
3745
3817
  delete rest.version;
3818
+ delete rest.web_url;
3819
+ delete rest.workflow_instance_id;
3746
3820
  if (String(rest.id ?? "").toLowerCase() === "new") {
3747
3821
  delete rest.id;
3748
3822
  }
@@ -5193,6 +5267,18 @@ Examples:
5193
5267
  });
5194
5268
  }
5195
5269
 
5270
+ // src/argv.ts
5271
+ function attachVersionValues(argv) {
5272
+ const out = [...argv];
5273
+ for (let i = 3; i < out.length - 1; i++) {
5274
+ if (out[i] === "--") break;
5275
+ if (out[i] === "--version" && !out[i + 1].startsWith("-")) {
5276
+ out.splice(i, 2, `--version=${out[i + 1]}`);
5277
+ }
5278
+ }
5279
+ return out;
5280
+ }
5281
+
5196
5282
  // src/index.ts
5197
5283
  function detectBinaryName() {
5198
5284
  const argv1 = process.argv[1] ?? "";
@@ -5274,7 +5360,7 @@ var isMainModule = process.argv[1]?.endsWith("index.js") || process.argv[1]?.end
5274
5360
  if (isMainModule) {
5275
5361
  const program = createProgram();
5276
5362
  try {
5277
- await program.parseAsync();
5363
+ await program.parseAsync(attachVersionValues(process.argv));
5278
5364
  } catch (err) {
5279
5365
  handleError(err);
5280
5366
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intercom/cli",
3
- "version": "0.14.1",
3
+ "version": "0.15.0",
4
4
  "description": "CLI tool for managing Intercom workspaces",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@10.23.0",