@intercom/cli 0.4.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.
- package/dist/index.js +457 -29
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -714,6 +714,14 @@ Examples:
|
|
|
714
714
|
});
|
|
715
715
|
}
|
|
716
716
|
|
|
717
|
+
// src/utils/claude-code-hint.ts
|
|
718
|
+
var HINT_LINE = '<claude-code-hint v="1" type="plugin" value="intercom@claude-plugins-official" />';
|
|
719
|
+
function emitClaudeCodeHint() {
|
|
720
|
+
if (!process.env.CLAUDECODE) return;
|
|
721
|
+
process.stderr.write(`${HINT_LINE}
|
|
722
|
+
`);
|
|
723
|
+
}
|
|
724
|
+
|
|
717
725
|
// src/commands/auth/login.ts
|
|
718
726
|
function registerLoginCommand(auth) {
|
|
719
727
|
auth.command("login").description("Authenticate with Intercom").option("--token <token>", "Authenticate with an API key or token directly").option("--device", "Use device flow (for headless environments)").addHelpText(
|
|
@@ -752,6 +760,7 @@ async function loginWithToken(token) {
|
|
|
752
760
|
await store.set(appId, { token, auth_type: authType, app_id: appId });
|
|
753
761
|
await addWorkspace(configDir2, { app_id: appId, name });
|
|
754
762
|
console.log(`Authenticated to ${name} (${appId})`);
|
|
763
|
+
emitClaudeCodeHint();
|
|
755
764
|
}
|
|
756
765
|
|
|
757
766
|
// src/auth/cookies.ts
|
|
@@ -1015,7 +1024,12 @@ async function fetchAccessToken(baseUrl, cookies, appPackageId, appId, timeoutMs
|
|
|
1015
1024
|
}
|
|
1016
1025
|
async function getToken(options) {
|
|
1017
1026
|
const { baseUrl, email, password, appId, appPackageId, timeoutMs } = options;
|
|
1018
|
-
|
|
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);
|
|
1019
1033
|
const appPackage = await findOrCreateAppPackage(
|
|
1020
1034
|
baseUrl,
|
|
1021
1035
|
cookies,
|
|
@@ -1796,6 +1810,38 @@ Examples:
|
|
|
1796
1810
|
}
|
|
1797
1811
|
|
|
1798
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
|
+
}
|
|
1799
1845
|
function formatDatetime(ts) {
|
|
1800
1846
|
if (!ts) return "";
|
|
1801
1847
|
const n = typeof ts === "number" ? ts : Number(ts);
|
|
@@ -1893,9 +1939,10 @@ Examples:
|
|
|
1893
1939
|
result = await client.get("/conversations", { per_page: limit });
|
|
1894
1940
|
}
|
|
1895
1941
|
const response = result;
|
|
1896
|
-
const items = response.conversations ?? [];
|
|
1942
|
+
const items = (response.conversations ?? []).map(sanitizeConversation);
|
|
1943
|
+
const sanitizedResult = { ...response, conversations: items };
|
|
1897
1944
|
render(
|
|
1898
|
-
|
|
1945
|
+
sanitizedResult,
|
|
1899
1946
|
opts,
|
|
1900
1947
|
() => {
|
|
1901
1948
|
const rows = items.map((c2) => ({
|
|
@@ -1916,10 +1963,11 @@ Examples:
|
|
|
1916
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) {
|
|
1917
1964
|
const opts = this.optsWithGlobals();
|
|
1918
1965
|
const client = await resolveClient(opts);
|
|
1919
|
-
const result =
|
|
1966
|
+
const result = sanitizeConversation(
|
|
1967
|
+
await client.get(`/conversations/${id}`)
|
|
1968
|
+
);
|
|
1920
1969
|
render(result, opts, () => {
|
|
1921
|
-
|
|
1922
|
-
return renderConversationStream(r);
|
|
1970
|
+
return renderConversationStream(result);
|
|
1923
1971
|
});
|
|
1924
1972
|
});
|
|
1925
1973
|
conversations.command("reply <id>").description("Reply to a conversation").option(
|
|
@@ -1969,9 +2017,10 @@ Required fields: body, type (admin or user). Also: admin_id, message_type.`
|
|
|
1969
2017
|
};
|
|
1970
2018
|
const result = await client.post("/conversations/search", body);
|
|
1971
2019
|
const response = result;
|
|
1972
|
-
const items = response.conversations ?? [];
|
|
2020
|
+
const items = (response.conversations ?? []).map(sanitizeConversation);
|
|
2021
|
+
const sanitizedResult = { ...response, conversations: items };
|
|
1973
2022
|
render(
|
|
1974
|
-
|
|
2023
|
+
sanitizedResult,
|
|
1975
2024
|
opts,
|
|
1976
2025
|
() => {
|
|
1977
2026
|
const rows = items.map((c2) => ({
|
|
@@ -2395,7 +2444,7 @@ Examples:
|
|
|
2395
2444
|
}
|
|
2396
2445
|
|
|
2397
2446
|
// src/commands/fin.ts
|
|
2398
|
-
import { writeFile as
|
|
2447
|
+
import { writeFile as writeFile3 } from "fs/promises";
|
|
2399
2448
|
|
|
2400
2449
|
// src/commands/fin/procedures.ts
|
|
2401
2450
|
import { readFile as readFile2 } from "fs/promises";
|
|
@@ -2604,6 +2653,11 @@ function registerAudiencesCommand(fin) {
|
|
|
2604
2653
|
}
|
|
2605
2654
|
|
|
2606
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;
|
|
2607
2661
|
function registerWorkflowsCommand(fin) {
|
|
2608
2662
|
const workflows = fin.command("workflows").description("Manage Fin SDK workflows");
|
|
2609
2663
|
workflows.command("list").description("List Fin SDK workflows").option("--json", "Output as JSON").option("--verbose", "Show HTTP request/response details").action(async function() {
|
|
@@ -2624,19 +2678,122 @@ function registerWorkflowsCommand(fin) {
|
|
|
2624
2678
|
return table({ columns: ["ID", "TITLE", "STATE", "TRIGGER"], rows });
|
|
2625
2679
|
}, items);
|
|
2626
2680
|
});
|
|
2627
|
-
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) {
|
|
2628
2682
|
const opts = this.optsWithGlobals();
|
|
2629
2683
|
const client = await resolveClient(opts);
|
|
2630
|
-
const
|
|
2631
|
-
const
|
|
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);
|
|
2632
2717
|
render(
|
|
2633
2718
|
result,
|
|
2634
2719
|
opts,
|
|
2635
2720
|
() => detail({
|
|
2636
|
-
"ID": String(
|
|
2637
|
-
"
|
|
2638
|
-
"
|
|
2639
|
-
"
|
|
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
|
|
2640
2797
|
})
|
|
2641
2798
|
);
|
|
2642
2799
|
});
|
|
@@ -2879,6 +3036,275 @@ Fin is live on workspace ${appId}`);
|
|
|
2879
3036
|
});
|
|
2880
3037
|
}
|
|
2881
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
|
+
|
|
2882
3308
|
// src/commands/fin.ts
|
|
2883
3309
|
function renderResourceTable(label, items) {
|
|
2884
3310
|
if (items.length === 0) {
|
|
@@ -2939,19 +3365,21 @@ Examples:
|
|
|
2939
3365
|
registerAudiencesCommand(fin);
|
|
2940
3366
|
registerWorkflowsCommand(fin);
|
|
2941
3367
|
registerSimulationsCommand(fin);
|
|
3368
|
+
registerMonitorsCommand(fin);
|
|
3369
|
+
registerConversationCommand(fin);
|
|
2942
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() {
|
|
2943
3371
|
const opts = this.optsWithGlobals();
|
|
2944
3372
|
const outputPath = opts.output ?? "./fin-resources.zip";
|
|
2945
3373
|
const client = await resolveClient(opts);
|
|
2946
3374
|
const buffer = await client.getBuffer("/fin-sdk/download");
|
|
2947
|
-
await
|
|
3375
|
+
await writeFile3(outputPath, buffer);
|
|
2948
3376
|
console.log(`Downloaded ${buffer.length} bytes to ${outputPath}`);
|
|
2949
3377
|
});
|
|
2950
3378
|
}
|
|
2951
3379
|
|
|
2952
3380
|
// src/commands/setup.ts
|
|
2953
3381
|
import { readdirSync, readFileSync as readFileSync4, statSync } from "fs";
|
|
2954
|
-
import { join as
|
|
3382
|
+
import { join as join5, basename as basename3 } from "path";
|
|
2955
3383
|
function isGreenfield(opts) {
|
|
2956
3384
|
return !!(opts.email && opts.name && (process.env.INTERCOM_PASSWORD || opts.passwordStdin));
|
|
2957
3385
|
}
|
|
@@ -3009,8 +3437,8 @@ function buildPlan(opts) {
|
|
|
3009
3437
|
}
|
|
3010
3438
|
if (mdFiles.length > 0) {
|
|
3011
3439
|
const articleCommands = mdFiles.map((file) => {
|
|
3012
|
-
const title =
|
|
3013
|
-
return `intercom articles create -f title="${title}" -F body=@${
|
|
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)`;
|
|
3014
3442
|
});
|
|
3015
3443
|
steps.push({
|
|
3016
3444
|
step: stepNum++,
|
|
@@ -3076,8 +3504,8 @@ async function runPostAuthSteps(client, opts, results, appId) {
|
|
|
3076
3504
|
let created = 0;
|
|
3077
3505
|
let skipped = 0;
|
|
3078
3506
|
for (const file of mdFiles) {
|
|
3079
|
-
const title =
|
|
3080
|
-
const body = readFileSync4(
|
|
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");
|
|
3081
3509
|
if (existingTitles.has(title)) {
|
|
3082
3510
|
skipped++;
|
|
3083
3511
|
} else {
|
|
@@ -3554,7 +3982,7 @@ function registerPricingCommand(program) {
|
|
|
3554
3982
|
}
|
|
3555
3983
|
|
|
3556
3984
|
// src/commands/config.ts
|
|
3557
|
-
import { join as
|
|
3985
|
+
import { join as join6 } from "path";
|
|
3558
3986
|
var SUPPORTED_KEYS = ["default_workspace", "timeout"];
|
|
3559
3987
|
function configDir() {
|
|
3560
3988
|
return process.env.INTERCOM_CONFIG_DIR ?? defaultConfigDir();
|
|
@@ -3643,7 +4071,7 @@ function registerConfigCommand(program) {
|
|
|
3643
4071
|
console.log(`Set ${key} = ${value}`);
|
|
3644
4072
|
});
|
|
3645
4073
|
cmd.command("path").description("Show configuration file path").action(async function() {
|
|
3646
|
-
console.log(
|
|
4074
|
+
console.log(join6(configDir(), "config.json"));
|
|
3647
4075
|
});
|
|
3648
4076
|
}
|
|
3649
4077
|
|
|
@@ -3667,17 +4095,17 @@ function fullCommandName(cmd) {
|
|
|
3667
4095
|
function createProgram() {
|
|
3668
4096
|
const program = new Command();
|
|
3669
4097
|
const binName2 = detectBinaryName();
|
|
3670
|
-
program.name(binName2).description("CLI tool for managing Intercom workspaces").version(VERSION).option("--json", "Output as JSON").option("--jq <expr>", "Filter JSON output").option("--workspace <id>", "Override default workspace").option("--verbose", "Show HTTP request/response details").option("--no-retry", "Disable automatic rate limit retry").option("--timeout <seconds>", "Request timeout in seconds").option("-n, --dry-run", "Print HTTP requests to stderr without executing them").option("--agent", "Force machine-readable output (JSON/NDJSON)").option("--human", "Force human-readable output even when piped").option("--wide", "Show all columns with wider truncation").addHelpText(
|
|
3671
|
-
|
|
3672
|
-
`
|
|
4098
|
+
program.name(binName2).description("CLI tool for managing Intercom workspaces").version(VERSION).option("--json", "Output as JSON").option("--jq <expr>", "Filter JSON output").option("--workspace <id>", "Override default workspace").option("--verbose", "Show HTTP request/response details").option("--no-retry", "Disable automatic rate limit retry").option("--timeout <seconds>", "Request timeout in seconds").option("-n, --dry-run", "Print HTTP requests to stderr without executing them").option("--agent", "Force machine-readable output (JSON/NDJSON)").option("--human", "Force human-readable output even when piped").option("--wide", "Show all columns with wider truncation").addHelpText("after", () => {
|
|
4099
|
+
emitClaudeCodeHint();
|
|
4100
|
+
return `
|
|
3673
4101
|
Quick start:
|
|
3674
4102
|
$ intercom auth login --token <your-api-token>
|
|
3675
4103
|
$ intercom me
|
|
3676
4104
|
$ intercom articles list --json
|
|
3677
4105
|
$ intercom setup --company-name "Acme" --articles-from ./docs
|
|
3678
4106
|
|
|
3679
|
-
Environment: Set INTERCOM_TOKEN to skip login entirely
|
|
3680
|
-
);
|
|
4107
|
+
Environment: Set INTERCOM_TOKEN to skip login entirely.`;
|
|
4108
|
+
});
|
|
3681
4109
|
registerApiCommand(program);
|
|
3682
4110
|
registerAuthCommand(program);
|
|
3683
4111
|
registerMeCommand(program);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intercom/cli",
|
|
3
|
-
"version": "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",
|