@intercom/cli 0.14.1 → 0.15.1

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 +20 -0
  2. package/dist/index.js +151 -62
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -7,6 +7,26 @@ 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.1] - 2026-11-08
11
+
12
+ ### Fixed
13
+
14
+ - `intercom fin procedures upload --verify` no longer reports drift on `version`, or on `id` when the file says `id: new`. The server assigns both on save, so uploading a procedure you just downloaded no longer fails verification.
15
+
16
+ ## [0.15.0] - 2026-11-06
17
+
18
+ ### Added
19
+
20
+ - `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.
21
+ - `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.
22
+ - `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>`.
23
+
24
+ ### Fixed
25
+
26
+ - `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.
27
+ - `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.
28
+ - `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.
29
+
10
30
  ## [0.14.1] - 2026-10-27
11
31
 
12
32
  ### 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,50 @@ 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
+ }
3026
+ }
3027
+ function withoutExpectedChanges(sent) {
3028
+ if (!isRecord(sent)) {
3029
+ return sent;
3030
+ }
3031
+ const rest = { ...sent };
3032
+ delete rest.type;
3033
+ delete rest.version;
3034
+ delete rest.web_url;
3035
+ delete rest.workflow_instance_id;
3036
+ if (String(rest.id ?? "").toLowerCase() === "new") {
3037
+ delete rest.id;
3038
+ }
3039
+ return rest;
2970
3040
  }
2971
3041
  function findDrift(sent, saved, path = "") {
2972
3042
  const here = path || ".";
@@ -3204,7 +3274,10 @@ function verifySaved(filePath, sentYaml, result) {
3204
3274
  if (typeof result.new_file_content !== "string") {
3205
3275
  throw new CLIError("Cannot verify: the server did not return the saved procedure");
3206
3276
  }
3207
- const drift = findDrift(parseYaml(sentYaml), parseYaml(result.new_file_content));
3277
+ const drift = findDrift(
3278
+ withoutExpectedChanges(parseYaml(sentYaml)),
3279
+ parseYaml(result.new_file_content)
3280
+ );
3208
3281
  if (drift.length === 0) {
3209
3282
  console.error(`Verified: the saved procedure matches ${filePath}`);
3210
3283
  return;
@@ -3532,7 +3605,16 @@ function ensureWorkflowType(raw) {
3532
3605
  }
3533
3606
  function registerWorkflowsCommand(fin) {
3534
3607
  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() {
3608
+ workflows.command("list").description("List Fin SDK workflows").option("--json", "Output as JSON").option("--verbose", "Show HTTP request/response details").addHelpText(
3609
+ "after",
3610
+ `
3611
+ Examples:
3612
+ $ intercom fin workflows list
3613
+ $ intercom fin workflows list --jq '.web_url'
3614
+
3615
+ Each workflow's web_url opens it in the visual builder. Use it rather than
3616
+ building a URL from id: the builder uses a different id.`
3617
+ ).action(async function() {
3536
3618
  const opts = this.optsWithGlobals();
3537
3619
  const client = await resolveClient(opts);
3538
3620
  const result = await client.get("/fin-sdk/workflows");
@@ -3561,7 +3643,8 @@ Examples:
3561
3643
  $ intercom fin workflows get 42 --version 4821 # print that version's YAML
3562
3644
 
3563
3645
  The YAML it prints is ready to edit and re-upload with
3564
- \`intercom fin workflows upload\`.
3646
+ \`intercom fin workflows upload\`. Its web_url field links to the workflow in
3647
+ the visual builder, whose URL uses a different id from this one.
3565
3648
 
3566
3649
  --version downloads the re-uploadable YAML of a historical version, so you can
3567
3650
  diff a local file against what is live (\`intercom fin workflows versions <id>\`
@@ -3582,19 +3665,22 @@ shows which version that is).`
3582
3665
  console.log(text.replace(/\n+$/, ""));
3583
3666
  }
3584
3667
  });
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(
3668
+ 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
3669
  "after",
3587
3670
  `
3588
3671
  Examples:
3589
3672
  $ intercom fin workflows upload ./welcome.yaml
3590
3673
  $ 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
3674
+ $ intercom fin workflows upload ./new.yaml --json | jq .web_url
3592
3675
  $ intercom fin workflows upload ./welcome.yaml --verify
3676
+ $ intercom fin workflows upload ./welcome.yaml --note "added VIP routing"
3593
3677
 
3594
3678
  The YAML file's id field determines whether this creates or updates:
3595
3679
  id: "new" \u2192 creates a new workflow
3596
3680
  id: <int> \u2192 updates the existing workflow with that id
3597
3681
 
3682
+ --note labels this save in \`intercom fin workflows versions <id>\`.
3683
+
3598
3684
  --verify compares the workflow the server saved against the file just uploaded
3599
3685
  and exits non-zero listing any field that did not survive the round-trip. Fields
3600
3686
  the server adds (ids, defaults) are ignored; only content that was sent and came
@@ -3607,6 +3693,9 @@ back missing or changed is reported.`
3607
3693
  const formData = new FormData();
3608
3694
  const blob = new Blob([yamlBody], { type: "application/x-yaml" });
3609
3695
  formData.append("file_data", blob, basename3(filePath));
3696
+ if (opts.note) {
3697
+ formData.append("version_note", opts.note);
3698
+ }
3610
3699
  const result = await client.postFormData("/fin-sdk/upload", formData);
3611
3700
  render(
3612
3701
  result,
@@ -3615,7 +3704,7 @@ back missing or changed is reported.`
3615
3704
  "ID": String(result.id ?? ""),
3616
3705
  "Type": result.type != null ? String(result.type) : void 0,
3617
3706
  "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
3707
+ "Web URL": result.web_url != null || result.visual_builder_url != null ? String(result.web_url ?? result.visual_builder_url) : void 0
3619
3708
  })
3620
3709
  );
3621
3710
  if (opts.verify && !isDryRun()) {
@@ -3736,18 +3825,6 @@ async function getVersionYaml2(client, id, versionId) {
3736
3825
  }
3737
3826
  return typeof content === "string" ? content : stringifyYaml3(content);
3738
3827
  }
3739
- function withoutExpectedChanges(sent) {
3740
- if (sent == null || typeof sent !== "object" || Array.isArray(sent)) {
3741
- return sent;
3742
- }
3743
- const rest = { ...sent };
3744
- delete rest.type;
3745
- delete rest.version;
3746
- if (String(rest.id ?? "").toLowerCase() === "new") {
3747
- delete rest.id;
3748
- }
3749
- return rest;
3750
- }
3751
3828
  function verifySaved2(filePath, sentYaml, result) {
3752
3829
  if (typeof result.new_file_content !== "string") {
3753
3830
  throw new CLIError("Cannot verify: the server did not return the saved workflow");
@@ -5193,6 +5270,18 @@ Examples:
5193
5270
  });
5194
5271
  }
5195
5272
 
5273
+ // src/argv.ts
5274
+ function attachVersionValues(argv) {
5275
+ const out = [...argv];
5276
+ for (let i = 3; i < out.length - 1; i++) {
5277
+ if (out[i] === "--") break;
5278
+ if (out[i] === "--version" && !out[i + 1].startsWith("-")) {
5279
+ out.splice(i, 2, `--version=${out[i + 1]}`);
5280
+ }
5281
+ }
5282
+ return out;
5283
+ }
5284
+
5196
5285
  // src/index.ts
5197
5286
  function detectBinaryName() {
5198
5287
  const argv1 = process.argv[1] ?? "";
@@ -5274,7 +5363,7 @@ var isMainModule = process.argv[1]?.endsWith("index.js") || process.argv[1]?.end
5274
5363
  if (isMainModule) {
5275
5364
  const program = createProgram();
5276
5365
  try {
5277
- await program.parseAsync();
5366
+ await program.parseAsync(attachVersionValues(process.argv));
5278
5367
  } catch (err) {
5279
5368
  handleError(err);
5280
5369
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intercom/cli",
3
- "version": "0.14.1",
3
+ "version": "0.15.1",
4
4
  "description": "CLI tool for managing Intercom workspaces",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@10.23.0",