@intercom/cli 0.5.0 → 0.7.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.
|
@@ -26,8 +26,8 @@ var NotFoundError = class extends CLIError {
|
|
|
26
26
|
}
|
|
27
27
|
};
|
|
28
28
|
var PermissionError = class extends CLIError {
|
|
29
|
-
constructor(message) {
|
|
30
|
-
super(message, 5);
|
|
29
|
+
constructor(message, error) {
|
|
30
|
+
super(message, 5, error);
|
|
31
31
|
}
|
|
32
32
|
};
|
|
33
33
|
var TimeoutError = class extends CLIError {
|
|
@@ -106,4 +106,4 @@ export {
|
|
|
106
106
|
formatJson,
|
|
107
107
|
applyJq
|
|
108
108
|
};
|
|
109
|
-
//# sourceMappingURL=chunk-
|
|
109
|
+
//# sourceMappingURL=chunk-3V2ONMAU.js.map
|
package/dist/index.js
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
applyJq,
|
|
10
10
|
detectAuthType,
|
|
11
11
|
formatJson
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-3V2ONMAU.js";
|
|
13
13
|
|
|
14
14
|
// src/index.ts
|
|
15
15
|
import { Command } from "commander";
|
|
@@ -65,7 +65,7 @@ var _dryRun = false;
|
|
|
65
65
|
function setDryRun(enabled) {
|
|
66
66
|
_dryRun = enabled;
|
|
67
67
|
}
|
|
68
|
-
var IntercomClient = class {
|
|
68
|
+
var IntercomClient = class _IntercomClient {
|
|
69
69
|
token;
|
|
70
70
|
authType;
|
|
71
71
|
appId;
|
|
@@ -88,7 +88,49 @@ var IntercomClient = class {
|
|
|
88
88
|
}
|
|
89
89
|
return `Bearer ${this.token}`;
|
|
90
90
|
}
|
|
91
|
-
|
|
91
|
+
// Dedicated error code the API returns when the `fin-sdk` feature flag gates
|
|
92
|
+
// a fin subcommand. It's specific to that self-fixable gate (not the generic
|
|
93
|
+
// plan-restriction code), so seeing it is a safe signal to auto-enable.
|
|
94
|
+
static FIN_SDK_GATE_CODE = "fin_sdk_not_enabled";
|
|
95
|
+
// Thin wrapper over requestOnce that transparently recovers from the fin-sdk
|
|
96
|
+
// gate: if a call is blocked because the flag isn't enabled, opt the
|
|
97
|
+
// workspace in via POST /fin-sdk/enable and retry the original request once.
|
|
98
|
+
// If enabling fails, the original gate error is surfaced unchanged.
|
|
99
|
+
async request(method, path, body, responseType = "json", extraHeaders, isFinSdkRetry = false) {
|
|
100
|
+
try {
|
|
101
|
+
return await this.requestOnce(
|
|
102
|
+
method,
|
|
103
|
+
path,
|
|
104
|
+
body,
|
|
105
|
+
responseType,
|
|
106
|
+
extraHeaders
|
|
107
|
+
);
|
|
108
|
+
} catch (err) {
|
|
109
|
+
if (!isFinSdkRetry && !_dryRun && err instanceof PermissionError && err.error?.code === _IntercomClient.FIN_SDK_GATE_CODE) {
|
|
110
|
+
try {
|
|
111
|
+
await this.enableFinSdk();
|
|
112
|
+
} catch {
|
|
113
|
+
throw err;
|
|
114
|
+
}
|
|
115
|
+
return this.request(
|
|
116
|
+
method,
|
|
117
|
+
path,
|
|
118
|
+
body,
|
|
119
|
+
responseType,
|
|
120
|
+
extraHeaders,
|
|
121
|
+
true
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
throw err;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// Opt the current workspace into the fin-sdk feature flag. The notice goes to
|
|
128
|
+
// stderr so it never contaminates --json / piped stdout output.
|
|
129
|
+
async enableFinSdk() {
|
|
130
|
+
console.error("Fin SDK not enabled for this workspace \u2014 enabling it now...");
|
|
131
|
+
await this.requestOnce("POST", "/fin-sdk/enable", {}, "json");
|
|
132
|
+
}
|
|
133
|
+
async requestOnce(method, path, body, responseType = "json", extraHeaders) {
|
|
92
134
|
const url = `${this.baseUrl}${path}`;
|
|
93
135
|
if (_dryRun) {
|
|
94
136
|
console.error(`> ${method} ${url}`);
|
|
@@ -149,13 +191,18 @@ var IntercomClient = class {
|
|
|
149
191
|
}
|
|
150
192
|
const errorBody = await response.json().catch(() => ({}));
|
|
151
193
|
const errorMsg = errorBody?.errors?.[0]?.message ?? `HTTP ${response.status}`;
|
|
194
|
+
const errorCode = errorBody?.errors?.[0]?.code ?? "";
|
|
152
195
|
if (response.status === 401) {
|
|
153
196
|
throw new AuthError(
|
|
154
197
|
`Authentication expired or invalid: ${errorMsg}. Run "intercom auth login" to re-authenticate.`
|
|
155
198
|
);
|
|
156
199
|
}
|
|
157
200
|
if (response.status === 403) {
|
|
158
|
-
throw new PermissionError(`Permission denied: ${errorMsg}
|
|
201
|
+
throw new PermissionError(`Permission denied: ${errorMsg}`, {
|
|
202
|
+
code: errorCode,
|
|
203
|
+
message: errorMsg,
|
|
204
|
+
status: 403
|
|
205
|
+
});
|
|
159
206
|
}
|
|
160
207
|
if (response.status === 404) {
|
|
161
208
|
throw new NotFoundError(errorMsg);
|
|
@@ -905,13 +952,18 @@ async function deviseSignIn(baseUrl, email, password, timeoutMs = API_TIMEOUT_MS
|
|
|
905
952
|
}
|
|
906
953
|
const location = signInResponse.headers.get("location") ?? "";
|
|
907
954
|
if (location.includes("/admins/sign_in")) {
|
|
908
|
-
throw new
|
|
955
|
+
throw new AuthError("Invalid credentials");
|
|
909
956
|
}
|
|
910
957
|
if (location.includes("two_factor")) {
|
|
911
|
-
throw new
|
|
958
|
+
throw new AuthError(
|
|
912
959
|
"Two-factor authentication is required. Please use an API key instead."
|
|
913
960
|
);
|
|
914
961
|
}
|
|
962
|
+
if (location.includes("/login_verification")) {
|
|
963
|
+
throw new AuthError(
|
|
964
|
+
"Login email verification is required.\n Please sign in to the Intercom web app to complete verification, then retry this command."
|
|
965
|
+
);
|
|
966
|
+
}
|
|
915
967
|
cookies = mergeCookies(cookies, extractCookies(signInResponse));
|
|
916
968
|
return cookies;
|
|
917
969
|
}
|
|
@@ -1024,7 +1076,12 @@ async function fetchAccessToken(baseUrl, cookies, appPackageId, appId, timeoutMs
|
|
|
1024
1076
|
}
|
|
1025
1077
|
async function getToken(options) {
|
|
1026
1078
|
const { baseUrl, email, password, appId, appPackageId, timeoutMs } = options;
|
|
1027
|
-
|
|
1079
|
+
let cookies = await deviseSignIn(baseUrl, email, password, timeoutMs);
|
|
1080
|
+
const appResponse = await fetchWithTimeout(`${baseUrl}/a/apps/${appId}`, {
|
|
1081
|
+
headers: { Cookie: cookies }
|
|
1082
|
+
}, { timeoutMs: timeoutMs ?? API_TIMEOUT_MS });
|
|
1083
|
+
const appCookies = extractCookies(appResponse);
|
|
1084
|
+
if (appCookies) cookies = mergeCookies(cookies, appCookies);
|
|
1028
1085
|
const appPackage = await findOrCreateAppPackage(
|
|
1029
1086
|
baseUrl,
|
|
1030
1087
|
cookies,
|
|
@@ -1805,6 +1862,38 @@ Examples:
|
|
|
1805
1862
|
}
|
|
1806
1863
|
|
|
1807
1864
|
// src/commands/conversations.ts
|
|
1865
|
+
var BLOCKED_FIELDS = [
|
|
1866
|
+
"topics",
|
|
1867
|
+
"ai_topics",
|
|
1868
|
+
"monitor_evaluations",
|
|
1869
|
+
"scorecards"
|
|
1870
|
+
];
|
|
1871
|
+
var BLOCKED_CUSTOM_ATTRIBUTE_KEYS = [
|
|
1872
|
+
"AI Topic",
|
|
1873
|
+
"AI Subtopic",
|
|
1874
|
+
"AI Topic and Subtopic",
|
|
1875
|
+
"AI Issue summary",
|
|
1876
|
+
"Query Type",
|
|
1877
|
+
"CX Score rating",
|
|
1878
|
+
"CX Score explanation",
|
|
1879
|
+
"CX Score reasons",
|
|
1880
|
+
"Review status",
|
|
1881
|
+
"Reviewer"
|
|
1882
|
+
];
|
|
1883
|
+
function sanitizeConversation(conversation) {
|
|
1884
|
+
const sanitized = { ...conversation };
|
|
1885
|
+
for (const field of BLOCKED_FIELDS) {
|
|
1886
|
+
delete sanitized[field];
|
|
1887
|
+
}
|
|
1888
|
+
if (sanitized.custom_attributes && typeof sanitized.custom_attributes === "object") {
|
|
1889
|
+
const filtered = { ...sanitized.custom_attributes };
|
|
1890
|
+
for (const key of BLOCKED_CUSTOM_ATTRIBUTE_KEYS) {
|
|
1891
|
+
delete filtered[key];
|
|
1892
|
+
}
|
|
1893
|
+
sanitized.custom_attributes = filtered;
|
|
1894
|
+
}
|
|
1895
|
+
return sanitized;
|
|
1896
|
+
}
|
|
1808
1897
|
function formatDatetime(ts) {
|
|
1809
1898
|
if (!ts) return "";
|
|
1810
1899
|
const n = typeof ts === "number" ? ts : Number(ts);
|
|
@@ -1902,9 +1991,10 @@ Examples:
|
|
|
1902
1991
|
result = await client.get("/conversations", { per_page: limit });
|
|
1903
1992
|
}
|
|
1904
1993
|
const response = result;
|
|
1905
|
-
const items = response.conversations ?? [];
|
|
1994
|
+
const items = (response.conversations ?? []).map(sanitizeConversation);
|
|
1995
|
+
const sanitizedResult = { ...response, conversations: items };
|
|
1906
1996
|
render(
|
|
1907
|
-
|
|
1997
|
+
sanitizedResult,
|
|
1908
1998
|
opts,
|
|
1909
1999
|
() => {
|
|
1910
2000
|
const rows = items.map((c2) => ({
|
|
@@ -1925,10 +2015,11 @@ Examples:
|
|
|
1925
2015
|
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
2016
|
const opts = this.optsWithGlobals();
|
|
1927
2017
|
const client = await resolveClient(opts);
|
|
1928
|
-
const result =
|
|
2018
|
+
const result = sanitizeConversation(
|
|
2019
|
+
await client.get(`/conversations/${id}`)
|
|
2020
|
+
);
|
|
1929
2021
|
render(result, opts, () => {
|
|
1930
|
-
|
|
1931
|
-
return renderConversationStream(r);
|
|
2022
|
+
return renderConversationStream(result);
|
|
1932
2023
|
});
|
|
1933
2024
|
});
|
|
1934
2025
|
conversations.command("reply <id>").description("Reply to a conversation").option(
|
|
@@ -1978,9 +2069,10 @@ Required fields: body, type (admin or user). Also: admin_id, message_type.`
|
|
|
1978
2069
|
};
|
|
1979
2070
|
const result = await client.post("/conversations/search", body);
|
|
1980
2071
|
const response = result;
|
|
1981
|
-
const items = response.conversations ?? [];
|
|
2072
|
+
const items = (response.conversations ?? []).map(sanitizeConversation);
|
|
2073
|
+
const sanitizedResult = { ...response, conversations: items };
|
|
1982
2074
|
render(
|
|
1983
|
-
|
|
2075
|
+
sanitizedResult,
|
|
1984
2076
|
opts,
|
|
1985
2077
|
() => {
|
|
1986
2078
|
const rows = items.map((c2) => ({
|
|
@@ -2404,7 +2496,7 @@ Examples:
|
|
|
2404
2496
|
}
|
|
2405
2497
|
|
|
2406
2498
|
// src/commands/fin.ts
|
|
2407
|
-
import { writeFile as
|
|
2499
|
+
import { writeFile as writeFile3 } from "fs/promises";
|
|
2408
2500
|
|
|
2409
2501
|
// src/commands/fin/procedures.ts
|
|
2410
2502
|
import { readFile as readFile2 } from "fs/promises";
|
|
@@ -2613,6 +2705,11 @@ function registerAudiencesCommand(fin) {
|
|
|
2613
2705
|
}
|
|
2614
2706
|
|
|
2615
2707
|
// src/commands/fin/workflows.ts
|
|
2708
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
2709
|
+
import { basename as basename2 } from "path";
|
|
2710
|
+
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
|
2711
|
+
var STATE_LIVE = 1;
|
|
2712
|
+
var STATE_PAUSED = 2;
|
|
2616
2713
|
function registerWorkflowsCommand(fin) {
|
|
2617
2714
|
const workflows = fin.command("workflows").description("Manage Fin SDK workflows");
|
|
2618
2715
|
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 +2730,122 @@ function registerWorkflowsCommand(fin) {
|
|
|
2633
2730
|
return table({ columns: ["ID", "TITLE", "STATE", "TRIGGER"], rows });
|
|
2634
2731
|
}, items);
|
|
2635
2732
|
});
|
|
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) {
|
|
2733
|
+
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
2734
|
const opts = this.optsWithGlobals();
|
|
2638
2735
|
const client = await resolveClient(opts);
|
|
2639
|
-
const
|
|
2640
|
-
const
|
|
2736
|
+
const text = await client.getText(`/fin-sdk/workflows/${id}`);
|
|
2737
|
+
const parsed = parseYaml(text);
|
|
2738
|
+
render(
|
|
2739
|
+
parsed,
|
|
2740
|
+
opts,
|
|
2741
|
+
() => detail({
|
|
2742
|
+
"ID": String(parsed.id ?? ""),
|
|
2743
|
+
"Title": String(parsed.title ?? ""),
|
|
2744
|
+
"State": String(parsed.state ?? ""),
|
|
2745
|
+
"Version": parsed.version != null ? String(parsed.version) : void 0,
|
|
2746
|
+
"Trigger": parsed.trigger_type != null ? String(parsed.trigger_type) : void 0
|
|
2747
|
+
})
|
|
2748
|
+
);
|
|
2749
|
+
});
|
|
2750
|
+
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(
|
|
2751
|
+
"after",
|
|
2752
|
+
`
|
|
2753
|
+
Examples:
|
|
2754
|
+
$ intercom fin workflows upload ./welcome.yaml
|
|
2755
|
+
$ intercom fin workflows get wf-42 > local.yaml; vim local.yaml; intercom fin workflows upload local.yaml
|
|
2756
|
+
$ intercom fin workflows upload ./new.yaml --json | jq .visual_builder_url
|
|
2757
|
+
|
|
2758
|
+
The YAML file's id field determines whether this creates or updates:
|
|
2759
|
+
id: "new" \u2192 creates a new workflow
|
|
2760
|
+
id: <int> \u2192 updates the existing workflow with that id`
|
|
2761
|
+
).action(async function(filePath) {
|
|
2762
|
+
const opts = this.optsWithGlobals();
|
|
2763
|
+
const client = await resolveClient(opts);
|
|
2764
|
+
const fileContent = await readFile3(filePath);
|
|
2765
|
+
const formData = new FormData();
|
|
2766
|
+
const blob = new Blob([fileContent], { type: "application/x-yaml" });
|
|
2767
|
+
formData.append("file_data", blob, basename2(filePath));
|
|
2768
|
+
const result = await client.postFormData("/fin-sdk/upload", formData);
|
|
2769
|
+
render(
|
|
2770
|
+
result,
|
|
2771
|
+
opts,
|
|
2772
|
+
() => detail({
|
|
2773
|
+
"ID": String(result.id ?? ""),
|
|
2774
|
+
"Type": result.type != null ? String(result.type) : void 0,
|
|
2775
|
+
"Message": result.message != null ? String(result.message) : void 0,
|
|
2776
|
+
"Visual Builder URL": result.visual_builder_url != null ? String(result.visual_builder_url) : void 0
|
|
2777
|
+
})
|
|
2778
|
+
);
|
|
2779
|
+
});
|
|
2780
|
+
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(
|
|
2781
|
+
"after",
|
|
2782
|
+
`
|
|
2783
|
+
Examples:
|
|
2784
|
+
$ intercom fin workflows enable 42
|
|
2785
|
+
$ intercom fin workflows enable 42 --json
|
|
2786
|
+
|
|
2787
|
+
This is a convenience for GET \u2192 set state \u2192 POST. If another operator
|
|
2788
|
+
edits the workflow between the GET and POST, those edits will be
|
|
2789
|
+
overwritten (the server does not expose a concurrency token).`
|
|
2790
|
+
).action(async function(id) {
|
|
2791
|
+
const opts = this.optsWithGlobals();
|
|
2792
|
+
const client = await resolveClient(opts);
|
|
2793
|
+
const text = await client.getText(`/fin-sdk/workflows/${id}`);
|
|
2794
|
+
let parsed;
|
|
2795
|
+
try {
|
|
2796
|
+
parsed = parseYaml(text);
|
|
2797
|
+
} catch (err) {
|
|
2798
|
+
throw new Error(`server returned unparseable YAML for workflow ${id}: ${err.message}`);
|
|
2799
|
+
}
|
|
2800
|
+
parsed.state = STATE_LIVE;
|
|
2801
|
+
const yamlBody = stringifyYaml(parsed);
|
|
2802
|
+
const formData = new FormData();
|
|
2803
|
+
const blob = new Blob([yamlBody], { type: "application/x-yaml" });
|
|
2804
|
+
formData.append("file_data", blob, `workflow-${id}.yaml`);
|
|
2805
|
+
const result = await client.postFormData("/fin-sdk/upload", formData);
|
|
2806
|
+
render(
|
|
2807
|
+
result,
|
|
2808
|
+
opts,
|
|
2809
|
+
() => detail({
|
|
2810
|
+
"ID": String(result.id ?? ""),
|
|
2811
|
+
"Type": result.type != null ? String(result.type) : void 0,
|
|
2812
|
+
"Message": result.message != null ? String(result.message) : void 0
|
|
2813
|
+
})
|
|
2814
|
+
);
|
|
2815
|
+
});
|
|
2816
|
+
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(
|
|
2817
|
+
"after",
|
|
2818
|
+
`
|
|
2819
|
+
Examples:
|
|
2820
|
+
$ intercom fin workflows disable 42
|
|
2821
|
+
$ intercom fin workflows disable 42 --json
|
|
2822
|
+
|
|
2823
|
+
This is a convenience for GET \u2192 set state \u2192 POST. If another operator
|
|
2824
|
+
edits the workflow between the GET and POST, those edits will be
|
|
2825
|
+
overwritten (the server does not expose a concurrency token).`
|
|
2826
|
+
).action(async function(id) {
|
|
2827
|
+
const opts = this.optsWithGlobals();
|
|
2828
|
+
const client = await resolveClient(opts);
|
|
2829
|
+
const text = await client.getText(`/fin-sdk/workflows/${id}`);
|
|
2830
|
+
let parsed;
|
|
2831
|
+
try {
|
|
2832
|
+
parsed = parseYaml(text);
|
|
2833
|
+
} catch (err) {
|
|
2834
|
+
throw new Error(`server returned unparseable YAML for workflow ${id}: ${err.message}`);
|
|
2835
|
+
}
|
|
2836
|
+
parsed.state = STATE_PAUSED;
|
|
2837
|
+
const yamlBody = stringifyYaml(parsed);
|
|
2838
|
+
const formData = new FormData();
|
|
2839
|
+
const blob = new Blob([yamlBody], { type: "application/x-yaml" });
|
|
2840
|
+
formData.append("file_data", blob, `workflow-${id}.yaml`);
|
|
2841
|
+
const result = await client.postFormData("/fin-sdk/upload", formData);
|
|
2641
2842
|
render(
|
|
2642
2843
|
result,
|
|
2643
2844
|
opts,
|
|
2644
2845
|
() => detail({
|
|
2645
|
-
"ID": String(
|
|
2646
|
-
"
|
|
2647
|
-
"
|
|
2648
|
-
"Trigger": r.trigger_type != null ? String(r.trigger_type) : void 0
|
|
2846
|
+
"ID": String(result.id ?? ""),
|
|
2847
|
+
"Type": result.type != null ? String(result.type) : void 0,
|
|
2848
|
+
"Message": result.message != null ? String(result.message) : void 0
|
|
2649
2849
|
})
|
|
2650
2850
|
);
|
|
2651
2851
|
});
|
|
@@ -2888,6 +3088,275 @@ Fin is live on workspace ${appId}`);
|
|
|
2888
3088
|
});
|
|
2889
3089
|
}
|
|
2890
3090
|
|
|
3091
|
+
// src/commands/fin/monitors.ts
|
|
3092
|
+
import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
|
|
3093
|
+
import { join as join3, relative, dirname } from "path";
|
|
3094
|
+
import { stringify } from "yaml";
|
|
3095
|
+
async function resolveMonitorId(client, nameOrId) {
|
|
3096
|
+
if (/^\d+$/.test(nameOrId)) {
|
|
3097
|
+
return parseInt(nameOrId, 10);
|
|
3098
|
+
}
|
|
3099
|
+
const monitors = await client.get("/fin-sdk/qa/monitors");
|
|
3100
|
+
const exact = monitors.find((m) => m.name === nameOrId);
|
|
3101
|
+
if (exact) return parseInt(exact.id, 10);
|
|
3102
|
+
const lower = nameOrId.toLowerCase();
|
|
3103
|
+
const fuzzy = monitors.find((m) => m.name.toLowerCase().includes(lower));
|
|
3104
|
+
if (fuzzy) return parseInt(fuzzy.id, 10);
|
|
3105
|
+
throw new CLIError(
|
|
3106
|
+
`Monitor not found: "${nameOrId}". Check the name or use the numeric ID.`
|
|
3107
|
+
);
|
|
3108
|
+
}
|
|
3109
|
+
function buildWorkingSetYaml(result, since) {
|
|
3110
|
+
const header = [
|
|
3111
|
+
`# Generated by: fin monitor pull-flagged "${result.monitor_name}" --since ${since}`,
|
|
3112
|
+
`# Generated at: ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
3113
|
+
`# Monitor: ${result.monitor_name} (ID: ${result.monitor_id})`,
|
|
3114
|
+
`# Showing: ${result.conversations.length} of ${result.total_count} total flagged`,
|
|
3115
|
+
"#",
|
|
3116
|
+
"# Edit this file:",
|
|
3117
|
+
"# - Remove conversations you don't want to act on",
|
|
3118
|
+
"# - Set the action and message below",
|
|
3119
|
+
"# - Then run: fin conversation bulk-send --yes",
|
|
3120
|
+
""
|
|
3121
|
+
].join("\n");
|
|
3122
|
+
const doc = {
|
|
3123
|
+
action: "reply",
|
|
3124
|
+
message: "Your message here \u2014 this will be sent to all conversations listed below.\n",
|
|
3125
|
+
close_after_reply: false,
|
|
3126
|
+
conversations: result.conversations.map((c2) => ({
|
|
3127
|
+
id: c2.id,
|
|
3128
|
+
flagged_at: c2.flagged_at,
|
|
3129
|
+
reasoning: c2.reasoning || "No reasoning provided"
|
|
3130
|
+
}))
|
|
3131
|
+
};
|
|
3132
|
+
return header + stringify(doc, { lineWidth: 120 });
|
|
3133
|
+
}
|
|
3134
|
+
function registerMonitorsCommand(fin) {
|
|
3135
|
+
const monitor = fin.command("monitor").description("Manage Fin QA monitors");
|
|
3136
|
+
monitor.command("pull-flagged <monitor>").description(
|
|
3137
|
+
"Pull conversations flagged by a QA monitor into an editable working set file"
|
|
3138
|
+
).option(
|
|
3139
|
+
"--since <period>",
|
|
3140
|
+
"Time window to look back (e.g. 24h, 7d, 2w)",
|
|
3141
|
+
"24h"
|
|
3142
|
+
).option("--limit <n>", "Maximum number of conversations to include", "50").option("--output <path>", "Output file path for the working set YAML").option(
|
|
3143
|
+
"--json",
|
|
3144
|
+
"Output flagged conversations as JSON instead of writing a file"
|
|
3145
|
+
).option("--jq <expr>", "Filter JSON output with jq expression").option("--verbose", "Show HTTP request/response details").addHelpText(
|
|
3146
|
+
"after",
|
|
3147
|
+
`
|
|
3148
|
+
Examples:
|
|
3149
|
+
$ fin monitor pull-flagged "Compliance Violations" --since 7d
|
|
3150
|
+
$ fin monitor pull-flagged 12345 --since 24h --limit 100
|
|
3151
|
+
$ fin monitor pull-flagged "My Monitor" --json`
|
|
3152
|
+
).action(async function(monitorArg) {
|
|
3153
|
+
const opts = this.optsWithGlobals();
|
|
3154
|
+
const client = await resolveClient(opts);
|
|
3155
|
+
const since = opts.since ?? "24h";
|
|
3156
|
+
const limit = parseInt(opts.limit ?? "50", 10);
|
|
3157
|
+
const monitorId = await resolveMonitorId(client, monitorArg);
|
|
3158
|
+
const result = await client.get(
|
|
3159
|
+
`/fin-sdk/qa/monitors/${monitorId}/flagged-conversations`,
|
|
3160
|
+
{ since, limit: String(limit) }
|
|
3161
|
+
);
|
|
3162
|
+
if (opts.json || opts.jq || !process.stdout.isTTY) {
|
|
3163
|
+
render(result, opts, () => {
|
|
3164
|
+
return `${result.conversations.length} conversations flagged by "${result.monitor_name}"`;
|
|
3165
|
+
});
|
|
3166
|
+
return;
|
|
3167
|
+
}
|
|
3168
|
+
if (result.conversations.length === 0) {
|
|
3169
|
+
console.log(
|
|
3170
|
+
`No conversations flagged by "${result.monitor_name}" in the last ${since}.`
|
|
3171
|
+
);
|
|
3172
|
+
return;
|
|
3173
|
+
}
|
|
3174
|
+
const outputPath = opts.output ?? join3(
|
|
3175
|
+
process.cwd(),
|
|
3176
|
+
"src",
|
|
3177
|
+
"qa",
|
|
3178
|
+
"bulk-actions",
|
|
3179
|
+
"flagged-conversations.yaml"
|
|
3180
|
+
);
|
|
3181
|
+
await mkdir2(dirname(outputPath), { recursive: true });
|
|
3182
|
+
await writeFile2(outputPath, buildWorkingSetYaml(result, since), "utf-8");
|
|
3183
|
+
const relPath = relative(process.cwd(), outputPath);
|
|
3184
|
+
console.log(
|
|
3185
|
+
`Wrote ${result.conversations.length} conversations to ${relPath}`
|
|
3186
|
+
);
|
|
3187
|
+
console.log("");
|
|
3188
|
+
console.log("Next steps:");
|
|
3189
|
+
console.log(` 1. Edit ${relPath}`);
|
|
3190
|
+
console.log(" - Remove conversations you don't want to act on");
|
|
3191
|
+
console.log(" - Set action: reply, note, or close");
|
|
3192
|
+
console.log(" - Write your message");
|
|
3193
|
+
console.log(" 2. Preview: fin conversation bulk-send --dry-run");
|
|
3194
|
+
console.log(" 3. Send: fin conversation bulk-send --yes");
|
|
3195
|
+
});
|
|
3196
|
+
}
|
|
3197
|
+
|
|
3198
|
+
// src/commands/fin/conversation.ts
|
|
3199
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
3200
|
+
import { join as join4 } from "path";
|
|
3201
|
+
import { parse } from "yaml";
|
|
3202
|
+
var PLACEHOLDER_MESSAGE = "Your message here \u2014 this will be sent to all conversations listed below.";
|
|
3203
|
+
function validateWorkingSet(ws) {
|
|
3204
|
+
const errors = [];
|
|
3205
|
+
if (!ws.action) {
|
|
3206
|
+
errors.push('Missing "action" field. Must be: reply, note, close, tag, or run_procedure.');
|
|
3207
|
+
} else if (!["reply", "note", "close", "tag", "run_procedure"].includes(ws.action)) {
|
|
3208
|
+
errors.push(
|
|
3209
|
+
`Invalid action "${ws.action}". Must be one of: reply, note, close, tag, run_procedure.`
|
|
3210
|
+
);
|
|
3211
|
+
}
|
|
3212
|
+
if (ws.action === "reply" || ws.action === "note") {
|
|
3213
|
+
if (!ws.message || ws.message.trim().startsWith(PLACEHOLDER_MESSAGE.slice(0, 20))) {
|
|
3214
|
+
errors.push(
|
|
3215
|
+
`Message is required for action "${ws.action}". Edit the message field.`
|
|
3216
|
+
);
|
|
3217
|
+
}
|
|
3218
|
+
}
|
|
3219
|
+
if (ws.action === "run_procedure" && !ws.procedure_id && !ws.procedure_name) {
|
|
3220
|
+
errors.push("procedure_id or procedure_name is required for run_procedure actions.");
|
|
3221
|
+
} else if (ws.action === "run_procedure" && ws.procedure_id && typeof ws.procedure_id !== "number") {
|
|
3222
|
+
errors.push("procedure_id must be a number.");
|
|
3223
|
+
}
|
|
3224
|
+
if (ws.action === "tag" && !ws.tag_id && !ws.tag_name) {
|
|
3225
|
+
errors.push("tag_id or tag_name is required for tag actions.");
|
|
3226
|
+
} else if (ws.action === "tag" && ws.tag_id && typeof ws.tag_id !== "number") {
|
|
3227
|
+
errors.push("tag_id must be a number.");
|
|
3228
|
+
}
|
|
3229
|
+
if (!ws.conversations || ws.conversations.length === 0) {
|
|
3230
|
+
errors.push("conversations list is empty. Add at least one conversation.");
|
|
3231
|
+
} else {
|
|
3232
|
+
const invalid = ws.conversations.filter(
|
|
3233
|
+
(c2) => typeof c2.id !== "number" || isNaN(c2.id)
|
|
3234
|
+
);
|
|
3235
|
+
if (invalid.length > 0) {
|
|
3236
|
+
errors.push(
|
|
3237
|
+
`${invalid.length} conversation(s) have invalid IDs. Each entry must have a numeric id.`
|
|
3238
|
+
);
|
|
3239
|
+
}
|
|
3240
|
+
}
|
|
3241
|
+
return errors;
|
|
3242
|
+
}
|
|
3243
|
+
function registerConversationCommand(fin) {
|
|
3244
|
+
const conversation = fin.command("conversation").description("Manage conversations in bulk");
|
|
3245
|
+
conversation.command("bulk-send").description(
|
|
3246
|
+
"Send bulk actions (reply, note, close, tag, run_procedure) from a working set file"
|
|
3247
|
+
).option(
|
|
3248
|
+
"--file <path>",
|
|
3249
|
+
"Path to the working set YAML file",
|
|
3250
|
+
join4(process.cwd(), "src", "qa", "bulk-actions", "flagged-conversations.yaml")
|
|
3251
|
+
).option("--dry-run", "Preview actions without sending").option("--yes", "Skip confirmation prompt (required in TTY mode)").option(
|
|
3252
|
+
"--json",
|
|
3253
|
+
"Output result as JSON"
|
|
3254
|
+
).option("--jq <expr>", "Filter JSON output with jq expression").option("--verbose", "Show HTTP request/response details").addHelpText(
|
|
3255
|
+
"after",
|
|
3256
|
+
`
|
|
3257
|
+
Examples:
|
|
3258
|
+
$ fin conversation bulk-send --dry-run
|
|
3259
|
+
$ fin conversation bulk-send --yes
|
|
3260
|
+
$ fin conversation bulk-send --file ./my-working-set.yaml --yes
|
|
3261
|
+
$ fin conversation bulk-send --json`
|
|
3262
|
+
).action(async function() {
|
|
3263
|
+
const opts = this.optsWithGlobals();
|
|
3264
|
+
const filePath = opts.file ?? join4(
|
|
3265
|
+
process.cwd(),
|
|
3266
|
+
"src",
|
|
3267
|
+
"qa",
|
|
3268
|
+
"bulk-actions",
|
|
3269
|
+
"flagged-conversations.yaml"
|
|
3270
|
+
);
|
|
3271
|
+
let raw;
|
|
3272
|
+
try {
|
|
3273
|
+
raw = await readFile4(filePath, "utf-8");
|
|
3274
|
+
} catch (err) {
|
|
3275
|
+
const e = err;
|
|
3276
|
+
if (e.code === "ENOENT") {
|
|
3277
|
+
throw new CLIError(
|
|
3278
|
+
`Working set file not found: ${filePath}
|
|
3279
|
+
Run "fin monitor pull-flagged <monitor>" to create one.`
|
|
3280
|
+
);
|
|
3281
|
+
}
|
|
3282
|
+
throw new CLIError(`Failed to read working set file: ${e.message}`);
|
|
3283
|
+
}
|
|
3284
|
+
let ws;
|
|
3285
|
+
try {
|
|
3286
|
+
ws = parse(raw);
|
|
3287
|
+
} catch (err) {
|
|
3288
|
+
const e = err;
|
|
3289
|
+
throw new CLIError(`Invalid YAML in working set file: ${e.message}`);
|
|
3290
|
+
}
|
|
3291
|
+
const errors = validateWorkingSet(ws);
|
|
3292
|
+
if (errors.length > 0) {
|
|
3293
|
+
throw new CLIError(
|
|
3294
|
+
`Working set validation failed:
|
|
3295
|
+
${errors.map((e) => ` - ${e}`).join("\n")}`
|
|
3296
|
+
);
|
|
3297
|
+
}
|
|
3298
|
+
const conversationIds = ws.conversations.map((c2) => c2.id);
|
|
3299
|
+
const count = conversationIds.length;
|
|
3300
|
+
if (opts.dryRun) {
|
|
3301
|
+
const dryResult = {
|
|
3302
|
+
dry_run: true,
|
|
3303
|
+
action_type: ws.action,
|
|
3304
|
+
conversation_count: count,
|
|
3305
|
+
...ws.message ? { message: ws.message } : {},
|
|
3306
|
+
...ws.close_after_reply ? { close_after_reply: true } : {},
|
|
3307
|
+
...ws.procedure_id ? { procedure_id: ws.procedure_id } : {},
|
|
3308
|
+
...ws.procedure_name ? { procedure_name: ws.procedure_name } : {},
|
|
3309
|
+
...ws.tag_id ? { tag_id: ws.tag_id } : {},
|
|
3310
|
+
...ws.tag_name ? { tag_name: ws.tag_name } : {}
|
|
3311
|
+
};
|
|
3312
|
+
render(dryResult, opts, () => {
|
|
3313
|
+
const lines = [
|
|
3314
|
+
`Dry run \u2014 no changes will be made.`,
|
|
3315
|
+
`Action: ${ws.action}`,
|
|
3316
|
+
`Conversations: ${count}`
|
|
3317
|
+
];
|
|
3318
|
+
if (ws.message) lines.push(`Message: ${ws.message.trim()}`);
|
|
3319
|
+
if (ws.close_after_reply) lines.push(`Close after: yes`);
|
|
3320
|
+
if (ws.procedure_id) lines.push(`Procedure ID: ${ws.procedure_id}`);
|
|
3321
|
+
if (ws.procedure_name) lines.push(`Procedure: ${ws.procedure_name}`);
|
|
3322
|
+
if (ws.tag_id) lines.push(`Tag ID: ${ws.tag_id}`);
|
|
3323
|
+
if (ws.tag_name) lines.push(`Tag: ${ws.tag_name}`);
|
|
3324
|
+
return lines.join("\n");
|
|
3325
|
+
});
|
|
3326
|
+
return;
|
|
3327
|
+
}
|
|
3328
|
+
if (process.stdout.isTTY && !opts.yes) {
|
|
3329
|
+
console.log(
|
|
3330
|
+
`About to send "${ws.action}" to ${count} conversation(s).`
|
|
3331
|
+
);
|
|
3332
|
+
if (ws.message) {
|
|
3333
|
+
console.log(`Message: ${ws.message.trim()}`);
|
|
3334
|
+
}
|
|
3335
|
+
throw new CLIError(
|
|
3336
|
+
`Add --yes to confirm, or use --dry-run to preview.`
|
|
3337
|
+
);
|
|
3338
|
+
}
|
|
3339
|
+
const client = await resolveClient(opts);
|
|
3340
|
+
const body = {
|
|
3341
|
+
conversation_ids: conversationIds,
|
|
3342
|
+
action_type: ws.action
|
|
3343
|
+
};
|
|
3344
|
+
if (ws.message) body.message = ws.message;
|
|
3345
|
+
if (ws.close_after_reply != null) body.close_after_reply = ws.close_after_reply;
|
|
3346
|
+
if (ws.procedure_id != null) body.procedure_id = ws.procedure_id;
|
|
3347
|
+
if (ws.procedure_name != null) body.procedure_name = ws.procedure_name;
|
|
3348
|
+
if (ws.tag_id != null) body.tag_id = ws.tag_id;
|
|
3349
|
+
if (ws.tag_name != null) body.tag_name = ws.tag_name;
|
|
3350
|
+
const result = await client.post(
|
|
3351
|
+
"/fin-sdk/bulk-actions",
|
|
3352
|
+
body
|
|
3353
|
+
);
|
|
3354
|
+
render(result, opts, () => {
|
|
3355
|
+
return `Sent "${result.action_type}" to ${result.conversation_count} conversation(s). Status: ${result.status}`;
|
|
3356
|
+
});
|
|
3357
|
+
});
|
|
3358
|
+
}
|
|
3359
|
+
|
|
2891
3360
|
// src/commands/fin.ts
|
|
2892
3361
|
function renderResourceTable(label, items) {
|
|
2893
3362
|
if (items.length === 0) {
|
|
@@ -2948,19 +3417,21 @@ Examples:
|
|
|
2948
3417
|
registerAudiencesCommand(fin);
|
|
2949
3418
|
registerWorkflowsCommand(fin);
|
|
2950
3419
|
registerSimulationsCommand(fin);
|
|
3420
|
+
registerMonitorsCommand(fin);
|
|
3421
|
+
registerConversationCommand(fin);
|
|
2951
3422
|
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
3423
|
const opts = this.optsWithGlobals();
|
|
2953
3424
|
const outputPath = opts.output ?? "./fin-resources.zip";
|
|
2954
3425
|
const client = await resolveClient(opts);
|
|
2955
3426
|
const buffer = await client.getBuffer("/fin-sdk/download");
|
|
2956
|
-
await
|
|
3427
|
+
await writeFile3(outputPath, buffer);
|
|
2957
3428
|
console.log(`Downloaded ${buffer.length} bytes to ${outputPath}`);
|
|
2958
3429
|
});
|
|
2959
3430
|
}
|
|
2960
3431
|
|
|
2961
3432
|
// src/commands/setup.ts
|
|
2962
3433
|
import { readdirSync, readFileSync as readFileSync4, statSync } from "fs";
|
|
2963
|
-
import { join as
|
|
3434
|
+
import { join as join5, basename as basename3 } from "path";
|
|
2964
3435
|
function isGreenfield(opts) {
|
|
2965
3436
|
return !!(opts.email && opts.name && (process.env.INTERCOM_PASSWORD || opts.passwordStdin));
|
|
2966
3437
|
}
|
|
@@ -3018,8 +3489,8 @@ function buildPlan(opts) {
|
|
|
3018
3489
|
}
|
|
3019
3490
|
if (mdFiles.length > 0) {
|
|
3020
3491
|
const articleCommands = mdFiles.map((file) => {
|
|
3021
|
-
const title =
|
|
3022
|
-
return `intercom articles create -f title="${title}" -F body=@${
|
|
3492
|
+
const title = basename3(file, ".md").replace(/-/g, " ").replace(/\b\w/g, (c2) => c2.toUpperCase());
|
|
3493
|
+
return `intercom articles create -f title="${title}" -F body=@${join5(opts.articlesFrom, file)} -f author_id=$(intercom me --json | jq -r .id)`;
|
|
3023
3494
|
});
|
|
3024
3495
|
steps.push({
|
|
3025
3496
|
step: stepNum++,
|
|
@@ -3085,8 +3556,8 @@ async function runPostAuthSteps(client, opts, results, appId) {
|
|
|
3085
3556
|
let created = 0;
|
|
3086
3557
|
let skipped = 0;
|
|
3087
3558
|
for (const file of mdFiles) {
|
|
3088
|
-
const title =
|
|
3089
|
-
const body = readFileSync4(
|
|
3559
|
+
const title = basename3(file, ".md").replace(/-/g, " ").replace(/\b\w/g, (c2) => c2.toUpperCase());
|
|
3560
|
+
const body = readFileSync4(join5(opts.articlesFrom, file), "utf-8");
|
|
3090
3561
|
if (existingTitles.has(title)) {
|
|
3091
3562
|
skipped++;
|
|
3092
3563
|
} else {
|
|
@@ -3554,7 +4025,7 @@ function registerPricingCommand(program) {
|
|
|
3554
4025
|
}
|
|
3555
4026
|
const markdown = await response.text();
|
|
3556
4027
|
if (opts.json) {
|
|
3557
|
-
const { formatJson: formatJson2 } = await import("./json-
|
|
4028
|
+
const { formatJson: formatJson2 } = await import("./json-OEUXXCTX.js");
|
|
3558
4029
|
console.log(formatJson2({ url: PRICING_URL, content: markdown }));
|
|
3559
4030
|
} else {
|
|
3560
4031
|
console.log(markdown);
|
|
@@ -3563,7 +4034,7 @@ function registerPricingCommand(program) {
|
|
|
3563
4034
|
}
|
|
3564
4035
|
|
|
3565
4036
|
// src/commands/config.ts
|
|
3566
|
-
import { join as
|
|
4037
|
+
import { join as join6 } from "path";
|
|
3567
4038
|
var SUPPORTED_KEYS = ["default_workspace", "timeout"];
|
|
3568
4039
|
function configDir() {
|
|
3569
4040
|
return process.env.INTERCOM_CONFIG_DIR ?? defaultConfigDir();
|
|
@@ -3652,7 +4123,7 @@ function registerConfigCommand(program) {
|
|
|
3652
4123
|
console.log(`Set ${key} = ${value}`);
|
|
3653
4124
|
});
|
|
3654
4125
|
cmd.command("path").description("Show configuration file path").action(async function() {
|
|
3655
|
-
console.log(
|
|
4126
|
+
console.log(join6(configDir(), "config.json"));
|
|
3656
4127
|
});
|
|
3657
4128
|
}
|
|
3658
4129
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intercom/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.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",
|
|
@@ -37,6 +38,6 @@
|
|
|
37
38
|
"msw": "^2.12.13",
|
|
38
39
|
"tsup": "^8.5.1",
|
|
39
40
|
"typescript": "^5.9.3",
|
|
40
|
-
"vitest": "^
|
|
41
|
+
"vitest": "^4.1.0"
|
|
41
42
|
}
|
|
42
43
|
}
|