@intercom/cli 0.8.3 → 0.9.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/CHANGELOG.md CHANGED
@@ -7,6 +7,14 @@ 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.9.0] - 2026-07-03
11
+
12
+ ### Added
13
+
14
+ - `fin attributes upload <file>` creates or updates Fin custom attributes from a YAML file. Attributes were previously read-only (`list`). The file can be a list of attributes, a single attribute, or the full `type: attribute` / `attributes:` server shape — the CLI normalizes it and sets the routing type for you. Each attribute needs a dotted `name` (e.g. `user.order_status`) and a data `type` (`string`, `integer`, `decimal`, `float`, `boolean`, `list`, `date`, `object`, or `array`).
15
+ - `fin procedures get <id> [output] --with-simulations` downloads a procedure's simulations alongside it. With the flag, `[output]` is a directory (default: current directory) and the CLI writes both `procedure.yaml` and `simulations.yaml`. Without it, `[output]` is a single file for the procedure YAML (previously `get` could only print to stdout).
16
+ - `fin simulations upload <file>` re-uploads a simulation set (the `simulations.yaml` written by `fin procedures get --with-simulations`, or `fin simulations list` output), completing the export/edit/re-import round-trip for a procedure's tests.
17
+
10
18
  ## [0.8.3] - 2026-06-18
11
19
 
12
20
  ### Fixed
package/dist/index.js CHANGED
@@ -2569,8 +2569,8 @@ Examples:
2569
2569
  import { writeFile as writeFile3 } from "fs/promises";
2570
2570
 
2571
2571
  // src/commands/fin/procedures.ts
2572
- import { readFile as readFile2 } from "fs/promises";
2573
- import { basename } from "path";
2572
+ import { readFile as readFile2, mkdir as mkdir2 } from "fs/promises";
2573
+ import { basename, join as join3 } from "path";
2574
2574
  import AdmZip from "adm-zip";
2575
2575
  function registerProceduresCommand(fin) {
2576
2576
  const procedures = fin.command("procedures").description("Manage Fin SDK procedures");
@@ -2591,7 +2591,21 @@ function registerProceduresCommand(fin) {
2591
2591
  return table({ columns: ["ID", "NAME"], rows });
2592
2592
  }, items);
2593
2593
  });
2594
- procedures.command("get <id>").description("Download a procedure by ID and print its YAML content").option("--verbose", "Show HTTP request/response details").action(async function(id) {
2594
+ procedures.command("get <id> [output]").description("Download a procedure by ID as YAML (writes to [output], or stdout)").option("--with-simulations", "Also download the procedure's simulations").option("--verbose", "Show HTTP request/response details").addHelpText(
2595
+ "after",
2596
+ `
2597
+ Examples:
2598
+ $ intercom fin procedures get proc-1 # print YAML to stdout
2599
+ $ intercom fin procedures get proc-1 procedure.yaml # write YAML to a file
2600
+ $ intercom fin procedures get proc-1 ./out --with-simulations
2601
+ # writes ./out/procedure.yaml and ./out/simulations.yaml
2602
+
2603
+ With --with-simulations, [output] is treated as a directory (default: current
2604
+ directory). Without it, [output] is a single file for the procedure YAML.
2605
+
2606
+ Re-import the pieces with \`intercom fin procedures upload <procedure.yaml>\`
2607
+ and \`intercom fin simulations upload <simulations.yaml>\`.`
2608
+ ).action(async function(id, output) {
2595
2609
  const opts = this.optsWithGlobals();
2596
2610
  const client = await resolveClient(opts);
2597
2611
  const buffer = await client.getBuffer(`/fin-sdk/procedures/${id}/download`);
@@ -2600,7 +2614,28 @@ function registerProceduresCommand(fin) {
2600
2614
  if (!entry) {
2601
2615
  throw new Error("procedure.yaml not found in downloaded ZIP");
2602
2616
  }
2603
- console.log(zip.readAsText(entry));
2617
+ const procedureYaml = zip.readAsText(entry);
2618
+ let simulationsYaml;
2619
+ if (opts.withSimulations) {
2620
+ simulationsYaml = await client.getText(`/fin-sdk/procedures/${id}/simulations`);
2621
+ }
2622
+ if (opts.withSimulations) {
2623
+ const dir = output ?? ".";
2624
+ await mkdir2(dir, { recursive: true });
2625
+ const procedurePath = join3(dir, "procedure.yaml");
2626
+ const simulationsPath = join3(dir, "simulations.yaml");
2627
+ await atomicWriteFile(procedurePath, procedureYaml, 420);
2628
+ await atomicWriteFile(simulationsPath, simulationsYaml ?? "", 420);
2629
+ console.error(`Wrote procedure ${id} to ${procedurePath}`);
2630
+ console.error(`Wrote simulations to ${simulationsPath}`);
2631
+ return;
2632
+ }
2633
+ if (output) {
2634
+ await atomicWriteFile(output, procedureYaml, 420);
2635
+ console.error(`Wrote procedure ${id} to ${output}`);
2636
+ } else {
2637
+ console.log(procedureYaml);
2638
+ }
2604
2639
  });
2605
2640
  procedures.command("upload <file>").description("Upload a procedure file to Fin SDK").option("--json", "Output as JSON").option("--jq <expr>", "Filter JSON output with jq expression").option("--verbose", "Show HTTP request/response details").action(async function(filePath) {
2606
2641
  const opts = this.optsWithGlobals();
@@ -2766,6 +2801,29 @@ Optional fields: state (live or paused, default: paused), audiences`
2766
2801
  }
2767
2802
 
2768
2803
  // src/commands/fin/attributes.ts
2804
+ import { readFile as readFile3 } from "fs/promises";
2805
+ import { basename as basename2 } from "path";
2806
+ import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
2807
+ var ATTRIBUTE_TYPE = "attribute";
2808
+ function buildAttributesUploadBody(raw) {
2809
+ let parsed;
2810
+ try {
2811
+ parsed = parseYaml(raw);
2812
+ } catch {
2813
+ return raw;
2814
+ }
2815
+ if (Array.isArray(parsed)) {
2816
+ return stringifyYaml({ type: ATTRIBUTE_TYPE, attributes: parsed });
2817
+ }
2818
+ if (parsed == null || typeof parsed !== "object") {
2819
+ return raw;
2820
+ }
2821
+ const obj = parsed;
2822
+ if (Array.isArray(obj.attributes)) {
2823
+ return stringifyYaml({ ...obj, type: ATTRIBUTE_TYPE });
2824
+ }
2825
+ return stringifyYaml({ type: ATTRIBUTE_TYPE, attributes: [obj] });
2826
+ }
2769
2827
  function registerAttributesCommand(fin) {
2770
2828
  const attributes = fin.command("attributes").description("Manage Fin SDK attributes");
2771
2829
  attributes.command("list").description("List Fin SDK attributes").option("--json", "Output as JSON").option("--verbose", "Show HTTP request/response details").action(async function() {
@@ -2785,6 +2843,49 @@ function registerAttributesCommand(fin) {
2785
2843
  return table({ columns: ["NAME", "TYPE", "DESCRIPTION"], rows });
2786
2844
  }, items);
2787
2845
  });
2846
+ attributes.command("upload <file>").description("Upload attributes from a YAML file (creates or updates)").option("--json", "Output as JSON").option("--jq <expr>", "Filter JSON output with jq expression").option("--verbose", "Show HTTP request/response details").addHelpText(
2847
+ "after",
2848
+ `
2849
+ The file holds one or more attributes. The CLI wraps them in the bulk shape the
2850
+ server expects and sets the routing type for you, so any of these work:
2851
+
2852
+ # a list of attributes
2853
+ - name: user.order_status # name must contain a dot
2854
+ type: string # data type: string, integer, decimal, float,
2855
+ description: Order state # boolean, list, date, object, or array
2856
+
2857
+ # ...or a single attribute
2858
+ name: company.tier
2859
+ type: string
2860
+
2861
+ # ...or the full server shape
2862
+ type: attribute
2863
+ attributes:
2864
+ - name: user.order_status
2865
+ type: string
2866
+
2867
+ Examples:
2868
+ $ intercom fin attributes upload ./attributes.yaml
2869
+ $ intercom fin attributes upload ./attributes.yaml --json`
2870
+ ).action(async function(filePath) {
2871
+ const opts = this.optsWithGlobals();
2872
+ const client = await resolveClient(opts);
2873
+ const fileContent = await readFile3(filePath, "utf8");
2874
+ const yamlBody = buildAttributesUploadBody(fileContent);
2875
+ const formData = new FormData();
2876
+ const blob = new Blob([yamlBody], { type: "application/x-yaml" });
2877
+ formData.append("file_data", blob, basename2(filePath));
2878
+ const result = await client.postFormData("/fin-sdk/upload", formData);
2879
+ render(
2880
+ result,
2881
+ opts,
2882
+ () => detail({
2883
+ "ID": result.id != null ? String(result.id) : void 0,
2884
+ "Type": result.type != null ? String(result.type) : void 0,
2885
+ "Message": result.message != null ? String(result.message) : void 0
2886
+ })
2887
+ );
2888
+ });
2788
2889
  }
2789
2890
 
2790
2891
  // src/commands/fin/audiences.ts
@@ -2809,16 +2910,16 @@ function registerAudiencesCommand(fin) {
2809
2910
  }
2810
2911
 
2811
2912
  // src/commands/fin/workflows.ts
2812
- import { readFile as readFile3 } from "fs/promises";
2813
- import { basename as basename2 } from "path";
2814
- import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
2913
+ import { readFile as readFile4 } from "fs/promises";
2914
+ import { basename as basename3 } from "path";
2915
+ import { parse as parseYaml2, stringify as stringifyYaml2 } from "yaml";
2815
2916
  var STATE_LIVE = "live";
2816
2917
  var STATE_PAUSED = "paused";
2817
2918
  var WORKFLOW_TYPE = "workflow";
2818
2919
  function ensureWorkflowType(raw) {
2819
2920
  let parsed;
2820
2921
  try {
2821
- parsed = parseYaml(raw);
2922
+ parsed = parseYaml2(raw);
2822
2923
  } catch {
2823
2924
  return raw;
2824
2925
  }
@@ -2830,7 +2931,7 @@ function ensureWorkflowType(raw) {
2830
2931
  return raw;
2831
2932
  }
2832
2933
  obj.type = WORKFLOW_TYPE;
2833
- return stringifyYaml(obj);
2934
+ return stringifyYaml2(obj);
2834
2935
  }
2835
2936
  function registerWorkflowsCommand(fin) {
2836
2937
  const workflows = fin.command("workflows").description("Manage Fin SDK workflows");
@@ -2867,7 +2968,7 @@ The YAML it prints is ready to edit and re-upload with
2867
2968
  const client = await resolveClient(opts);
2868
2969
  const text = await client.getText(`/fin-sdk/workflows/${id}`);
2869
2970
  if (opts.json || opts.jq || opts.agent) {
2870
- const parsed = parseYaml(text);
2971
+ const parsed = parseYaml2(text);
2871
2972
  render(parsed, opts, () => formatJson(parsed));
2872
2973
  return;
2873
2974
  }
@@ -2892,11 +2993,11 @@ The YAML file's id field determines whether this creates or updates:
2892
2993
  ).action(async function(filePath) {
2893
2994
  const opts = this.optsWithGlobals();
2894
2995
  const client = await resolveClient(opts);
2895
- const fileContent = await readFile3(filePath, "utf8");
2996
+ const fileContent = await readFile4(filePath, "utf8");
2896
2997
  const yamlBody = ensureWorkflowType(fileContent);
2897
2998
  const formData = new FormData();
2898
2999
  const blob = new Blob([yamlBody], { type: "application/x-yaml" });
2899
- formData.append("file_data", blob, basename2(filePath));
3000
+ formData.append("file_data", blob, basename3(filePath));
2900
3001
  const result = await client.postFormData("/fin-sdk/upload", formData);
2901
3002
  render(
2902
3003
  result,
@@ -2925,13 +3026,13 @@ overwritten (the server does not expose a concurrency token).`
2925
3026
  const text = await client.getText(`/fin-sdk/workflows/${id}`);
2926
3027
  let parsed;
2927
3028
  try {
2928
- parsed = parseYaml(text);
3029
+ parsed = parseYaml2(text);
2929
3030
  } catch (err) {
2930
3031
  throw new Error(`server returned unparseable YAML for workflow ${id}: ${err.message}`);
2931
3032
  }
2932
3033
  parsed.type = WORKFLOW_TYPE;
2933
3034
  parsed.state = STATE_LIVE;
2934
- const yamlBody = stringifyYaml(parsed);
3035
+ const yamlBody = stringifyYaml2(parsed);
2935
3036
  const formData = new FormData();
2936
3037
  const blob = new Blob([yamlBody], { type: "application/x-yaml" });
2937
3038
  formData.append("file_data", blob, `workflow-${id}.yaml`);
@@ -2962,13 +3063,13 @@ overwritten (the server does not expose a concurrency token).`
2962
3063
  const text = await client.getText(`/fin-sdk/workflows/${id}`);
2963
3064
  let parsed;
2964
3065
  try {
2965
- parsed = parseYaml(text);
3066
+ parsed = parseYaml2(text);
2966
3067
  } catch (err) {
2967
3068
  throw new Error(`server returned unparseable YAML for workflow ${id}: ${err.message}`);
2968
3069
  }
2969
3070
  parsed.type = WORKFLOW_TYPE;
2970
3071
  parsed.state = STATE_PAUSED;
2971
- const yamlBody = stringifyYaml(parsed);
3072
+ const yamlBody = stringifyYaml2(parsed);
2972
3073
  const formData = new FormData();
2973
3074
  const blob = new Blob([yamlBody], { type: "application/x-yaml" });
2974
3075
  formData.append("file_data", blob, `workflow-${id}.yaml`);
@@ -2986,6 +3087,27 @@ overwritten (the server does not expose a concurrency token).`
2986
3087
  }
2987
3088
 
2988
3089
  // src/commands/fin/simulations.ts
3090
+ import { readFile as readFile5 } from "fs/promises";
3091
+ import { basename as basename4 } from "path";
3092
+ import { parse as parseYaml3, stringify as stringifyYaml3 } from "yaml";
3093
+ var SIMULATION_SET_TYPE = "simulation_set";
3094
+ function ensureSimulationSetType(raw) {
3095
+ let parsed;
3096
+ try {
3097
+ parsed = parseYaml3(raw);
3098
+ } catch {
3099
+ return raw;
3100
+ }
3101
+ if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) {
3102
+ return raw;
3103
+ }
3104
+ const obj = parsed;
3105
+ if (obj.type != null && obj.type !== "") {
3106
+ return raw;
3107
+ }
3108
+ obj.type = SIMULATION_SET_TYPE;
3109
+ return stringifyYaml3(obj);
3110
+ }
2989
3111
  function registerSimulationsCommand(fin) {
2990
3112
  const simulations = fin.command("simulations").description("Manage Fin SDK simulations");
2991
3113
  simulations.command("list <procedure_id>").description("List simulations for a procedure (displays YAML)").option("--verbose", "Show HTTP request/response details").action(async function(procedureId) {
@@ -2994,6 +3116,36 @@ function registerSimulationsCommand(fin) {
2994
3116
  const text = await client.getText(`/fin-sdk/procedures/${procedureId}/simulations`);
2995
3117
  console.log(text);
2996
3118
  });
3119
+ simulations.command("upload <file>").description("Upload a simulation set YAML file for its procedure").option("--json", "Output as JSON").option("--jq <expr>", "Filter JSON output with jq expression").option("--verbose", "Show HTTP request/response details").addHelpText(
3120
+ "after",
3121
+ `
3122
+ The file is a simulation set carrying its own procedure_id, as produced by
3123
+ \`intercom fin procedures get <id> ./out --with-simulations\` (writes
3124
+ ./out/simulations.yaml) or \`intercom fin simulations list <procedure_id>\`.
3125
+ This completes the export/edit/re-import round-trip for a procedure's tests.
3126
+
3127
+ Example:
3128
+ $ intercom fin simulations upload ./out/simulations.yaml`
3129
+ ).action(async function(filePath) {
3130
+ const opts = this.optsWithGlobals();
3131
+ const client = await resolveClient(opts);
3132
+ const fileContent = await readFile5(filePath, "utf8");
3133
+ const yamlBody = ensureSimulationSetType(fileContent);
3134
+ const formData = new FormData();
3135
+ const blob = new Blob([yamlBody], { type: "application/x-yaml" });
3136
+ formData.append("file_data", blob, basename4(filePath));
3137
+ const result = await client.postFormData("/fin-sdk/upload", formData);
3138
+ render(
3139
+ result,
3140
+ opts,
3141
+ () => detail({
3142
+ "ID": result.id != null ? String(result.id) : void 0,
3143
+ "Procedure ID": result.procedure_id != null ? String(result.procedure_id) : void 0,
3144
+ "Type": result.type != null ? String(result.type) : void 0,
3145
+ "Message": result.message != null ? String(result.message) : void 0
3146
+ })
3147
+ );
3148
+ });
2997
3149
  simulations.command("run <procedure_id> [test_id]").description("Run one or all simulations for a procedure").option("--json", "Output as JSON").option("--verbose", "Show HTTP request/response details").action(async function(procedureId, testId) {
2998
3150
  const opts = this.optsWithGlobals();
2999
3151
  const client = await resolveClient(opts);
@@ -3223,8 +3375,8 @@ Fin is live on workspace ${appId}`);
3223
3375
  }
3224
3376
 
3225
3377
  // src/commands/fin/monitors.ts
3226
- import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
3227
- import { join as join3, relative, dirname } from "path";
3378
+ import { mkdir as mkdir3, writeFile as writeFile2 } from "fs/promises";
3379
+ import { join as join4, relative, dirname } from "path";
3228
3380
  import { stringify } from "yaml";
3229
3381
  async function resolveMonitorId(client, nameOrId) {
3230
3382
  if (/^\d+$/.test(nameOrId)) {
@@ -3305,14 +3457,14 @@ Examples:
3305
3457
  );
3306
3458
  return;
3307
3459
  }
3308
- const outputPath = opts.output ?? join3(
3460
+ const outputPath = opts.output ?? join4(
3309
3461
  process.cwd(),
3310
3462
  "src",
3311
3463
  "qa",
3312
3464
  "bulk-actions",
3313
3465
  "flagged-conversations.yaml"
3314
3466
  );
3315
- await mkdir2(dirname(outputPath), { recursive: true });
3467
+ await mkdir3(dirname(outputPath), { recursive: true });
3316
3468
  await writeFile2(outputPath, buildWorkingSetYaml(result, since), "utf-8");
3317
3469
  const relPath = relative(process.cwd(), outputPath);
3318
3470
  console.log(
@@ -3330,8 +3482,8 @@ Examples:
3330
3482
  }
3331
3483
 
3332
3484
  // src/commands/fin/conversation.ts
3333
- import { readFile as readFile4 } from "fs/promises";
3334
- import { join as join4 } from "path";
3485
+ import { readFile as readFile6 } from "fs/promises";
3486
+ import { join as join5 } from "path";
3335
3487
  import { parse } from "yaml";
3336
3488
  var PLACEHOLDER_MESSAGE = "Your message here \u2014 this will be sent to all conversations listed below.";
3337
3489
  function validateWorkingSet(ws) {
@@ -3381,7 +3533,7 @@ function registerConversationCommand(fin) {
3381
3533
  ).option(
3382
3534
  "--file <path>",
3383
3535
  "Path to the working set YAML file",
3384
- join4(process.cwd(), "src", "qa", "bulk-actions", "flagged-conversations.yaml")
3536
+ join5(process.cwd(), "src", "qa", "bulk-actions", "flagged-conversations.yaml")
3385
3537
  ).option("--dry-run", "Preview actions without sending").option("--yes", "Skip confirmation prompt (required in TTY mode)").option(
3386
3538
  "--json",
3387
3539
  "Output result as JSON"
@@ -3395,7 +3547,7 @@ Examples:
3395
3547
  $ fin conversation bulk-send --json`
3396
3548
  ).action(async function() {
3397
3549
  const opts = this.optsWithGlobals();
3398
- const filePath = opts.file ?? join4(
3550
+ const filePath = opts.file ?? join5(
3399
3551
  process.cwd(),
3400
3552
  "src",
3401
3553
  "qa",
@@ -3404,7 +3556,7 @@ Examples:
3404
3556
  );
3405
3557
  let raw;
3406
3558
  try {
3407
- raw = await readFile4(filePath, "utf-8");
3559
+ raw = await readFile6(filePath, "utf-8");
3408
3560
  } catch (err) {
3409
3561
  const e = err;
3410
3562
  if (e.code === "ENOENT") {
@@ -3565,7 +3717,7 @@ Examples:
3565
3717
 
3566
3718
  // src/commands/setup.ts
3567
3719
  import { readdirSync, readFileSync as readFileSync4, statSync } from "fs";
3568
- import { join as join5, basename as basename3 } from "path";
3720
+ import { join as join6, basename as basename5 } from "path";
3569
3721
  function isGreenfield(opts) {
3570
3722
  return !!(opts.email && opts.name && (process.env.INTERCOM_PASSWORD || opts.passwordStdin));
3571
3723
  }
@@ -3623,8 +3775,8 @@ function buildPlan(opts) {
3623
3775
  }
3624
3776
  if (mdFiles.length > 0) {
3625
3777
  const articleCommands = mdFiles.map((file) => {
3626
- const title = basename3(file, ".md").replace(/-/g, " ").replace(/\b\w/g, (c2) => c2.toUpperCase());
3627
- return `intercom articles create -f title="${title}" -F body=@${join5(opts.articlesFrom, file)} -f author_id=$(intercom me --json | jq -r .id)`;
3778
+ const title = basename5(file, ".md").replace(/-/g, " ").replace(/\b\w/g, (c2) => c2.toUpperCase());
3779
+ return `intercom articles create -f title="${title}" -F body=@${join6(opts.articlesFrom, file)} -f author_id=$(intercom me --json | jq -r .id)`;
3628
3780
  });
3629
3781
  steps.push({
3630
3782
  step: stepNum++,
@@ -3690,8 +3842,8 @@ async function runPostAuthSteps(client, opts, results, appId) {
3690
3842
  let created = 0;
3691
3843
  let skipped = 0;
3692
3844
  for (const file of mdFiles) {
3693
- const title = basename3(file, ".md").replace(/-/g, " ").replace(/\b\w/g, (c2) => c2.toUpperCase());
3694
- const body = readFileSync4(join5(opts.articlesFrom, file), "utf-8");
3845
+ const title = basename5(file, ".md").replace(/-/g, " ").replace(/\b\w/g, (c2) => c2.toUpperCase());
3846
+ const body = readFileSync4(join6(opts.articlesFrom, file), "utf-8");
3695
3847
  if (existingTitles.has(title)) {
3696
3848
  skipped++;
3697
3849
  } else {
@@ -4168,7 +4320,7 @@ function registerPricingCommand(program) {
4168
4320
  }
4169
4321
 
4170
4322
  // src/commands/config.ts
4171
- import { join as join6 } from "path";
4323
+ import { join as join7 } from "path";
4172
4324
  var SUPPORTED_KEYS = ["default_workspace", "timeout"];
4173
4325
  function configDir() {
4174
4326
  return process.env.INTERCOM_CONFIG_DIR ?? defaultConfigDir();
@@ -4257,7 +4409,7 @@ function registerConfigCommand(program) {
4257
4409
  console.log(`Set ${key} = ${value}`);
4258
4410
  });
4259
4411
  cmd.command("path").description("Show configuration file path").action(async function() {
4260
- console.log(join6(configDir(), "config.json"));
4412
+ console.log(join7(configDir(), "config.json"));
4261
4413
  });
4262
4414
  }
4263
4415
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intercom/cli",
3
- "version": "0.8.3",
3
+ "version": "0.9.0",
4
4
  "description": "CLI tool for managing Intercom workspaces",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@10.23.0",