@intercom/cli 0.1.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 +2403 -0
- package/dist/index.js.map +1 -0
- package/package.json +38 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2403 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/types.ts
|
|
7
|
+
var CLIError = class extends Error {
|
|
8
|
+
constructor(message, exitCode = 1, error) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.exitCode = exitCode;
|
|
11
|
+
this.error = error;
|
|
12
|
+
this.name = "CLIError";
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
var AuthError = class extends CLIError {
|
|
16
|
+
constructor(message) {
|
|
17
|
+
super(message, 2);
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
var RateLimitError = class extends CLIError {
|
|
21
|
+
constructor(message, retryAfter) {
|
|
22
|
+
super(message, 3);
|
|
23
|
+
this.retryAfter = retryAfter;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
var NotFoundError = class extends CLIError {
|
|
27
|
+
constructor(message) {
|
|
28
|
+
super(message, 4);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
var PermissionError = class extends CLIError {
|
|
32
|
+
constructor(message) {
|
|
33
|
+
super(message, 5);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
var TimeoutError = class extends CLIError {
|
|
37
|
+
constructor(message) {
|
|
38
|
+
super(message, 6);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
function detectAuthType(token) {
|
|
42
|
+
try {
|
|
43
|
+
const decoded = Buffer.from(token, "base64").toString("utf-8");
|
|
44
|
+
if (decoded.startsWith("tok:")) {
|
|
45
|
+
return "oauth";
|
|
46
|
+
}
|
|
47
|
+
} catch {
|
|
48
|
+
}
|
|
49
|
+
return "api_key";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// src/output/json.ts
|
|
53
|
+
function formatJson(data) {
|
|
54
|
+
return JSON.stringify(data, null, 2);
|
|
55
|
+
}
|
|
56
|
+
function applyJq(data, expr) {
|
|
57
|
+
if (expr === ".") return data;
|
|
58
|
+
if (!expr.startsWith(".")) {
|
|
59
|
+
throw new CLIError(
|
|
60
|
+
`Unsupported jq expression: "${expr}". Supported: ".", ".field", ".field.nested", ".field[].nested"`
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
if (/[|(){},;]/.test(expr)) {
|
|
64
|
+
throw new CLIError(
|
|
65
|
+
`Unsupported jq expression: "${expr}". Supported: ".", ".field", ".field.nested", ".field[].nested"`
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
const parts = expr.replace(/^\.\s*/, "").split(/\./).filter(Boolean);
|
|
69
|
+
for (const part of parts) {
|
|
70
|
+
if (!/^\w+(\[\])?$/.test(part)) {
|
|
71
|
+
throw new CLIError(
|
|
72
|
+
`Unsupported jq expression: "${expr}". Supported: ".", ".field", ".field.nested", ".field[].nested"`
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
let current = data;
|
|
77
|
+
for (const part of parts) {
|
|
78
|
+
const arrayMatch = part.match(/^(\w+)\[\]$/);
|
|
79
|
+
if (arrayMatch) {
|
|
80
|
+
const key = arrayMatch[1];
|
|
81
|
+
if (current && typeof current === "object" && key in current) {
|
|
82
|
+
const arr = current[key];
|
|
83
|
+
if (!Array.isArray(arr)) return void 0;
|
|
84
|
+
current = arr;
|
|
85
|
+
} else {
|
|
86
|
+
return void 0;
|
|
87
|
+
}
|
|
88
|
+
} else if (Array.isArray(current)) {
|
|
89
|
+
current = current.map(
|
|
90
|
+
(item) => item && typeof item === "object" ? item[part] : void 0
|
|
91
|
+
);
|
|
92
|
+
} else if (current && typeof current === "object") {
|
|
93
|
+
current = current[part];
|
|
94
|
+
} else {
|
|
95
|
+
return void 0;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return current;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// src/commands/api.ts
|
|
102
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
103
|
+
|
|
104
|
+
// src/api/timeouts.ts
|
|
105
|
+
var API_TIMEOUT_MS = 3e4;
|
|
106
|
+
|
|
107
|
+
// src/api/fetch.ts
|
|
108
|
+
var USER_AGENT = "IntercomCLI/0.1.0";
|
|
109
|
+
async function fetchWithTimeout(url, init, options) {
|
|
110
|
+
const headers = new Headers(init.headers);
|
|
111
|
+
if (!headers.has("User-Agent")) {
|
|
112
|
+
headers.set("User-Agent", USER_AGENT);
|
|
113
|
+
}
|
|
114
|
+
if (options.verbose) {
|
|
115
|
+
console.error(`> ${init.method ?? "GET"} ${url}`);
|
|
116
|
+
}
|
|
117
|
+
let response;
|
|
118
|
+
try {
|
|
119
|
+
response = await fetch(url, { ...init, headers, signal: AbortSignal.timeout(options.timeoutMs) });
|
|
120
|
+
} catch (err) {
|
|
121
|
+
if (err instanceof DOMException && err.name === "TimeoutError") {
|
|
122
|
+
throw new TimeoutError(`Request timed out after ${options.timeoutMs / 1e3}s: ${init.method ?? "GET"} ${url}`);
|
|
123
|
+
}
|
|
124
|
+
throw err;
|
|
125
|
+
}
|
|
126
|
+
if (options.verbose) {
|
|
127
|
+
console.error(`< ${response.status} ${response.statusText}`);
|
|
128
|
+
}
|
|
129
|
+
return response;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// src/api/client.ts
|
|
133
|
+
var DEFAULT_BASE_URL = "https://api.intercom.io";
|
|
134
|
+
var IntercomClient = class {
|
|
135
|
+
token;
|
|
136
|
+
authType;
|
|
137
|
+
appId;
|
|
138
|
+
baseUrl;
|
|
139
|
+
maxRetries;
|
|
140
|
+
verbose;
|
|
141
|
+
timeoutMs;
|
|
142
|
+
constructor(options) {
|
|
143
|
+
this.token = options.token;
|
|
144
|
+
this.authType = options.authType;
|
|
145
|
+
this.appId = options.appId ?? "";
|
|
146
|
+
this.baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
|
|
147
|
+
this.maxRetries = options.maxRetries ?? 3;
|
|
148
|
+
this.verbose = options.verbose ?? false;
|
|
149
|
+
this.timeoutMs = options.timeoutMs ?? API_TIMEOUT_MS;
|
|
150
|
+
}
|
|
151
|
+
authHeader() {
|
|
152
|
+
if (this.authType === "api_key") {
|
|
153
|
+
return `Basic ${Buffer.from(`${this.appId}:${this.token}`).toString("base64")}`;
|
|
154
|
+
}
|
|
155
|
+
return `Bearer ${this.token}`;
|
|
156
|
+
}
|
|
157
|
+
async request(method, path, body, responseType = "json", extraHeaders) {
|
|
158
|
+
const url = `${this.baseUrl}${path}`;
|
|
159
|
+
const headers = {
|
|
160
|
+
Authorization: this.authHeader()
|
|
161
|
+
};
|
|
162
|
+
const isFormData = body instanceof FormData;
|
|
163
|
+
if (responseType === "json") {
|
|
164
|
+
headers.Accept = "application/json";
|
|
165
|
+
}
|
|
166
|
+
if (!isFormData) {
|
|
167
|
+
headers["Content-Type"] = "application/json";
|
|
168
|
+
}
|
|
169
|
+
if (extraHeaders) {
|
|
170
|
+
Object.assign(headers, extraHeaders);
|
|
171
|
+
}
|
|
172
|
+
const maxAttempts = this.maxRetries + 1;
|
|
173
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
174
|
+
const options = { method, headers };
|
|
175
|
+
if (body && method !== "GET") {
|
|
176
|
+
if (isFormData) {
|
|
177
|
+
options.body = body;
|
|
178
|
+
} else {
|
|
179
|
+
options.body = JSON.stringify(body);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (this.verbose) {
|
|
183
|
+
console.error(`> ${method} ${url}`);
|
|
184
|
+
if (body) {
|
|
185
|
+
console.error(
|
|
186
|
+
`> Body: ${body instanceof FormData ? "[FormData]" : JSON.stringify(body)}`
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
const response = await fetchWithTimeout(url, options, {
|
|
191
|
+
timeoutMs: this.timeoutMs
|
|
192
|
+
});
|
|
193
|
+
if (this.verbose) {
|
|
194
|
+
console.error(`< ${response.status} ${response.statusText}`);
|
|
195
|
+
}
|
|
196
|
+
if (response.ok) {
|
|
197
|
+
if (response.status === 204) return {};
|
|
198
|
+
switch (responseType) {
|
|
199
|
+
case "text":
|
|
200
|
+
return response.text();
|
|
201
|
+
case "buffer":
|
|
202
|
+
return Buffer.from(await response.arrayBuffer());
|
|
203
|
+
default:
|
|
204
|
+
return response.json();
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
const errorBody = await response.json().catch(() => ({}));
|
|
208
|
+
const errorMsg = errorBody?.errors?.[0]?.message ?? `HTTP ${response.status}`;
|
|
209
|
+
if (response.status === 401) {
|
|
210
|
+
throw new AuthError(
|
|
211
|
+
`Authentication expired or invalid: ${errorMsg}. Run "intercom auth login" to re-authenticate.`
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
if (response.status === 403) {
|
|
215
|
+
throw new PermissionError(`Permission denied: ${errorMsg}`);
|
|
216
|
+
}
|
|
217
|
+
if (response.status === 404) {
|
|
218
|
+
throw new NotFoundError(errorMsg);
|
|
219
|
+
}
|
|
220
|
+
if (response.status === 429) {
|
|
221
|
+
const retryAfter = parseInt(
|
|
222
|
+
response.headers.get("Retry-After") ?? "5",
|
|
223
|
+
10
|
|
224
|
+
);
|
|
225
|
+
if (attempt < this.maxRetries) {
|
|
226
|
+
if (this.verbose) {
|
|
227
|
+
console.error(
|
|
228
|
+
`Rate limited. Retrying in ${retryAfter}s (attempt ${attempt + 1}/${this.maxRetries})...`
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
await new Promise((r) => setTimeout(r, retryAfter * 1e3));
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
throw new RateLimitError(
|
|
235
|
+
`Rate limit exceeded. Retry after ${retryAfter} seconds.`,
|
|
236
|
+
retryAfter
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
throw new CLIError(errorMsg);
|
|
240
|
+
}
|
|
241
|
+
throw new CLIError("Request failed after retries");
|
|
242
|
+
}
|
|
243
|
+
async get(path, params, headers) {
|
|
244
|
+
let url = path;
|
|
245
|
+
if (params && Object.keys(params).length > 0) {
|
|
246
|
+
const qs = new URLSearchParams(params).toString();
|
|
247
|
+
url = `${path}?${qs}`;
|
|
248
|
+
}
|
|
249
|
+
return this.request("GET", url, void 0, "json", headers);
|
|
250
|
+
}
|
|
251
|
+
async post(path, body, headers) {
|
|
252
|
+
return this.request("POST", path, body, "json", headers);
|
|
253
|
+
}
|
|
254
|
+
async put(path, body, headers) {
|
|
255
|
+
return this.request("PUT", path, body, "json", headers);
|
|
256
|
+
}
|
|
257
|
+
async patch(path, body, headers) {
|
|
258
|
+
return this.request("PATCH", path, body, "json", headers);
|
|
259
|
+
}
|
|
260
|
+
async delete(path, headers) {
|
|
261
|
+
return this.request("DELETE", path, void 0, "json", headers);
|
|
262
|
+
}
|
|
263
|
+
async getText(path, params) {
|
|
264
|
+
let url = path;
|
|
265
|
+
if (params && Object.keys(params).length > 0) {
|
|
266
|
+
const qs = new URLSearchParams(params).toString();
|
|
267
|
+
url = `${path}?${qs}`;
|
|
268
|
+
}
|
|
269
|
+
return this.request("GET", url, void 0, "text");
|
|
270
|
+
}
|
|
271
|
+
async getBuffer(path, params) {
|
|
272
|
+
let url = path;
|
|
273
|
+
if (params && Object.keys(params).length > 0) {
|
|
274
|
+
const qs = new URLSearchParams(params).toString();
|
|
275
|
+
url = `${path}?${qs}`;
|
|
276
|
+
}
|
|
277
|
+
return this.request("GET", url, void 0, "buffer");
|
|
278
|
+
}
|
|
279
|
+
async postFormData(path, formData) {
|
|
280
|
+
return this.request("POST", path, formData);
|
|
281
|
+
}
|
|
282
|
+
async paginate(path, params, dataKey = "data") {
|
|
283
|
+
const allData = [];
|
|
284
|
+
const firstResponse = await this.get(path, { ...params });
|
|
285
|
+
const firstData = firstResponse[dataKey];
|
|
286
|
+
if (firstData) allData.push(...firstData);
|
|
287
|
+
const pages = firstResponse.pages;
|
|
288
|
+
const startingAfter = pages?.next && typeof pages.next === "object" ? pages.next.starting_after : void 0;
|
|
289
|
+
if (startingAfter) {
|
|
290
|
+
let cursor = startingAfter;
|
|
291
|
+
while (cursor) {
|
|
292
|
+
const response = await this.get(path, {
|
|
293
|
+
...params,
|
|
294
|
+
starting_after: cursor
|
|
295
|
+
});
|
|
296
|
+
const data = response[dataKey];
|
|
297
|
+
if (data) allData.push(...data);
|
|
298
|
+
const p = response.pages;
|
|
299
|
+
cursor = p?.next && typeof p.next === "object" ? p.next.starting_after : void 0;
|
|
300
|
+
}
|
|
301
|
+
return allData;
|
|
302
|
+
}
|
|
303
|
+
const totalPages = pages?.total_pages ?? 1;
|
|
304
|
+
for (let page = 2; page <= totalPages; page++) {
|
|
305
|
+
const response = await this.get(path, { ...params, page: String(page) });
|
|
306
|
+
const data = response[dataKey];
|
|
307
|
+
if (data) allData.push(...data);
|
|
308
|
+
}
|
|
309
|
+
return allData;
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
// src/auth/store.ts
|
|
314
|
+
import { readFileSync, mkdirSync } from "fs";
|
|
315
|
+
|
|
316
|
+
// src/utils/fs.ts
|
|
317
|
+
import { writeFile, rename } from "fs/promises";
|
|
318
|
+
import { writeFileSync, renameSync } from "fs";
|
|
319
|
+
async function atomicWriteFile(filePath, data, mode = 384) {
|
|
320
|
+
const tmpPath = filePath + ".tmp";
|
|
321
|
+
await writeFile(tmpPath, data, { mode });
|
|
322
|
+
await rename(tmpPath, filePath);
|
|
323
|
+
}
|
|
324
|
+
function atomicWriteFileSync(filePath, data, mode = 384) {
|
|
325
|
+
const tmpPath = filePath + ".tmp";
|
|
326
|
+
writeFileSync(tmpPath, data, { mode });
|
|
327
|
+
renameSync(tmpPath, filePath);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// src/auth/store.ts
|
|
331
|
+
import { join } from "path";
|
|
332
|
+
import { homedir } from "os";
|
|
333
|
+
import { randomBytes, createCipheriv, createDecipheriv } from "crypto";
|
|
334
|
+
var SERVICE_NAME = "intercom-cli";
|
|
335
|
+
var NativeKeychainBackend = class {
|
|
336
|
+
async get(service, account) {
|
|
337
|
+
const { Entry } = await import("@napi-rs/keyring");
|
|
338
|
+
const entry = new Entry(service, account);
|
|
339
|
+
return entry.getPassword() ?? void 0;
|
|
340
|
+
}
|
|
341
|
+
async set(service, account, value) {
|
|
342
|
+
const { Entry } = await import("@napi-rs/keyring");
|
|
343
|
+
const entry = new Entry(service, account);
|
|
344
|
+
entry.setPassword(value);
|
|
345
|
+
}
|
|
346
|
+
async delete(service, account) {
|
|
347
|
+
const { Entry } = await import("@napi-rs/keyring");
|
|
348
|
+
const entry = new Entry(service, account);
|
|
349
|
+
try {
|
|
350
|
+
entry.deletePassword();
|
|
351
|
+
} catch (err) {
|
|
352
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
353
|
+
if (/not found|no matching/i.test(msg)) return;
|
|
354
|
+
throw err;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
var CREDENTIALS_FILE = "credentials";
|
|
359
|
+
var KEY_FILE = ".key";
|
|
360
|
+
var ALGORITHM = "aes-256-gcm";
|
|
361
|
+
var EncryptedFileBackend = class {
|
|
362
|
+
dir;
|
|
363
|
+
constructor(dir) {
|
|
364
|
+
this.dir = dir ?? join(homedir(), ".intercom");
|
|
365
|
+
}
|
|
366
|
+
getEncryptionKey() {
|
|
367
|
+
const keyPath = join(this.dir, KEY_FILE);
|
|
368
|
+
try {
|
|
369
|
+
return Buffer.from(readFileSync(keyPath, "utf-8").trim(), "hex");
|
|
370
|
+
} catch (err) {
|
|
371
|
+
if (err.code !== "ENOENT") throw err;
|
|
372
|
+
mkdirSync(this.dir, { recursive: true });
|
|
373
|
+
const key = randomBytes(32);
|
|
374
|
+
atomicWriteFileSync(keyPath, key.toString("hex") + "\n");
|
|
375
|
+
return key;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
readStore() {
|
|
379
|
+
const filePath = join(this.dir, CREDENTIALS_FILE);
|
|
380
|
+
let raw;
|
|
381
|
+
try {
|
|
382
|
+
raw = readFileSync(filePath, "utf-8");
|
|
383
|
+
} catch (err) {
|
|
384
|
+
if (err.code !== "ENOENT") throw err;
|
|
385
|
+
return {};
|
|
386
|
+
}
|
|
387
|
+
const { iv, tag, data } = JSON.parse(raw);
|
|
388
|
+
const key = this.getEncryptionKey();
|
|
389
|
+
const decipher = createDecipheriv(ALGORITHM, key, Buffer.from(iv, "hex"));
|
|
390
|
+
decipher.setAuthTag(Buffer.from(tag, "hex"));
|
|
391
|
+
let decrypted = decipher.update(data, "hex", "utf-8");
|
|
392
|
+
decrypted += decipher.final("utf-8");
|
|
393
|
+
return JSON.parse(decrypted);
|
|
394
|
+
}
|
|
395
|
+
writeStore(store) {
|
|
396
|
+
mkdirSync(this.dir, { recursive: true });
|
|
397
|
+
const key = this.getEncryptionKey();
|
|
398
|
+
const iv = randomBytes(16);
|
|
399
|
+
const cipher = createCipheriv(ALGORITHM, key, iv);
|
|
400
|
+
let encrypted = cipher.update(JSON.stringify(store), "utf-8", "hex");
|
|
401
|
+
encrypted += cipher.final("hex");
|
|
402
|
+
const tag = cipher.getAuthTag();
|
|
403
|
+
const filePath = join(this.dir, CREDENTIALS_FILE);
|
|
404
|
+
atomicWriteFileSync(
|
|
405
|
+
filePath,
|
|
406
|
+
JSON.stringify({ iv: iv.toString("hex"), tag: tag.toString("hex"), data: encrypted }) + "\n"
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
storeKey(service, account) {
|
|
410
|
+
return `${service}:${account}`;
|
|
411
|
+
}
|
|
412
|
+
async get(service, account) {
|
|
413
|
+
const store = this.readStore();
|
|
414
|
+
return store[this.storeKey(service, account)];
|
|
415
|
+
}
|
|
416
|
+
async set(service, account, value) {
|
|
417
|
+
const store = this.readStore();
|
|
418
|
+
store[this.storeKey(service, account)] = value;
|
|
419
|
+
this.writeStore(store);
|
|
420
|
+
}
|
|
421
|
+
async delete(service, account) {
|
|
422
|
+
const store = this.readStore();
|
|
423
|
+
delete store[this.storeKey(service, account)];
|
|
424
|
+
this.writeStore(store);
|
|
425
|
+
}
|
|
426
|
+
};
|
|
427
|
+
var FallbackBackend = class {
|
|
428
|
+
constructor(primary, secondary) {
|
|
429
|
+
this.primary = primary;
|
|
430
|
+
this.secondary = secondary;
|
|
431
|
+
this.active = primary;
|
|
432
|
+
}
|
|
433
|
+
active;
|
|
434
|
+
fallen = false;
|
|
435
|
+
fallback(err) {
|
|
436
|
+
if (this.fallen) throw err;
|
|
437
|
+
this.fallen = true;
|
|
438
|
+
this.active = this.secondary;
|
|
439
|
+
console.error(
|
|
440
|
+
`Warning: System keyring unavailable (${err instanceof Error ? err.message : String(err)}). Falling back to encrypted file storage.`
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
async get(service, account) {
|
|
444
|
+
try {
|
|
445
|
+
return await this.active.get(service, account);
|
|
446
|
+
} catch (err) {
|
|
447
|
+
this.fallback(err);
|
|
448
|
+
return this.active.get(service, account);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
async set(service, account, value) {
|
|
452
|
+
try {
|
|
453
|
+
return await this.active.set(service, account, value);
|
|
454
|
+
} catch (err) {
|
|
455
|
+
this.fallback(err);
|
|
456
|
+
return this.active.set(service, account, value);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
async delete(service, account) {
|
|
460
|
+
try {
|
|
461
|
+
return await this.active.delete(service, account);
|
|
462
|
+
} catch (err) {
|
|
463
|
+
this.fallback(err);
|
|
464
|
+
return this.active.delete(service, account);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
};
|
|
468
|
+
function createDefaultBackend(configDir) {
|
|
469
|
+
if (configDir) {
|
|
470
|
+
return new EncryptedFileBackend(configDir);
|
|
471
|
+
}
|
|
472
|
+
return new FallbackBackend(
|
|
473
|
+
new NativeKeychainBackend(),
|
|
474
|
+
new EncryptedFileBackend()
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
var CredentialStore = class {
|
|
478
|
+
constructor(backend) {
|
|
479
|
+
this.backend = backend;
|
|
480
|
+
}
|
|
481
|
+
async get(appId) {
|
|
482
|
+
const raw = await this.backend.get(SERVICE_NAME, appId);
|
|
483
|
+
if (!raw) return void 0;
|
|
484
|
+
return JSON.parse(raw);
|
|
485
|
+
}
|
|
486
|
+
async set(appId, credential) {
|
|
487
|
+
await this.backend.set(SERVICE_NAME, appId, JSON.stringify(credential));
|
|
488
|
+
}
|
|
489
|
+
async delete(appId) {
|
|
490
|
+
await this.backend.delete(SERVICE_NAME, appId);
|
|
491
|
+
}
|
|
492
|
+
resolveFromEnv(envToken) {
|
|
493
|
+
return {
|
|
494
|
+
token: envToken,
|
|
495
|
+
auth_type: detectAuthType(envToken),
|
|
496
|
+
app_id: "env"
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
// src/auth/config.ts
|
|
502
|
+
import { readFile, mkdir } from "fs/promises";
|
|
503
|
+
import { join as join2 } from "path";
|
|
504
|
+
import { homedir as homedir2 } from "os";
|
|
505
|
+
var CONFIG_FILE = "config.json";
|
|
506
|
+
function defaultConfigDir() {
|
|
507
|
+
return join2(homedir2(), ".intercom");
|
|
508
|
+
}
|
|
509
|
+
async function readConfig(configDir) {
|
|
510
|
+
const dir = configDir ?? defaultConfigDir();
|
|
511
|
+
try {
|
|
512
|
+
const raw = await readFile(join2(dir, CONFIG_FILE), "utf-8");
|
|
513
|
+
return JSON.parse(raw);
|
|
514
|
+
} catch {
|
|
515
|
+
return { workspaces: {} };
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
async function writeConfig(config, configDir) {
|
|
519
|
+
const dir = configDir ?? defaultConfigDir();
|
|
520
|
+
await mkdir(dir, { recursive: true });
|
|
521
|
+
await atomicWriteFile(join2(dir, CONFIG_FILE), JSON.stringify(config, null, 2) + "\n");
|
|
522
|
+
}
|
|
523
|
+
async function addWorkspace(configDir, workspace) {
|
|
524
|
+
const config = await readConfig(configDir);
|
|
525
|
+
config.workspaces[workspace.app_id] = workspace;
|
|
526
|
+
if (!config.default) {
|
|
527
|
+
config.default = workspace.app_id;
|
|
528
|
+
}
|
|
529
|
+
await writeConfig(config, configDir);
|
|
530
|
+
}
|
|
531
|
+
async function removeWorkspace(configDir, appId) {
|
|
532
|
+
const config = await readConfig(configDir);
|
|
533
|
+
delete config.workspaces[appId];
|
|
534
|
+
if (config.default === appId) {
|
|
535
|
+
const remaining = Object.keys(config.workspaces);
|
|
536
|
+
config.default = remaining.length > 0 ? remaining[0] : void 0;
|
|
537
|
+
}
|
|
538
|
+
await writeConfig(config, configDir);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// src/commands/helpers.ts
|
|
542
|
+
async function resolveClient(options = {}) {
|
|
543
|
+
const envToken = process.env.INTERCOM_TOKEN;
|
|
544
|
+
if (envToken) {
|
|
545
|
+
return new IntercomClient({
|
|
546
|
+
token: envToken,
|
|
547
|
+
authType: detectAuthType(envToken),
|
|
548
|
+
appId: process.env.INTERCOM_APP_ID ?? "",
|
|
549
|
+
baseUrl: process.env.INTERCOM_API_BASE_URL || void 0,
|
|
550
|
+
verbose: options.verbose,
|
|
551
|
+
maxRetries: options.retry === false ? 0 : void 0,
|
|
552
|
+
timeoutMs: options.timeout ? parseInt(options.timeout, 10) * 1e3 : void 0
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
const configDir = process.env.INTERCOM_CONFIG_DIR;
|
|
556
|
+
const config = await readConfig(configDir);
|
|
557
|
+
const appId = options.workspace ?? config.default;
|
|
558
|
+
if (!appId) {
|
|
559
|
+
throw new AuthError(
|
|
560
|
+
'Not authenticated. Run "intercom auth login" to get started.'
|
|
561
|
+
);
|
|
562
|
+
}
|
|
563
|
+
const store = new CredentialStore(createDefaultBackend(configDir));
|
|
564
|
+
const cred = await store.get(appId);
|
|
565
|
+
if (!cred) {
|
|
566
|
+
throw new AuthError(
|
|
567
|
+
`No credentials found for workspace ${appId}. Run "intercom auth login".`
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
return new IntercomClient({
|
|
571
|
+
token: cred.token,
|
|
572
|
+
authType: cred.auth_type,
|
|
573
|
+
appId: cred.app_id,
|
|
574
|
+
baseUrl: process.env.INTERCOM_API_BASE_URL || void 0,
|
|
575
|
+
verbose: options.verbose,
|
|
576
|
+
maxRetries: options.retry === false ? 0 : void 0,
|
|
577
|
+
timeoutMs: options.timeout ? parseInt(options.timeout, 10) * 1e3 : void 0
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
function parseFieldArgs(fields) {
|
|
581
|
+
return Object.fromEntries(
|
|
582
|
+
fields.map((f) => {
|
|
583
|
+
const eq = f.indexOf("=");
|
|
584
|
+
if (eq === -1) {
|
|
585
|
+
throw new CLIError(
|
|
586
|
+
`Invalid field argument "${f}": expected format key=value`
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
return [f.slice(0, eq), f.slice(eq + 1)];
|
|
590
|
+
})
|
|
591
|
+
);
|
|
592
|
+
}
|
|
593
|
+
function formatTimestamp(ts) {
|
|
594
|
+
if (!ts) return "";
|
|
595
|
+
const n = typeof ts === "number" ? ts : Number(ts);
|
|
596
|
+
if (isNaN(n)) return String(ts);
|
|
597
|
+
return new Date(n * 1e3).toISOString().slice(0, 10);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// src/output/render.ts
|
|
601
|
+
function render(data, opts, humanRenderer, listItems) {
|
|
602
|
+
if (opts.jq) {
|
|
603
|
+
const filtered = applyJq(data, opts.jq);
|
|
604
|
+
if (Array.isArray(filtered)) {
|
|
605
|
+
for (const item of filtered) {
|
|
606
|
+
console.log(JSON.stringify(item));
|
|
607
|
+
}
|
|
608
|
+
} else {
|
|
609
|
+
console.log(JSON.stringify(filtered));
|
|
610
|
+
}
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
if (opts.json) {
|
|
614
|
+
console.log(formatJson(data));
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
if (!process.stdout.isTTY) {
|
|
618
|
+
if (listItems) {
|
|
619
|
+
for (const item of listItems) {
|
|
620
|
+
console.log(JSON.stringify(item));
|
|
621
|
+
}
|
|
622
|
+
} else {
|
|
623
|
+
console.log(JSON.stringify(data));
|
|
624
|
+
}
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
console.log(humanRenderer());
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// src/commands/api.ts
|
|
631
|
+
function parseFields(fields, typed) {
|
|
632
|
+
const result = {};
|
|
633
|
+
for (const field of fields) {
|
|
634
|
+
const eqIndex = field.indexOf("=");
|
|
635
|
+
if (eqIndex === -1) {
|
|
636
|
+
throw new CLIError(
|
|
637
|
+
`Invalid field argument "${field}": expected format key=value`
|
|
638
|
+
);
|
|
639
|
+
}
|
|
640
|
+
const key = field.slice(0, eqIndex);
|
|
641
|
+
const rawValue = field.slice(eqIndex + 1);
|
|
642
|
+
if (typed) {
|
|
643
|
+
if (rawValue === "true") result[key] = true;
|
|
644
|
+
else if (rawValue === "false") result[key] = false;
|
|
645
|
+
else if (rawValue === "null") result[key] = null;
|
|
646
|
+
else if (rawValue.startsWith("@")) {
|
|
647
|
+
const filePath = rawValue.slice(1);
|
|
648
|
+
result[key] = readFileSync2(filePath, "utf-8");
|
|
649
|
+
} else if (/^\d+$/.test(rawValue)) result[key] = parseInt(rawValue, 10);
|
|
650
|
+
else result[key] = rawValue;
|
|
651
|
+
} else {
|
|
652
|
+
result[key] = rawValue;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
return result;
|
|
656
|
+
}
|
|
657
|
+
function registerApiCommand(program) {
|
|
658
|
+
program.command("api").description("Make an authenticated API request (like gh api)").argument("<endpoint>", "API endpoint path (e.g., /articles)").option("-X, --method <method>", "HTTP method").option("-f, --field <key=value...>", "String field").option(
|
|
659
|
+
"-F, --typed-field <key=value...>",
|
|
660
|
+
"Typed field (bools, numbers, file reads)"
|
|
661
|
+
).option("--jq <expr>", "Filter response with jq expression").option("--json", "Output as JSON").option("--paginate", "Auto-paginate through all pages").option("--verbose", "Show full HTTP request/response").action(async function(endpoint) {
|
|
662
|
+
const options = this.optsWithGlobals();
|
|
663
|
+
const client = await resolveClient(options);
|
|
664
|
+
const stringFields = options.field ?? [];
|
|
665
|
+
const typedFields = options.typedField ?? [];
|
|
666
|
+
const body = {
|
|
667
|
+
...parseFields(stringFields, false),
|
|
668
|
+
...parseFields(typedFields, true)
|
|
669
|
+
};
|
|
670
|
+
const hasBody = stringFields.length > 0 || typedFields.length > 0;
|
|
671
|
+
const method = options.method ?? (hasBody ? "POST" : "GET");
|
|
672
|
+
let result;
|
|
673
|
+
if (options.paginate && method.toUpperCase() === "GET") {
|
|
674
|
+
result = await client.paginate(
|
|
675
|
+
endpoint,
|
|
676
|
+
hasBody ? body : void 0
|
|
677
|
+
);
|
|
678
|
+
} else {
|
|
679
|
+
switch (method.toUpperCase()) {
|
|
680
|
+
case "GET":
|
|
681
|
+
result = await client.get(
|
|
682
|
+
endpoint,
|
|
683
|
+
hasBody ? body : void 0
|
|
684
|
+
);
|
|
685
|
+
break;
|
|
686
|
+
case "POST":
|
|
687
|
+
result = await client.post(endpoint, hasBody ? body : void 0);
|
|
688
|
+
break;
|
|
689
|
+
case "PUT":
|
|
690
|
+
result = await client.put(endpoint, hasBody ? body : void 0);
|
|
691
|
+
break;
|
|
692
|
+
case "PATCH":
|
|
693
|
+
result = await client.patch(endpoint, hasBody ? body : void 0);
|
|
694
|
+
break;
|
|
695
|
+
case "DELETE":
|
|
696
|
+
result = await client.delete(endpoint);
|
|
697
|
+
break;
|
|
698
|
+
default:
|
|
699
|
+
throw new CLIError(`Unsupported method: ${method}`);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
let listItems;
|
|
703
|
+
if (Array.isArray(result)) {
|
|
704
|
+
listItems = result;
|
|
705
|
+
} else if (result && typeof result === "object" && Array.isArray(result.data)) {
|
|
706
|
+
listItems = result.data;
|
|
707
|
+
}
|
|
708
|
+
render(result, options, () => formatJson(result), listItems);
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// src/commands/auth/login.ts
|
|
713
|
+
function registerLoginCommand(auth) {
|
|
714
|
+
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)").action(async (options) => {
|
|
715
|
+
if (options.token) {
|
|
716
|
+
await loginWithToken(options.token);
|
|
717
|
+
} else if (options.device) {
|
|
718
|
+
throw new CLIError("Device flow not yet available. Server-side endpoints required. Use --token for now, or run the browser flow without --device.", 1);
|
|
719
|
+
} else {
|
|
720
|
+
throw new CLIError(
|
|
721
|
+
"OAuth login is not yet available. Use --token <token> or set the INTERCOM_TOKEN environment variable.",
|
|
722
|
+
1
|
|
723
|
+
);
|
|
724
|
+
}
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
async function loginWithToken(token) {
|
|
728
|
+
const authType = detectAuthType(token);
|
|
729
|
+
const client = new IntercomClient({
|
|
730
|
+
token,
|
|
731
|
+
authType,
|
|
732
|
+
baseUrl: process.env.INTERCOM_API_BASE_URL || void 0
|
|
733
|
+
});
|
|
734
|
+
const me = await client.get("/me");
|
|
735
|
+
const app = me.app;
|
|
736
|
+
const appId = app?.id_code ?? me.id;
|
|
737
|
+
const name = app?.name ?? "Unknown";
|
|
738
|
+
const configDir = process.env.INTERCOM_CONFIG_DIR;
|
|
739
|
+
const store = new CredentialStore(createDefaultBackend(configDir));
|
|
740
|
+
await store.set(appId, { token, auth_type: authType, app_id: appId });
|
|
741
|
+
await addWorkspace(configDir, { app_id: appId, name });
|
|
742
|
+
console.log(`Authenticated to ${name} (${appId})`);
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// src/auth/cookies.ts
|
|
746
|
+
function extractCookies(response) {
|
|
747
|
+
const setCookies = typeof response.headers.getSetCookie === "function" ? response.headers.getSetCookie() : (response.headers.get("set-cookie") ?? "").split(/,(?=[^ ])/).filter(Boolean);
|
|
748
|
+
return setCookies.map((c) => c.split(";")[0]).join("; ");
|
|
749
|
+
}
|
|
750
|
+
function mergeCookies(existing, newCookies) {
|
|
751
|
+
const map = /* @__PURE__ */ new Map();
|
|
752
|
+
for (const cookie of [...existing.split("; "), ...newCookies.split("; ")]) {
|
|
753
|
+
const eqIdx = cookie.indexOf("=");
|
|
754
|
+
if (eqIdx === -1) continue;
|
|
755
|
+
const name = cookie.slice(0, eqIdx).trim();
|
|
756
|
+
const value = cookie.slice(eqIdx + 1);
|
|
757
|
+
if (name) map.set(name, value);
|
|
758
|
+
}
|
|
759
|
+
return Array.from(map.entries()).map(([k, v]) => `${k}=${v}`).join("; ");
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// src/auth/signup.ts
|
|
763
|
+
var DEFAULT_APP_BASE_URL = "https://app.intercom.com";
|
|
764
|
+
function getAppBaseUrl() {
|
|
765
|
+
return process.env.INTERCOM_APP_BASE_URL ?? DEFAULT_APP_BASE_URL;
|
|
766
|
+
}
|
|
767
|
+
var PLAN_SOLUTION_IDS = {
|
|
768
|
+
essential: 28,
|
|
769
|
+
advanced: 29,
|
|
770
|
+
expert: 30
|
|
771
|
+
};
|
|
772
|
+
var CSRF_TOKEN_PATTERN = /<meta\s+name="csrf-token"\s+content="([^"]+)"/;
|
|
773
|
+
async function fetchCsrfToken(url, timeoutMs = API_TIMEOUT_MS, init) {
|
|
774
|
+
const response = await fetchWithTimeout(url, init ?? {}, { timeoutMs });
|
|
775
|
+
if (!response.ok)
|
|
776
|
+
throw new Error(`Failed to fetch ${url}: ${response.status}`);
|
|
777
|
+
const html = await response.text();
|
|
778
|
+
const match = CSRF_TOKEN_PATTERN.exec(html);
|
|
779
|
+
if (!match) throw new Error("Could not find CSRF token");
|
|
780
|
+
return { token: match[1], cookies: extractCookies(response) };
|
|
781
|
+
}
|
|
782
|
+
async function signup(options) {
|
|
783
|
+
const {
|
|
784
|
+
baseUrl,
|
|
785
|
+
email,
|
|
786
|
+
name,
|
|
787
|
+
password,
|
|
788
|
+
companyName,
|
|
789
|
+
plan = "advanced",
|
|
790
|
+
timeoutMs = API_TIMEOUT_MS
|
|
791
|
+
} = options;
|
|
792
|
+
const csrfUrl = `${baseUrl}/admins/sign_up/email`;
|
|
793
|
+
const { token: csrfToken, cookies } = await fetchCsrfToken(
|
|
794
|
+
csrfUrl,
|
|
795
|
+
timeoutMs
|
|
796
|
+
);
|
|
797
|
+
const solutionId = PLAN_SOLUTION_IDS[plan] ?? PLAN_SOLUTION_IDS["advanced"];
|
|
798
|
+
const body = {
|
|
799
|
+
admin: {
|
|
800
|
+
name,
|
|
801
|
+
email,
|
|
802
|
+
password,
|
|
803
|
+
terms_of_service: true
|
|
804
|
+
},
|
|
805
|
+
app: {
|
|
806
|
+
name: companyName,
|
|
807
|
+
solution_id: solutionId
|
|
808
|
+
},
|
|
809
|
+
pageview: {},
|
|
810
|
+
email_submission: {},
|
|
811
|
+
sign_up_locale: "en",
|
|
812
|
+
new_app_feature_flags: [
|
|
813
|
+
"synchronously-create-subscription-on-signup",
|
|
814
|
+
"fin-sdk",
|
|
815
|
+
"messenger-public-api"
|
|
816
|
+
]
|
|
817
|
+
};
|
|
818
|
+
const response = await fetchWithTimeout(
|
|
819
|
+
`${baseUrl}/ember/signups`,
|
|
820
|
+
{
|
|
821
|
+
method: "POST",
|
|
822
|
+
headers: {
|
|
823
|
+
"Content-Type": "application/json",
|
|
824
|
+
"X-CSRF-Token": csrfToken,
|
|
825
|
+
Accept: "application/json",
|
|
826
|
+
...cookies ? { Cookie: cookies } : {}
|
|
827
|
+
},
|
|
828
|
+
body: JSON.stringify(body)
|
|
829
|
+
},
|
|
830
|
+
{ timeoutMs }
|
|
831
|
+
);
|
|
832
|
+
if (response.status === 409) {
|
|
833
|
+
throw new Error("An account with that email address already exists.");
|
|
834
|
+
}
|
|
835
|
+
if (response.status === 422) {
|
|
836
|
+
const data = await response.json();
|
|
837
|
+
const msg = data.errors?.join(", ") ?? "Validation error";
|
|
838
|
+
throw new Error(`Signup validation failed: ${msg}`);
|
|
839
|
+
}
|
|
840
|
+
if (response.status === 403) {
|
|
841
|
+
throw new Error(
|
|
842
|
+
"Signup blocked (403 Forbidden). The request may have been blocked by WAF."
|
|
843
|
+
);
|
|
844
|
+
}
|
|
845
|
+
if (!response.ok) {
|
|
846
|
+
throw new Error(`Signup failed with status ${response.status}`);
|
|
847
|
+
}
|
|
848
|
+
const workspace = await response.json();
|
|
849
|
+
return { id: workspace.id, name: workspace.name };
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
// src/auth/devhub.ts
|
|
853
|
+
async function deviseSignIn(baseUrl, email, password, timeoutMs = API_TIMEOUT_MS) {
|
|
854
|
+
const signInUrl = `${baseUrl}/admins/sign_in`;
|
|
855
|
+
const { token: csrfToken, cookies: initialCookies } = await fetchCsrfToken(
|
|
856
|
+
signInUrl,
|
|
857
|
+
timeoutMs,
|
|
858
|
+
{ redirect: "manual" }
|
|
859
|
+
);
|
|
860
|
+
let cookies = initialCookies;
|
|
861
|
+
const body = new URLSearchParams({
|
|
862
|
+
"admin[email]": email,
|
|
863
|
+
"admin[password]": password,
|
|
864
|
+
"admin[remember_me]": "1",
|
|
865
|
+
authenticity_token: csrfToken
|
|
866
|
+
});
|
|
867
|
+
const signInResponse = await fetchWithTimeout(
|
|
868
|
+
signInUrl,
|
|
869
|
+
{
|
|
870
|
+
method: "POST",
|
|
871
|
+
headers: {
|
|
872
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
873
|
+
Accept: "text/html",
|
|
874
|
+
...cookies ? { Cookie: cookies } : {}
|
|
875
|
+
},
|
|
876
|
+
body: body.toString(),
|
|
877
|
+
redirect: "manual"
|
|
878
|
+
},
|
|
879
|
+
{ timeoutMs }
|
|
880
|
+
);
|
|
881
|
+
if (signInResponse.status !== 302 && signInResponse.status !== 301) {
|
|
882
|
+
throw new Error(
|
|
883
|
+
`Sign-in returned unexpected status ${signInResponse.status}`
|
|
884
|
+
);
|
|
885
|
+
}
|
|
886
|
+
const location = signInResponse.headers.get("location") ?? "";
|
|
887
|
+
if (location.includes("/admins/sign_in")) {
|
|
888
|
+
throw new Error("Invalid credentials");
|
|
889
|
+
}
|
|
890
|
+
if (location.includes("two_factor")) {
|
|
891
|
+
throw new Error(
|
|
892
|
+
"Two-factor authentication is required. Please use an API key instead."
|
|
893
|
+
);
|
|
894
|
+
}
|
|
895
|
+
cookies = mergeCookies(cookies, extractCookies(signInResponse));
|
|
896
|
+
return cookies;
|
|
897
|
+
}
|
|
898
|
+
async function findOrCreateAppPackage(baseUrl, cookies, appId, appPackageId, timeoutMs = API_TIMEOUT_MS) {
|
|
899
|
+
if (appPackageId) {
|
|
900
|
+
return { id: appPackageId, name: "CLI Access Token", created: false };
|
|
901
|
+
}
|
|
902
|
+
const listUrl = `${baseUrl}/ember/developer_home/app_packages?app_id=${appId}`;
|
|
903
|
+
const listResponse = await fetchWithTimeout(
|
|
904
|
+
listUrl,
|
|
905
|
+
{
|
|
906
|
+
headers: {
|
|
907
|
+
Accept: "application/json",
|
|
908
|
+
Cookie: cookies
|
|
909
|
+
}
|
|
910
|
+
},
|
|
911
|
+
{ timeoutMs }
|
|
912
|
+
);
|
|
913
|
+
if (!listResponse.ok) {
|
|
914
|
+
throw new Error(`Failed to list app packages: ${listResponse.status}`);
|
|
915
|
+
}
|
|
916
|
+
const data = await listResponse.json();
|
|
917
|
+
const packages = data.app_packages ?? (Array.isArray(data) ? data : []);
|
|
918
|
+
if (packages.length > 0) {
|
|
919
|
+
return {
|
|
920
|
+
id: String(packages[0].id),
|
|
921
|
+
name: packages[0].name ?? "CLI Access Token",
|
|
922
|
+
created: false
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
let freshCsrf = "";
|
|
926
|
+
try {
|
|
927
|
+
const { token, cookies: csrfCookies } = await fetchCsrfToken(
|
|
928
|
+
`${baseUrl}/admins/sign_in`,
|
|
929
|
+
timeoutMs,
|
|
930
|
+
{ headers: { Cookie: cookies } }
|
|
931
|
+
);
|
|
932
|
+
freshCsrf = token;
|
|
933
|
+
if (csrfCookies) cookies = mergeCookies(cookies, csrfCookies);
|
|
934
|
+
} catch {
|
|
935
|
+
}
|
|
936
|
+
const createResponse = await fetchWithTimeout(
|
|
937
|
+
`${baseUrl}/ember/developer_home/app_packages`,
|
|
938
|
+
{
|
|
939
|
+
method: "POST",
|
|
940
|
+
headers: {
|
|
941
|
+
"Content-Type": "application/json",
|
|
942
|
+
Accept: "application/json",
|
|
943
|
+
Cookie: cookies,
|
|
944
|
+
...freshCsrf ? { "X-CSRF-Token": freshCsrf } : {}
|
|
945
|
+
},
|
|
946
|
+
body: JSON.stringify({
|
|
947
|
+
name: "CLI Access Token",
|
|
948
|
+
owner_app_id: appId,
|
|
949
|
+
app_id: appId
|
|
950
|
+
})
|
|
951
|
+
},
|
|
952
|
+
{ timeoutMs }
|
|
953
|
+
);
|
|
954
|
+
if (!createResponse.ok) {
|
|
955
|
+
throw new Error(`Failed to create app package: ${createResponse.status}`);
|
|
956
|
+
}
|
|
957
|
+
const created = await createResponse.json();
|
|
958
|
+
const pkg = created.app_package ?? created;
|
|
959
|
+
return {
|
|
960
|
+
id: String(pkg.id),
|
|
961
|
+
name: pkg.name ?? "CLI Access Token",
|
|
962
|
+
created: true
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
async function fetchAccessToken(baseUrl, cookies, appPackageId, appId, timeoutMs = API_TIMEOUT_MS) {
|
|
966
|
+
const url = `${baseUrl}/ember/developer_home/app_package_installs?app_package_id=${appPackageId}&only_owner_app=true&app_id=${appId}`;
|
|
967
|
+
const response = await fetchWithTimeout(
|
|
968
|
+
url,
|
|
969
|
+
{
|
|
970
|
+
headers: {
|
|
971
|
+
Accept: "application/json",
|
|
972
|
+
Cookie: cookies
|
|
973
|
+
}
|
|
974
|
+
},
|
|
975
|
+
{ timeoutMs }
|
|
976
|
+
);
|
|
977
|
+
if (!response.ok) {
|
|
978
|
+
throw new Error(`Failed to fetch app package installs: ${response.status}`);
|
|
979
|
+
}
|
|
980
|
+
const data = await response.json();
|
|
981
|
+
if (Array.isArray(data)) {
|
|
982
|
+
const token = data[0]?.access_token;
|
|
983
|
+
if (token) return token;
|
|
984
|
+
} else if (data.access_token) {
|
|
985
|
+
return data.access_token;
|
|
986
|
+
} else if (data.app_package_installs?.[0]?.access_token) {
|
|
987
|
+
return data.app_package_installs[0].access_token;
|
|
988
|
+
}
|
|
989
|
+
throw new Error("No access token found in app package installs response");
|
|
990
|
+
}
|
|
991
|
+
async function getToken(options) {
|
|
992
|
+
const { baseUrl, email, password, appId, appPackageId, timeoutMs } = options;
|
|
993
|
+
const cookies = await deviseSignIn(baseUrl, email, password, timeoutMs);
|
|
994
|
+
const appPackage = await findOrCreateAppPackage(
|
|
995
|
+
baseUrl,
|
|
996
|
+
cookies,
|
|
997
|
+
appId,
|
|
998
|
+
appPackageId,
|
|
999
|
+
timeoutMs
|
|
1000
|
+
);
|
|
1001
|
+
const token = await fetchAccessToken(
|
|
1002
|
+
baseUrl,
|
|
1003
|
+
cookies,
|
|
1004
|
+
appPackage.id,
|
|
1005
|
+
appId,
|
|
1006
|
+
timeoutMs
|
|
1007
|
+
);
|
|
1008
|
+
return { token, appId, appPackage };
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
// src/auth/password.ts
|
|
1012
|
+
async function resolvePassword(options) {
|
|
1013
|
+
const envPassword = process.env.INTERCOM_PASSWORD;
|
|
1014
|
+
if (envPassword) return envPassword;
|
|
1015
|
+
if (options.passwordStdin) {
|
|
1016
|
+
return readStdin();
|
|
1017
|
+
}
|
|
1018
|
+
throw new CLIError(
|
|
1019
|
+
'Password required. Set INTERCOM_PASSWORD env var or pass --password-stdin.\n Example: INTERCOM_PASSWORD=secret intercom auth bootstrap --email admin@co.com --app-id abc123\n Example: echo "$PASSWORD" | intercom auth bootstrap --password-stdin --email admin@co.com --app-id abc123'
|
|
1020
|
+
);
|
|
1021
|
+
}
|
|
1022
|
+
async function readStdin() {
|
|
1023
|
+
if (process.stdin.isTTY) {
|
|
1024
|
+
throw new CLIError(
|
|
1025
|
+
'--password-stdin requires piped input. Stdin is a TTY.\n Example: echo "$PASSWORD" | intercom auth bootstrap --password-stdin ...'
|
|
1026
|
+
);
|
|
1027
|
+
}
|
|
1028
|
+
return new Promise((resolve, reject) => {
|
|
1029
|
+
let data = "";
|
|
1030
|
+
process.stdin.setEncoding("utf8");
|
|
1031
|
+
process.stdin.on("data", (chunk) => {
|
|
1032
|
+
data += chunk;
|
|
1033
|
+
});
|
|
1034
|
+
process.stdin.on("end", () => resolve(data.trim()));
|
|
1035
|
+
process.stdin.on("error", reject);
|
|
1036
|
+
});
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
// src/commands/auth/bootstrap.ts
|
|
1040
|
+
function registerBootstrapCommand(auth) {
|
|
1041
|
+
auth.command("bootstrap").description("Acquire a Developer Hub access token for a workspace").requiredOption("--email <email>", "Admin email address").requiredOption("--app-id <appId>", "Workspace app ID (e.g. abc123de)").option("--app-package-id <id>", "Skip package discovery and use this app package ID").option("--password-stdin", "Read password from stdin (or set INTERCOM_PASSWORD env var)").action(async (options) => {
|
|
1042
|
+
const baseUrl = getAppBaseUrl();
|
|
1043
|
+
try {
|
|
1044
|
+
const password = await resolvePassword({ passwordStdin: options.passwordStdin });
|
|
1045
|
+
const { token, appId, appPackage } = await getToken({
|
|
1046
|
+
baseUrl,
|
|
1047
|
+
email: options.email,
|
|
1048
|
+
password,
|
|
1049
|
+
appId: options.appId,
|
|
1050
|
+
appPackageId: options.appPackageId
|
|
1051
|
+
});
|
|
1052
|
+
console.log("Signed in to Intercom");
|
|
1053
|
+
if (appPackage.created) {
|
|
1054
|
+
console.log(`Created app package: ${appPackage.name} (id: ${appPackage.id})`);
|
|
1055
|
+
} else {
|
|
1056
|
+
console.log(`Found app package: ${appPackage.name} (id: ${appPackage.id})`);
|
|
1057
|
+
}
|
|
1058
|
+
console.log("Access token acquired");
|
|
1059
|
+
const configDir = process.env.INTERCOM_CONFIG_DIR;
|
|
1060
|
+
const store = new CredentialStore(createDefaultBackend(configDir));
|
|
1061
|
+
await store.set(appId, { token, auth_type: "oauth", app_id: appId });
|
|
1062
|
+
await addWorkspace(configDir, { app_id: appId, name: appId });
|
|
1063
|
+
console.log(`
|
|
1064
|
+
Authenticated to workspace ${appId}`);
|
|
1065
|
+
} catch (err) {
|
|
1066
|
+
if (err instanceof CLIError) throw err;
|
|
1067
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1068
|
+
throw new CLIError(message);
|
|
1069
|
+
}
|
|
1070
|
+
});
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
// src/output/formatter.ts
|
|
1074
|
+
function table({ columns, rows }) {
|
|
1075
|
+
if (rows.length === 0) return "";
|
|
1076
|
+
const widths = columns.map(
|
|
1077
|
+
(col) => Math.max(col.length, ...rows.map((row) => (row[col] ?? "").length))
|
|
1078
|
+
);
|
|
1079
|
+
const formatRow = (values) => values.map((val, i) => i < values.length - 1 ? val.padEnd(widths[i]) : val).join(" ");
|
|
1080
|
+
const header = formatRow(columns);
|
|
1081
|
+
const body = rows.map(
|
|
1082
|
+
(row) => formatRow(columns.map((col) => row[col] ?? ""))
|
|
1083
|
+
);
|
|
1084
|
+
return [header, ...body].join("\n");
|
|
1085
|
+
}
|
|
1086
|
+
function detail(data) {
|
|
1087
|
+
const entries = Object.entries(data).filter(
|
|
1088
|
+
([, v]) => v !== null && v !== void 0
|
|
1089
|
+
);
|
|
1090
|
+
if (entries.length === 0) return "";
|
|
1091
|
+
const maxKeyLen = Math.max(...entries.map(([k]) => k.length));
|
|
1092
|
+
return entries.map(([key, value]) => `${key.padEnd(maxKeyLen)}: ${value}`).join("\n");
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
// src/commands/auth/status.ts
|
|
1096
|
+
function registerStatusCommand(auth) {
|
|
1097
|
+
auth.command("status").description("Show current authentication status").option("--json", "Output as JSON").action(async (options) => {
|
|
1098
|
+
const configDir = process.env.INTERCOM_CONFIG_DIR;
|
|
1099
|
+
const config = await readConfig(configDir);
|
|
1100
|
+
const appId = config.default;
|
|
1101
|
+
if (!appId) {
|
|
1102
|
+
throw new AuthError('Not authenticated. Run "intercom auth login" to get started.');
|
|
1103
|
+
}
|
|
1104
|
+
const store = new CredentialStore(createDefaultBackend(configDir));
|
|
1105
|
+
const cred = await store.get(appId);
|
|
1106
|
+
if (!cred) {
|
|
1107
|
+
throw new AuthError('Not authenticated. Run "intercom auth login" to get started.');
|
|
1108
|
+
}
|
|
1109
|
+
const workspace = config.workspaces[appId];
|
|
1110
|
+
if (options.json) {
|
|
1111
|
+
console.log(formatJson({
|
|
1112
|
+
app_id: cred.app_id,
|
|
1113
|
+
workspace_name: workspace?.name ?? cred.workspace_name,
|
|
1114
|
+
auth_type: cred.auth_type,
|
|
1115
|
+
expires_at: cred.expires_at ?? null
|
|
1116
|
+
}));
|
|
1117
|
+
} else {
|
|
1118
|
+
console.log(detail({
|
|
1119
|
+
"Workspace": workspace?.name ?? cred.workspace_name ?? cred.app_id,
|
|
1120
|
+
"App ID": cred.app_id,
|
|
1121
|
+
"Auth type": cred.auth_type,
|
|
1122
|
+
"Expires": cred.expires_at ?? void 0
|
|
1123
|
+
}));
|
|
1124
|
+
}
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
// src/commands/auth/logout.ts
|
|
1129
|
+
function registerLogoutCommand(auth) {
|
|
1130
|
+
auth.command("logout").description("Remove stored credentials").option("--workspace <id>", "Workspace app_id to log out of").action(async (options) => {
|
|
1131
|
+
const configDir = process.env.INTERCOM_CONFIG_DIR;
|
|
1132
|
+
const config = await readConfig(configDir);
|
|
1133
|
+
const appId = options.workspace ?? config.default;
|
|
1134
|
+
if (!appId) {
|
|
1135
|
+
throw new AuthError("Not authenticated.");
|
|
1136
|
+
}
|
|
1137
|
+
const store = new CredentialStore(createDefaultBackend(configDir));
|
|
1138
|
+
await store.delete(appId);
|
|
1139
|
+
await removeWorkspace(configDir, appId);
|
|
1140
|
+
const name = config.workspaces[appId]?.name ?? appId;
|
|
1141
|
+
console.log(`Logged out of ${name} (${appId})`);
|
|
1142
|
+
});
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
// src/commands/auth/signup.ts
|
|
1146
|
+
function registerSignupCommand(auth) {
|
|
1147
|
+
auth.command("signup").description("Create a new Intercom workspace").requiredOption("--email <email>", "Admin email address").requiredOption("--name <name>", "Admin full name").requiredOption("--company <company>", "Workspace/company name").option("--password-stdin", "Read password from stdin (or set INTERCOM_PASSWORD env var)").option("--plan <plan>", "Trial plan: essential, advanced, or expert", "advanced").action(async (options) => {
|
|
1148
|
+
const validPlans = ["essential", "advanced", "expert"];
|
|
1149
|
+
if (!validPlans.includes(options.plan)) {
|
|
1150
|
+
throw new CLIError(`Unknown plan "${options.plan}". Valid options: ${validPlans.join(", ")}`);
|
|
1151
|
+
}
|
|
1152
|
+
const baseUrl = getAppBaseUrl();
|
|
1153
|
+
try {
|
|
1154
|
+
const password = await resolvePassword({ passwordStdin: options.passwordStdin });
|
|
1155
|
+
const workspace = await signup({
|
|
1156
|
+
baseUrl,
|
|
1157
|
+
email: options.email,
|
|
1158
|
+
name: options.name,
|
|
1159
|
+
password,
|
|
1160
|
+
companyName: options.company,
|
|
1161
|
+
plan: options.plan
|
|
1162
|
+
});
|
|
1163
|
+
console.log("Workspace created successfully!");
|
|
1164
|
+
console.log(
|
|
1165
|
+
detail({
|
|
1166
|
+
Name: workspace.name,
|
|
1167
|
+
"Workspace ID": workspace.id,
|
|
1168
|
+
URL: `${baseUrl}/a/apps/${workspace.id}`
|
|
1169
|
+
})
|
|
1170
|
+
);
|
|
1171
|
+
} catch (err) {
|
|
1172
|
+
if (err instanceof CLIError) throw err;
|
|
1173
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1174
|
+
throw new CLIError(message);
|
|
1175
|
+
}
|
|
1176
|
+
});
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
// src/commands/auth/verify-email.ts
|
|
1180
|
+
function registerVerifyEmailCommand(auth) {
|
|
1181
|
+
auth.command("verify-email").description(
|
|
1182
|
+
'Verify workspace email address. After signup, Intercom sends a verification email.\nProvide the verification URL from that email, or verify manually in your browser.\n\nOption 1 \u2014 Manual verification (recommended for humans):\n 1. Open the inbox for the email address you signed up with\n 2. Find the email from Intercom (team@intercom.io) with subject\n "Verify your email address on Intercom" (check spam if not found)\n 3. Click the "Verify your email" button in the email\n 4. You should see a redirect to the Intercom getting-started page\n 5. Once verified, you can skip this command entirely\n\nOption 2 \u2014 Provide the verification URL to this command:\n intercom auth verify-email --url <url>\n\nOption 3 \u2014 Retrieve the URL automatically with a mail CLI:\n Google Workspace (gog): gog gmail messages search "from:team@intercom.io verify email"\n Then: gog gmail get <message_id> | grep verify_email\n Google Workspace (gws): gws gmail search "from:team@intercom.io verify email"\n Any tool that can read the inbox will work \u2014 the URL looks like:\n https://app.intercom.com/admins/<uuid>/verify_email?...'
|
|
1183
|
+
).requiredOption("--url <url>", "Verification URL from the email").action(async (options) => {
|
|
1184
|
+
const url = options.url;
|
|
1185
|
+
if (!url.includes("verify_email")) {
|
|
1186
|
+
throw new CLIError(
|
|
1187
|
+
"URL does not look like a verification link. Expected format:\n https://app.intercom.com/admins/<uuid>/verify_email?..."
|
|
1188
|
+
);
|
|
1189
|
+
}
|
|
1190
|
+
const response = await fetchWithTimeout(url, { redirect: "manual" }, { timeoutMs: API_TIMEOUT_MS });
|
|
1191
|
+
if (response.status === 302 || response.status === 301) {
|
|
1192
|
+
const location = response.headers.get("location") ?? "";
|
|
1193
|
+
if (location.includes("sign_in") || location.includes("getting-started") || location.includes("apps/")) {
|
|
1194
|
+
console.log("Email verified successfully!");
|
|
1195
|
+
return;
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
if (response.status === 200) {
|
|
1199
|
+
console.log("Email verified successfully!");
|
|
1200
|
+
return;
|
|
1201
|
+
}
|
|
1202
|
+
throw new CLIError(
|
|
1203
|
+
`Email verification returned unexpected status ${response.status}. The link may have expired \u2014 check for a newer verification email.`
|
|
1204
|
+
);
|
|
1205
|
+
});
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
// src/commands/auth/index.ts
|
|
1209
|
+
function registerAuthCommand(program) {
|
|
1210
|
+
const auth = program.command("auth").description("Manage authentication credentials");
|
|
1211
|
+
registerLoginCommand(auth);
|
|
1212
|
+
registerBootstrapCommand(auth);
|
|
1213
|
+
registerStatusCommand(auth);
|
|
1214
|
+
registerLogoutCommand(auth);
|
|
1215
|
+
registerSignupCommand(auth);
|
|
1216
|
+
registerVerifyEmailCommand(auth);
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
// src/commands/me.ts
|
|
1220
|
+
function registerMeCommand(program) {
|
|
1221
|
+
program.command("me").description("Show current authenticated admin and workspace info").option("--json", "Output as JSON").option("--jq <expr>", "Filter JSON output with jq expression").action(async function(options) {
|
|
1222
|
+
const opts = this.optsWithGlobals();
|
|
1223
|
+
const client = await resolveClient(opts);
|
|
1224
|
+
const me = await client.get("/me");
|
|
1225
|
+
const app = me.app;
|
|
1226
|
+
render(
|
|
1227
|
+
me,
|
|
1228
|
+
opts,
|
|
1229
|
+
() => detail({
|
|
1230
|
+
"Name": me.name,
|
|
1231
|
+
"Email": me.email,
|
|
1232
|
+
"App ID": app?.id_code ?? me.app_id,
|
|
1233
|
+
"Type": me.type
|
|
1234
|
+
})
|
|
1235
|
+
);
|
|
1236
|
+
});
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
// src/output/html.ts
|
|
1240
|
+
import TurndownService from "turndown";
|
|
1241
|
+
var turndown = new TurndownService({
|
|
1242
|
+
headingStyle: "atx",
|
|
1243
|
+
bulletListMarker: "-"
|
|
1244
|
+
});
|
|
1245
|
+
function htmlToText(html) {
|
|
1246
|
+
if (!html) return "";
|
|
1247
|
+
return turndown.turndown(html).trim();
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
// src/commands/articles.ts
|
|
1251
|
+
function registerArticlesCommand(program) {
|
|
1252
|
+
const articles = program.command("articles").description("Manage help center articles");
|
|
1253
|
+
articles.command("list").description("List help center articles").option("--json", "Output as JSON").option("--jq <expr>", "Filter JSON output with jq expression").option("--verbose", "Show HTTP request/response details").option("--limit <n>", "Max results to return", "25").option("--all", "Auto-paginate all pages").action(async function() {
|
|
1254
|
+
const opts = this.optsWithGlobals();
|
|
1255
|
+
const client = await resolveClient(opts);
|
|
1256
|
+
let result;
|
|
1257
|
+
if (opts.all) {
|
|
1258
|
+
const allItems = await client.paginate("/articles", { per_page: "25" });
|
|
1259
|
+
result = { type: "list", data: allItems };
|
|
1260
|
+
} else {
|
|
1261
|
+
const limit = opts.limit ?? "25";
|
|
1262
|
+
result = await client.get("/articles", { per_page: limit });
|
|
1263
|
+
}
|
|
1264
|
+
const response = result;
|
|
1265
|
+
const items = response.data ?? [];
|
|
1266
|
+
render(
|
|
1267
|
+
result,
|
|
1268
|
+
opts,
|
|
1269
|
+
() => {
|
|
1270
|
+
const rows = items.map((a) => ({
|
|
1271
|
+
ID: String(a.id ?? ""),
|
|
1272
|
+
TITLE: String(a.title ?? ""),
|
|
1273
|
+
STATE: String(a.state ?? ""),
|
|
1274
|
+
UPDATED: formatTimestamp(a.updated_at)
|
|
1275
|
+
}));
|
|
1276
|
+
return table({ columns: ["ID", "TITLE", "STATE", "UPDATED"], rows });
|
|
1277
|
+
},
|
|
1278
|
+
items
|
|
1279
|
+
);
|
|
1280
|
+
});
|
|
1281
|
+
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) {
|
|
1282
|
+
const opts = this.optsWithGlobals();
|
|
1283
|
+
const client = await resolveClient(opts);
|
|
1284
|
+
const result = await client.get(`/articles/${id}`);
|
|
1285
|
+
render(result, opts, () => {
|
|
1286
|
+
const meta = detail({
|
|
1287
|
+
ID: String(result.id ?? ""),
|
|
1288
|
+
Title: String(result.title ?? ""),
|
|
1289
|
+
State: String(result.state ?? ""),
|
|
1290
|
+
"Author ID": result.author_id != null ? String(result.author_id) : void 0,
|
|
1291
|
+
Updated: formatTimestamp(result.updated_at),
|
|
1292
|
+
Created: formatTimestamp(result.created_at)
|
|
1293
|
+
});
|
|
1294
|
+
const body = typeof result.body === "string" ? htmlToText(result.body) : "";
|
|
1295
|
+
return body ? `${meta}
|
|
1296
|
+
|
|
1297
|
+
${body}` : meta;
|
|
1298
|
+
});
|
|
1299
|
+
});
|
|
1300
|
+
articles.command("create").description("Create a help center article").option(
|
|
1301
|
+
"-f, --field <key=value>",
|
|
1302
|
+
"Field to set (repeatable)",
|
|
1303
|
+
(v, acc) => [...acc, v],
|
|
1304
|
+
[]
|
|
1305
|
+
).option("--json", "Output as JSON").option("--jq <expr>", "Filter JSON output with jq expression").option("--verbose", "Show HTTP request/response details").action(async function() {
|
|
1306
|
+
const opts = this.optsWithGlobals();
|
|
1307
|
+
const client = await resolveClient(opts);
|
|
1308
|
+
const body = parseFieldArgs(opts.field ?? []);
|
|
1309
|
+
if (body.author_id && typeof body.author_id === "string") {
|
|
1310
|
+
const parsed = parseInt(body.author_id, 10);
|
|
1311
|
+
if (!isNaN(parsed)) body.author_id = parsed;
|
|
1312
|
+
}
|
|
1313
|
+
const result = await client.post("/articles", body);
|
|
1314
|
+
render(
|
|
1315
|
+
result,
|
|
1316
|
+
opts,
|
|
1317
|
+
() => detail({
|
|
1318
|
+
ID: String(result.id ?? ""),
|
|
1319
|
+
Title: String(result.title ?? ""),
|
|
1320
|
+
State: String(result.state ?? "")
|
|
1321
|
+
})
|
|
1322
|
+
);
|
|
1323
|
+
});
|
|
1324
|
+
articles.command("update <id>").description("Update a help center article").option(
|
|
1325
|
+
"-f, --field <key=value>",
|
|
1326
|
+
"Field to set (repeatable)",
|
|
1327
|
+
(v, acc) => [...acc, v],
|
|
1328
|
+
[]
|
|
1329
|
+
).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) {
|
|
1330
|
+
const opts = this.optsWithGlobals();
|
|
1331
|
+
const client = await resolveClient(opts);
|
|
1332
|
+
const body = parseFieldArgs(opts.field ?? []);
|
|
1333
|
+
if (body.author_id && typeof body.author_id === "string") {
|
|
1334
|
+
const parsed = parseInt(body.author_id, 10);
|
|
1335
|
+
if (!isNaN(parsed)) body.author_id = parsed;
|
|
1336
|
+
}
|
|
1337
|
+
const result = await client.put(`/articles/${id}`, body);
|
|
1338
|
+
render(
|
|
1339
|
+
result,
|
|
1340
|
+
opts,
|
|
1341
|
+
() => detail({
|
|
1342
|
+
ID: String(result.id ?? ""),
|
|
1343
|
+
Title: String(result.title ?? ""),
|
|
1344
|
+
State: String(result.state ?? "")
|
|
1345
|
+
})
|
|
1346
|
+
);
|
|
1347
|
+
});
|
|
1348
|
+
articles.command("delete <id>").description("Delete a help center article").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) {
|
|
1349
|
+
const opts = this.optsWithGlobals();
|
|
1350
|
+
const client = await resolveClient(opts);
|
|
1351
|
+
const result = await client.delete(`/articles/${id}`);
|
|
1352
|
+
const deleted = result;
|
|
1353
|
+
render(result, opts, () => `Deleted article ${deleted.id ?? id}`);
|
|
1354
|
+
});
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
// src/commands/contacts.ts
|
|
1358
|
+
function registerContactsCommand(program) {
|
|
1359
|
+
const contacts = program.command("contacts").description("Manage contacts");
|
|
1360
|
+
contacts.command("list").description("List contacts").option("--json", "Output as JSON").option("--jq <expr>", "Filter JSON output with jq expression").option("--verbose", "Show HTTP request/response details").option("--limit <n>", "Max results to return", "25").option("--all", "Auto-paginate all pages").action(async function() {
|
|
1361
|
+
const opts = this.optsWithGlobals();
|
|
1362
|
+
const client = await resolveClient(opts);
|
|
1363
|
+
let result;
|
|
1364
|
+
if (opts.all) {
|
|
1365
|
+
const allItems = await client.paginate("/contacts", { per_page: "25" });
|
|
1366
|
+
result = { type: "list", data: allItems };
|
|
1367
|
+
} else {
|
|
1368
|
+
const limit = opts.limit ?? "25";
|
|
1369
|
+
result = await client.get("/contacts", { per_page: limit });
|
|
1370
|
+
}
|
|
1371
|
+
const response = result;
|
|
1372
|
+
const items = response.data ?? [];
|
|
1373
|
+
render(
|
|
1374
|
+
result,
|
|
1375
|
+
opts,
|
|
1376
|
+
() => {
|
|
1377
|
+
const rows = items.map((c) => ({
|
|
1378
|
+
ID: String(c.id ?? ""),
|
|
1379
|
+
EMAIL: String(c.email ?? ""),
|
|
1380
|
+
NAME: String(c.name ?? ""),
|
|
1381
|
+
ROLE: String(c.role ?? ""),
|
|
1382
|
+
UPDATED: formatTimestamp(c.updated_at)
|
|
1383
|
+
}));
|
|
1384
|
+
return table({
|
|
1385
|
+
columns: ["ID", "EMAIL", "NAME", "ROLE", "UPDATED"],
|
|
1386
|
+
rows
|
|
1387
|
+
});
|
|
1388
|
+
},
|
|
1389
|
+
items
|
|
1390
|
+
);
|
|
1391
|
+
});
|
|
1392
|
+
contacts.command("get <id>").description("Get a contact 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) {
|
|
1393
|
+
const opts = this.optsWithGlobals();
|
|
1394
|
+
const client = await resolveClient(opts);
|
|
1395
|
+
const result = await client.get(`/contacts/${id}`);
|
|
1396
|
+
render(
|
|
1397
|
+
result,
|
|
1398
|
+
opts,
|
|
1399
|
+
() => detail({
|
|
1400
|
+
ID: String(result.id ?? ""),
|
|
1401
|
+
Name: result.name != null ? String(result.name) : void 0,
|
|
1402
|
+
Email: result.email != null ? String(result.email) : void 0,
|
|
1403
|
+
Role: String(result.role ?? ""),
|
|
1404
|
+
Created: formatTimestamp(result.created_at),
|
|
1405
|
+
Updated: formatTimestamp(result.updated_at)
|
|
1406
|
+
})
|
|
1407
|
+
);
|
|
1408
|
+
});
|
|
1409
|
+
contacts.command("create").description("Create a contact").option(
|
|
1410
|
+
"-f, --field <key=value>",
|
|
1411
|
+
"Field to set (repeatable)",
|
|
1412
|
+
(v, acc) => [...acc, v],
|
|
1413
|
+
[]
|
|
1414
|
+
).option("--json", "Output as JSON").option("--jq <expr>", "Filter JSON output with jq expression").option("--verbose", "Show HTTP request/response details").action(async function() {
|
|
1415
|
+
const opts = this.optsWithGlobals();
|
|
1416
|
+
const client = await resolveClient(opts);
|
|
1417
|
+
const body = parseFieldArgs(opts.field ?? []);
|
|
1418
|
+
const result = await client.post("/contacts", body);
|
|
1419
|
+
const r = result;
|
|
1420
|
+
render(
|
|
1421
|
+
result,
|
|
1422
|
+
opts,
|
|
1423
|
+
() => detail({
|
|
1424
|
+
ID: String(r.id ?? ""),
|
|
1425
|
+
Name: r.name != null ? String(r.name) : void 0,
|
|
1426
|
+
Email: r.email != null ? String(r.email) : void 0,
|
|
1427
|
+
Role: String(r.role ?? "")
|
|
1428
|
+
})
|
|
1429
|
+
);
|
|
1430
|
+
});
|
|
1431
|
+
contacts.command("update <id>").description("Update a contact").option(
|
|
1432
|
+
"-f, --field <key=value>",
|
|
1433
|
+
"Field to set (repeatable)",
|
|
1434
|
+
(v, acc) => [...acc, v],
|
|
1435
|
+
[]
|
|
1436
|
+
).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) {
|
|
1437
|
+
const opts = this.optsWithGlobals();
|
|
1438
|
+
const client = await resolveClient(opts);
|
|
1439
|
+
const body = parseFieldArgs(opts.field ?? []);
|
|
1440
|
+
const result = await client.put(`/contacts/${id}`, body);
|
|
1441
|
+
const r = result;
|
|
1442
|
+
render(
|
|
1443
|
+
result,
|
|
1444
|
+
opts,
|
|
1445
|
+
() => detail({
|
|
1446
|
+
ID: String(r.id ?? ""),
|
|
1447
|
+
Name: r.name != null ? String(r.name) : void 0,
|
|
1448
|
+
Email: r.email != null ? String(r.email) : void 0,
|
|
1449
|
+
Role: String(r.role ?? "")
|
|
1450
|
+
})
|
|
1451
|
+
);
|
|
1452
|
+
});
|
|
1453
|
+
contacts.command("delete <id>").description("Delete a contact").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) {
|
|
1454
|
+
const opts = this.optsWithGlobals();
|
|
1455
|
+
const client = await resolveClient(opts);
|
|
1456
|
+
const result = await client.delete(`/contacts/${id}`);
|
|
1457
|
+
const deleted = result;
|
|
1458
|
+
render(result, opts, () => `Deleted contact ${deleted.id ?? id}`);
|
|
1459
|
+
});
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
// src/commands/conversations.ts
|
|
1463
|
+
function formatDatetime(ts) {
|
|
1464
|
+
if (!ts) return "";
|
|
1465
|
+
const n = typeof ts === "number" ? ts : Number(ts);
|
|
1466
|
+
if (isNaN(n)) return String(ts);
|
|
1467
|
+
return new Date(n * 1e3).toISOString().slice(0, 16).replace("T", " ");
|
|
1468
|
+
}
|
|
1469
|
+
function renderConversationStream(result) {
|
|
1470
|
+
const parts = [];
|
|
1471
|
+
parts.push(
|
|
1472
|
+
detail({
|
|
1473
|
+
ID: String(result.id ?? ""),
|
|
1474
|
+
State: String(result.state ?? ""),
|
|
1475
|
+
Open: String(result.open ?? ""),
|
|
1476
|
+
Created: formatTimestamp(result.created_at),
|
|
1477
|
+
Updated: formatTimestamp(result.updated_at)
|
|
1478
|
+
})
|
|
1479
|
+
);
|
|
1480
|
+
const source = result.source;
|
|
1481
|
+
const partsData = result.conversation_parts;
|
|
1482
|
+
const convParts = partsData?.conversation_parts ?? [];
|
|
1483
|
+
if (!source && convParts.length === 0) return parts.join("\n");
|
|
1484
|
+
parts.push("");
|
|
1485
|
+
parts.push("\u2500\u2500 Conversation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
1486
|
+
if (source) {
|
|
1487
|
+
const authorName = source.author?.name ?? "Unknown";
|
|
1488
|
+
const authorType = source.author?.type ?? "user";
|
|
1489
|
+
const body = htmlToText(source.body);
|
|
1490
|
+
parts.push("");
|
|
1491
|
+
parts.push(
|
|
1492
|
+
`${authorName} (${authorType}) \xB7 ${formatDatetime(result.created_at)}`
|
|
1493
|
+
);
|
|
1494
|
+
if (body) parts.push(body);
|
|
1495
|
+
}
|
|
1496
|
+
for (const part of convParts) {
|
|
1497
|
+
const author = part.author;
|
|
1498
|
+
const authorName = author?.name ?? "System";
|
|
1499
|
+
const authorType = author?.type ?? "bot";
|
|
1500
|
+
const partType = String(part.part_type ?? "unknown");
|
|
1501
|
+
const body = htmlToText(part.body);
|
|
1502
|
+
const ts = formatDatetime(part.created_at);
|
|
1503
|
+
parts.push("");
|
|
1504
|
+
switch (partType) {
|
|
1505
|
+
case "comment":
|
|
1506
|
+
parts.push(`${authorName} (${authorType}) \xB7 ${ts}`);
|
|
1507
|
+
if (body) parts.push(body);
|
|
1508
|
+
break;
|
|
1509
|
+
case "note":
|
|
1510
|
+
parts.push(`[note] ${authorName} (${authorType}) \xB7 ${ts}`);
|
|
1511
|
+
if (body) parts.push(body);
|
|
1512
|
+
break;
|
|
1513
|
+
case "assignment": {
|
|
1514
|
+
const assignedTo = part.assigned_to;
|
|
1515
|
+
parts.push(`[assignment] ${authorName} (${authorType}) \xB7 ${ts}`);
|
|
1516
|
+
if (assignedTo?.name) parts.push(`Assigned to ${assignedTo.name}`);
|
|
1517
|
+
break;
|
|
1518
|
+
}
|
|
1519
|
+
case "close":
|
|
1520
|
+
parts.push(`[closed] ${authorName} (${authorType}) \xB7 ${ts}`);
|
|
1521
|
+
if (body) parts.push(body);
|
|
1522
|
+
break;
|
|
1523
|
+
case "open":
|
|
1524
|
+
parts.push(`[opened] ${authorName} (${authorType}) \xB7 ${ts}`);
|
|
1525
|
+
if (body) parts.push(body);
|
|
1526
|
+
break;
|
|
1527
|
+
default:
|
|
1528
|
+
parts.push(`[${partType}] ${authorName} (${authorType}) \xB7 ${ts}`);
|
|
1529
|
+
if (body) parts.push(body);
|
|
1530
|
+
break;
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
return parts.join("\n");
|
|
1534
|
+
}
|
|
1535
|
+
function registerConversationsCommand(program) {
|
|
1536
|
+
const conversations = program.command("conversations").description("Manage conversations");
|
|
1537
|
+
conversations.command("list").description("List conversations").option("--json", "Output as JSON").option("--jq <expr>", "Filter JSON output with jq expression").option("--verbose", "Show HTTP request/response details").option("--limit <n>", "Max results to return", "25").option("--all", "Auto-paginate all pages").action(async function() {
|
|
1538
|
+
const opts = this.optsWithGlobals();
|
|
1539
|
+
const client = await resolveClient(opts);
|
|
1540
|
+
let result;
|
|
1541
|
+
if (opts.all) {
|
|
1542
|
+
const allItems = await client.paginate(
|
|
1543
|
+
"/conversations",
|
|
1544
|
+
{ per_page: "25" },
|
|
1545
|
+
"conversations"
|
|
1546
|
+
);
|
|
1547
|
+
result = { type: "conversation.list", conversations: allItems };
|
|
1548
|
+
} else {
|
|
1549
|
+
const limit = opts.limit ?? "25";
|
|
1550
|
+
result = await client.get("/conversations", { per_page: limit });
|
|
1551
|
+
}
|
|
1552
|
+
const response = result;
|
|
1553
|
+
const items = response.conversations ?? [];
|
|
1554
|
+
render(
|
|
1555
|
+
result,
|
|
1556
|
+
opts,
|
|
1557
|
+
() => {
|
|
1558
|
+
const rows = items.map((c) => ({
|
|
1559
|
+
ID: String(c.id ?? ""),
|
|
1560
|
+
STATE: String(c.state ?? ""),
|
|
1561
|
+
OPEN: String(c.open ?? ""),
|
|
1562
|
+
CREATED: formatTimestamp(c.created_at),
|
|
1563
|
+
UPDATED: formatTimestamp(c.updated_at)
|
|
1564
|
+
}));
|
|
1565
|
+
return table({
|
|
1566
|
+
columns: ["ID", "STATE", "OPEN", "CREATED", "UPDATED"],
|
|
1567
|
+
rows
|
|
1568
|
+
});
|
|
1569
|
+
},
|
|
1570
|
+
items
|
|
1571
|
+
);
|
|
1572
|
+
});
|
|
1573
|
+
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) {
|
|
1574
|
+
const opts = this.optsWithGlobals();
|
|
1575
|
+
const client = await resolveClient(opts);
|
|
1576
|
+
const result = await client.get(`/conversations/${id}`);
|
|
1577
|
+
render(result, opts, () => {
|
|
1578
|
+
const r = result;
|
|
1579
|
+
return renderConversationStream(r);
|
|
1580
|
+
});
|
|
1581
|
+
});
|
|
1582
|
+
conversations.command("reply <id>").description("Reply to a conversation").option(
|
|
1583
|
+
"-f, --field <key=value>",
|
|
1584
|
+
"Field to set (repeatable)",
|
|
1585
|
+
(v, acc) => [...acc, v],
|
|
1586
|
+
[]
|
|
1587
|
+
).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) {
|
|
1588
|
+
const opts = this.optsWithGlobals();
|
|
1589
|
+
const client = await resolveClient(opts);
|
|
1590
|
+
const body = parseFieldArgs(opts.field ?? []);
|
|
1591
|
+
const result = await client.post(`/conversations/${id}/reply`, body);
|
|
1592
|
+
const r = result;
|
|
1593
|
+
render(
|
|
1594
|
+
result,
|
|
1595
|
+
opts,
|
|
1596
|
+
() => detail({
|
|
1597
|
+
ID: String(r.id ?? ""),
|
|
1598
|
+
Type: String(r.type ?? ""),
|
|
1599
|
+
Body: r.body != null ? String(r.body) : void 0
|
|
1600
|
+
})
|
|
1601
|
+
);
|
|
1602
|
+
});
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
// src/commands/help-center.ts
|
|
1606
|
+
function registerHelpCenterCommand(program) {
|
|
1607
|
+
const helpCenter = program.command("help-center").description("Manage help center collections");
|
|
1608
|
+
helpCenter.command("list").description("List help center collections").option("--json", "Output as JSON").option("--jq <expr>", "Filter JSON output with jq expression").option("--verbose", "Show HTTP request/response details").option("--limit <n>", "Max results to return", "25").option("--all", "Auto-paginate all pages").action(async function() {
|
|
1609
|
+
const opts = this.optsWithGlobals();
|
|
1610
|
+
const client = await resolveClient(opts);
|
|
1611
|
+
let result;
|
|
1612
|
+
if (opts.all) {
|
|
1613
|
+
const allItems = await client.paginate("/help_center/collections", {
|
|
1614
|
+
per_page: "25"
|
|
1615
|
+
});
|
|
1616
|
+
result = { type: "list", data: allItems };
|
|
1617
|
+
} else {
|
|
1618
|
+
const limit = opts.limit ?? "25";
|
|
1619
|
+
result = await client.get("/help_center/collections", {
|
|
1620
|
+
per_page: limit
|
|
1621
|
+
});
|
|
1622
|
+
}
|
|
1623
|
+
const response = result;
|
|
1624
|
+
const items = response.data ?? [];
|
|
1625
|
+
render(
|
|
1626
|
+
result,
|
|
1627
|
+
opts,
|
|
1628
|
+
() => {
|
|
1629
|
+
const rows = items.map((c) => ({
|
|
1630
|
+
ID: String(c.id ?? ""),
|
|
1631
|
+
NAME: String(c.name ?? ""),
|
|
1632
|
+
DESCRIPTION: String(c.description ?? ""),
|
|
1633
|
+
UPDATED: formatTimestamp(c.updated_at)
|
|
1634
|
+
}));
|
|
1635
|
+
return table({
|
|
1636
|
+
columns: ["ID", "NAME", "DESCRIPTION", "UPDATED"],
|
|
1637
|
+
rows
|
|
1638
|
+
});
|
|
1639
|
+
},
|
|
1640
|
+
items
|
|
1641
|
+
);
|
|
1642
|
+
});
|
|
1643
|
+
helpCenter.command("get <id>").description("Get a help center collection 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) {
|
|
1644
|
+
const opts = this.optsWithGlobals();
|
|
1645
|
+
const client = await resolveClient(opts);
|
|
1646
|
+
const result = await client.get(`/help_center/collections/${id}`);
|
|
1647
|
+
render(
|
|
1648
|
+
result,
|
|
1649
|
+
opts,
|
|
1650
|
+
() => detail({
|
|
1651
|
+
ID: String(result.id ?? ""),
|
|
1652
|
+
Name: String(result.name ?? ""),
|
|
1653
|
+
Description: result.description != null ? String(result.description) : void 0,
|
|
1654
|
+
Updated: formatTimestamp(result.updated_at),
|
|
1655
|
+
Created: formatTimestamp(result.created_at)
|
|
1656
|
+
})
|
|
1657
|
+
);
|
|
1658
|
+
});
|
|
1659
|
+
helpCenter.command("create").description("Create a help center collection").option(
|
|
1660
|
+
"-f, --field <key=value>",
|
|
1661
|
+
"Field to set (repeatable)",
|
|
1662
|
+
(v, acc) => [...acc, v],
|
|
1663
|
+
[]
|
|
1664
|
+
).option("--json", "Output as JSON").option("--jq <expr>", "Filter JSON output with jq expression").option("--verbose", "Show HTTP request/response details").action(async function() {
|
|
1665
|
+
const opts = this.optsWithGlobals();
|
|
1666
|
+
const client = await resolveClient(opts);
|
|
1667
|
+
const body = parseFieldArgs(opts.field ?? []);
|
|
1668
|
+
const result = await client.post("/help_center/collections", body);
|
|
1669
|
+
const r = result;
|
|
1670
|
+
render(
|
|
1671
|
+
result,
|
|
1672
|
+
opts,
|
|
1673
|
+
() => detail({
|
|
1674
|
+
ID: String(r.id ?? ""),
|
|
1675
|
+
Name: String(r.name ?? ""),
|
|
1676
|
+
Description: r.description != null ? String(r.description) : void 0
|
|
1677
|
+
})
|
|
1678
|
+
);
|
|
1679
|
+
});
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
// src/commands/messenger.ts
|
|
1683
|
+
var UNSTABLE_HEADERS = { "Intercom-Version": "Unstable" };
|
|
1684
|
+
function generateHtmlSnippet(appId) {
|
|
1685
|
+
return `<script>
|
|
1686
|
+
window.intercomSettings = { api_base: "https://api-iam.intercom.io", app_id: "${appId}" };
|
|
1687
|
+
(function(){var w=window;var ic=w.Intercom;if(typeof ic==="function"){ic('reattach_activator');ic('update',w.intercomSettings);}else{var d=document;var i=function(){i.c(arguments);};i.q=[];i.c=function(args){i.q.push(args);};w.Intercom=i;var l=function(){var s=d.createElement('script');s.type='text/javascript';s.async=true;s.src='https://widget.intercom.io/widget/${appId}';var x=d.getElementsByTagName('script')[0];x.parentNode.insertBefore(s,x);};if(document.readyState==='complete'){l();}else if(w.attachEvent){w.attachEvent('onload',l);}else{w.addEventListener('load',l,false);}}})();
|
|
1688
|
+
</script>`;
|
|
1689
|
+
}
|
|
1690
|
+
function generateReactSnippet(appId) {
|
|
1691
|
+
return `import { useEffect } from "react";
|
|
1692
|
+
|
|
1693
|
+
export function IntercomWidget() {
|
|
1694
|
+
useEffect(() => {
|
|
1695
|
+
window.intercomSettings = { api_base: "https://api-iam.intercom.io", app_id: "${appId}" };
|
|
1696
|
+
(function(){var w=window;var ic=w.Intercom;if(typeof ic==="function"){ic('reattach_activator');ic('update',w.intercomSettings);}else{var d=document;var i=function(){i.c(arguments);};i.q=[];i.c=function(args){i.q.push(args);};w.Intercom=i;var l=function(){var s=d.createElement('script');s.type='text/javascript';s.async=true;s.src='https://widget.intercom.io/widget/${appId}';var x=d.getElementsByTagName('script')[0];x.parentNode.insertBefore(s,x);};if(document.readyState==='complete'){l();}else if(w.attachEvent){w.attachEvent('onload',l);}else{w.addEventListener('load',l,false);}}})();
|
|
1697
|
+
}, []);
|
|
1698
|
+
return null;
|
|
1699
|
+
}`;
|
|
1700
|
+
}
|
|
1701
|
+
function registerMessengerCommand(program) {
|
|
1702
|
+
const messenger = program.command("messenger").description("Manage Intercom Messenger settings");
|
|
1703
|
+
messenger.command("get").description("Get Messenger settings").option("--json", "Output as JSON").option("--jq <expr>", "Filter JSON output with jq expression").option("--verbose", "Show HTTP request/response details").action(async function() {
|
|
1704
|
+
const opts = this.optsWithGlobals();
|
|
1705
|
+
const client = await resolveClient(opts);
|
|
1706
|
+
const result = await client.get("/messenger", void 0, UNSTABLE_HEADERS);
|
|
1707
|
+
const r = result;
|
|
1708
|
+
render(
|
|
1709
|
+
result,
|
|
1710
|
+
opts,
|
|
1711
|
+
() => detail({
|
|
1712
|
+
"Color": r.secondary_color != null ? String(r.secondary_color) : void 0,
|
|
1713
|
+
"Alignment": r.alignment != null ? String(r.alignment) : void 0,
|
|
1714
|
+
"Theme": r.theme_mode != null ? String(r.theme_mode) : void 0,
|
|
1715
|
+
"Background": r.background_type != null ? String(r.background_type) : void 0,
|
|
1716
|
+
"Launcher (Users)": r.messenger_enabled_for_users != null ? String(r.messenger_enabled_for_users) : void 0,
|
|
1717
|
+
"Launcher (Visitors)": r.messenger_enabled_for_visitors != null ? String(r.messenger_enabled_for_visitors) : void 0,
|
|
1718
|
+
"Launcher (Mobile)": r.messenger_enabled_for_mobile_users != null ? String(r.messenger_enabled_for_mobile_users) : void 0,
|
|
1719
|
+
"Inbound (Users)": r.inbound_conversations_enabled_for_users != null ? String(r.inbound_conversations_enabled_for_users) : void 0,
|
|
1720
|
+
"Inbound (Visitors)": r.inbound_conversations_enabled_for_visitors != null ? String(r.inbound_conversations_enabled_for_visitors) : void 0
|
|
1721
|
+
})
|
|
1722
|
+
);
|
|
1723
|
+
});
|
|
1724
|
+
messenger.command("update").description("Update Messenger settings").option(
|
|
1725
|
+
"-f, --field <key=value>",
|
|
1726
|
+
"Field to set (repeatable)",
|
|
1727
|
+
(v, acc) => [...acc, v],
|
|
1728
|
+
[]
|
|
1729
|
+
).option("--json", "Output as JSON").option("--jq <expr>", "Filter JSON output with jq expression").option("--verbose", "Show HTTP request/response details").action(async function() {
|
|
1730
|
+
const opts = this.optsWithGlobals();
|
|
1731
|
+
const client = await resolveClient(opts);
|
|
1732
|
+
const body = parseFieldArgs(opts.field ?? []);
|
|
1733
|
+
const result = await client.put("/messenger", body, UNSTABLE_HEADERS);
|
|
1734
|
+
const r = result;
|
|
1735
|
+
render(
|
|
1736
|
+
result,
|
|
1737
|
+
opts,
|
|
1738
|
+
() => detail({
|
|
1739
|
+
"Color": r.secondary_color != null ? String(r.secondary_color) : void 0,
|
|
1740
|
+
"Alignment": r.alignment != null ? String(r.alignment) : void 0,
|
|
1741
|
+
"Theme": r.theme_mode != null ? String(r.theme_mode) : void 0,
|
|
1742
|
+
"Background": r.background_type != null ? String(r.background_type) : void 0,
|
|
1743
|
+
"Launcher (Users)": r.messenger_enabled_for_users != null ? String(r.messenger_enabled_for_users) : void 0,
|
|
1744
|
+
"Launcher (Visitors)": r.messenger_enabled_for_visitors != null ? String(r.messenger_enabled_for_visitors) : void 0,
|
|
1745
|
+
"Launcher (Mobile)": r.messenger_enabled_for_mobile_users != null ? String(r.messenger_enabled_for_mobile_users) : void 0,
|
|
1746
|
+
"Inbound (Users)": r.inbound_conversations_enabled_for_users != null ? String(r.inbound_conversations_enabled_for_users) : void 0,
|
|
1747
|
+
"Inbound (Visitors)": r.inbound_conversations_enabled_for_visitors != null ? String(r.inbound_conversations_enabled_for_visitors) : void 0
|
|
1748
|
+
})
|
|
1749
|
+
);
|
|
1750
|
+
});
|
|
1751
|
+
messenger.command("snippet").description("Generate the Intercom Messenger JavaScript install snippet").option(
|
|
1752
|
+
"--format <format>",
|
|
1753
|
+
"Output format: html (default) or react",
|
|
1754
|
+
"html"
|
|
1755
|
+
).option("--verbose", "Show HTTP request/response details").action(async function() {
|
|
1756
|
+
const opts = this.optsWithGlobals();
|
|
1757
|
+
const client = await resolveClient(opts);
|
|
1758
|
+
const me = await client.get("/me");
|
|
1759
|
+
const app = me.app;
|
|
1760
|
+
const appId = String(app?.id_code ?? "");
|
|
1761
|
+
if (opts.format === "react") {
|
|
1762
|
+
console.log(generateReactSnippet(appId));
|
|
1763
|
+
} else {
|
|
1764
|
+
console.log(generateHtmlSnippet(appId));
|
|
1765
|
+
}
|
|
1766
|
+
});
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
// src/commands/fin/guidance.ts
|
|
1770
|
+
function registerGuidanceCommand(fin) {
|
|
1771
|
+
const guidance = fin.command("guidance").description("Manage Fin SDK guidance");
|
|
1772
|
+
guidance.command("list").description("List guidance from the Fin SDK manifest").option("--json", "Output as JSON").option("--verbose", "Show HTTP request/response details").action(async function() {
|
|
1773
|
+
const opts = this.optsWithGlobals();
|
|
1774
|
+
const client = await resolveClient(opts);
|
|
1775
|
+
const result = await client.get("/fin-sdk/manifest");
|
|
1776
|
+
const manifest = result;
|
|
1777
|
+
const items = manifest.guidance ?? [];
|
|
1778
|
+
render(items, opts, () => {
|
|
1779
|
+
if (items.length === 0) {
|
|
1780
|
+
return "No guidance found.";
|
|
1781
|
+
}
|
|
1782
|
+
const rows = items.map((item) => ({
|
|
1783
|
+
ID: String(item.id ?? ""),
|
|
1784
|
+
NAME: String(item.name ?? "")
|
|
1785
|
+
}));
|
|
1786
|
+
return table({ columns: ["ID", "NAME"], rows });
|
|
1787
|
+
}, items);
|
|
1788
|
+
});
|
|
1789
|
+
guidance.command("get <id>").description("Download a guidance item by ID and print its YAML content").option("--verbose", "Show HTTP request/response details").action(async function(id) {
|
|
1790
|
+
const opts = this.optsWithGlobals();
|
|
1791
|
+
const client = await resolveClient(opts);
|
|
1792
|
+
const text = await client.getText(`/fin-sdk/guidance/${id}/download`);
|
|
1793
|
+
console.log(text);
|
|
1794
|
+
});
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
// src/auth/ember-session.ts
|
|
1798
|
+
async function getEmberSession(options) {
|
|
1799
|
+
const { email, password, appId, verifyEmailUrl, timeoutMs = API_TIMEOUT_MS } = options;
|
|
1800
|
+
const baseUrl = getAppBaseUrl();
|
|
1801
|
+
let cookies = await deviseSignIn(baseUrl, email, password, timeoutMs);
|
|
1802
|
+
if (verifyEmailUrl) {
|
|
1803
|
+
const verifyResponse = await fetchWithTimeout(verifyEmailUrl, {
|
|
1804
|
+
headers: { Cookie: cookies },
|
|
1805
|
+
redirect: "manual"
|
|
1806
|
+
}, { timeoutMs });
|
|
1807
|
+
const newCookies = extractCookies2(verifyResponse);
|
|
1808
|
+
if (newCookies) cookies = mergeCookies2(cookies, newCookies);
|
|
1809
|
+
}
|
|
1810
|
+
const appResponse = await fetchWithTimeout(`${baseUrl}/a/apps/${appId}`, {
|
|
1811
|
+
headers: { Cookie: cookies }
|
|
1812
|
+
}, { timeoutMs });
|
|
1813
|
+
const appCookies = extractCookies2(appResponse);
|
|
1814
|
+
if (appCookies) cookies = mergeCookies2(cookies, appCookies);
|
|
1815
|
+
const html = await appResponse.text();
|
|
1816
|
+
const csrfMatch = /<meta\s+name="csrf-token"\s+content="([^"]+)"/.exec(html);
|
|
1817
|
+
if (!csrfMatch) {
|
|
1818
|
+
throw new Error("Failed to get CSRF token from app page. Email may not be verified.");
|
|
1819
|
+
}
|
|
1820
|
+
return { cookies, csrf: csrfMatch[1], appId };
|
|
1821
|
+
}
|
|
1822
|
+
async function emberRequest(session, path, options = {}) {
|
|
1823
|
+
const { method = "GET", body, timeoutMs = API_TIMEOUT_MS } = options;
|
|
1824
|
+
const baseUrl = getAppBaseUrl();
|
|
1825
|
+
const headers = {
|
|
1826
|
+
Accept: "application/json",
|
|
1827
|
+
"X-CSRF-Token": session.csrf,
|
|
1828
|
+
Cookie: session.cookies
|
|
1829
|
+
};
|
|
1830
|
+
const fetchOptions = { method, headers };
|
|
1831
|
+
if (body !== void 0) {
|
|
1832
|
+
headers["Content-Type"] = "application/json";
|
|
1833
|
+
fetchOptions.body = JSON.stringify(body);
|
|
1834
|
+
}
|
|
1835
|
+
const response = await fetchWithTimeout(
|
|
1836
|
+
`${baseUrl}${path}`,
|
|
1837
|
+
fetchOptions,
|
|
1838
|
+
{ timeoutMs }
|
|
1839
|
+
);
|
|
1840
|
+
const text = await response.text();
|
|
1841
|
+
let data;
|
|
1842
|
+
try {
|
|
1843
|
+
data = JSON.parse(text);
|
|
1844
|
+
} catch {
|
|
1845
|
+
data = text;
|
|
1846
|
+
}
|
|
1847
|
+
return { status: response.status, data };
|
|
1848
|
+
}
|
|
1849
|
+
function extractCookies2(response) {
|
|
1850
|
+
const setCookies = typeof response.headers.getSetCookie === "function" ? response.headers.getSetCookie() : (response.headers.get("set-cookie") ?? "").split(/,(?=[^ ])/).filter(Boolean);
|
|
1851
|
+
return setCookies.map((c) => c.split(";")[0]).join("; ");
|
|
1852
|
+
}
|
|
1853
|
+
function mergeCookies2(existing, newCookies) {
|
|
1854
|
+
const map = /* @__PURE__ */ new Map();
|
|
1855
|
+
for (const cookie of [...existing.split("; "), ...newCookies.split("; ")]) {
|
|
1856
|
+
const eqIdx = cookie.indexOf("=");
|
|
1857
|
+
if (eqIdx === -1) continue;
|
|
1858
|
+
const name = cookie.slice(0, eqIdx).trim();
|
|
1859
|
+
const value = cookie.slice(eqIdx + 1);
|
|
1860
|
+
if (name) map.set(name, value);
|
|
1861
|
+
}
|
|
1862
|
+
return Array.from(map.entries()).map(([k, v]) => `${k}=${v}`).join("; ");
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
// src/commands/fin/enable.ts
|
|
1866
|
+
function registerFinEnableCommand(fin) {
|
|
1867
|
+
fin.command("enable").description(
|
|
1868
|
+
"Enable Fin AI Agent on the workspace.\n\nThis opts the workspace into Fin and activates it to answer conversations.\nRequires email/password authentication (ember session)."
|
|
1869
|
+
).requiredOption("--email <email>", "Admin email address").option("--password-stdin", "Read password from stdin (or set INTERCOM_PASSWORD)").option("--verify-email-url <url>", "Email verification URL (if email not yet verified)").option("--verbose", "Show detailed output").action(async function() {
|
|
1870
|
+
const opts = this.optsWithGlobals();
|
|
1871
|
+
const password = await resolvePassword({ passwordStdin: opts.passwordStdin });
|
|
1872
|
+
const configDir = process.env.INTERCOM_CONFIG_DIR;
|
|
1873
|
+
const config = await readConfig(configDir);
|
|
1874
|
+
const appId = opts.workspace ?? config.default;
|
|
1875
|
+
if (!appId) {
|
|
1876
|
+
throw new CLIError(
|
|
1877
|
+
'No workspace configured. Run "intercom auth signup" or "intercom auth login" first.'
|
|
1878
|
+
);
|
|
1879
|
+
}
|
|
1880
|
+
if (opts.verbose) {
|
|
1881
|
+
console.log(`Workspace: ${appId}`);
|
|
1882
|
+
console.log(`Email: ${opts.email}`);
|
|
1883
|
+
}
|
|
1884
|
+
console.log("Signing in...");
|
|
1885
|
+
const session = await getEmberSession({
|
|
1886
|
+
email: opts.email,
|
|
1887
|
+
password,
|
|
1888
|
+
appId,
|
|
1889
|
+
verifyEmailUrl: opts.verifyEmailUrl
|
|
1890
|
+
});
|
|
1891
|
+
console.log("Enabling Fin...");
|
|
1892
|
+
const optIn = await emberRequest(session, "/ember/fin_opt_in", {
|
|
1893
|
+
method: "POST",
|
|
1894
|
+
body: { app_id: appId }
|
|
1895
|
+
});
|
|
1896
|
+
if (optIn.status === 200) {
|
|
1897
|
+
console.log("\u2713 Fin opted in");
|
|
1898
|
+
} else if (optIn.status === 409) {
|
|
1899
|
+
console.log("\u2713 Fin already opted in");
|
|
1900
|
+
} else {
|
|
1901
|
+
throw new CLIError(
|
|
1902
|
+
`Fin opt-in failed (${optIn.status}). ` + (optIn.status === 403 ? "Email may not be verified \u2014 use --verify-email-url." : JSON.stringify(optIn.data))
|
|
1903
|
+
);
|
|
1904
|
+
}
|
|
1905
|
+
console.log("Checking Fin configuration...");
|
|
1906
|
+
const profile = await emberRequest(
|
|
1907
|
+
session,
|
|
1908
|
+
`/ember/operator_workflows/fin_setup_behavior?app_id=${appId}`
|
|
1909
|
+
);
|
|
1910
|
+
if (profile.status === 204) {
|
|
1911
|
+
throw new CLIError(
|
|
1912
|
+
"Fin setup profile not found. The workspace may not have Fin available on this plan."
|
|
1913
|
+
);
|
|
1914
|
+
}
|
|
1915
|
+
if (profile.status !== 200) {
|
|
1916
|
+
throw new CLIError(
|
|
1917
|
+
`Failed to get Fin profile (${profile.status}): ${JSON.stringify(profile.data)}`
|
|
1918
|
+
);
|
|
1919
|
+
}
|
|
1920
|
+
const profileData = profile.data;
|
|
1921
|
+
const rulesetId = profileData.id;
|
|
1922
|
+
const state = profileData.state;
|
|
1923
|
+
if (opts.verbose) {
|
|
1924
|
+
console.log(` Ruleset ID: ${rulesetId}, current state: ${state}`);
|
|
1925
|
+
}
|
|
1926
|
+
if (state === 1) {
|
|
1927
|
+
console.log("\u2713 Fin already active");
|
|
1928
|
+
} else {
|
|
1929
|
+
console.log("Activating Fin...");
|
|
1930
|
+
const activate = await emberRequest(
|
|
1931
|
+
session,
|
|
1932
|
+
`/ember/matching_system/rulesets/${rulesetId}/activate?app_id=${appId}`,
|
|
1933
|
+
{ method: "PUT", body: {} }
|
|
1934
|
+
);
|
|
1935
|
+
if (activate.status === 200) {
|
|
1936
|
+
const result = activate.data;
|
|
1937
|
+
if (result.state === 1) {
|
|
1938
|
+
console.log("\u2713 Fin activated!");
|
|
1939
|
+
} else {
|
|
1940
|
+
console.log(`\u26A0 Activation returned state ${result.state}`);
|
|
1941
|
+
}
|
|
1942
|
+
} else {
|
|
1943
|
+
throw new CLIError(
|
|
1944
|
+
`Fin activation failed (${activate.status}): ${JSON.stringify(activate.data)}`
|
|
1945
|
+
);
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1948
|
+
console.log(`
|
|
1949
|
+
Fin is live on workspace ${appId}`);
|
|
1950
|
+
});
|
|
1951
|
+
}
|
|
1952
|
+
|
|
1953
|
+
// src/commands/fin.ts
|
|
1954
|
+
function renderResourceTable(label, items) {
|
|
1955
|
+
if (items.length === 0) {
|
|
1956
|
+
return `${label}:
|
|
1957
|
+
(none)`;
|
|
1958
|
+
}
|
|
1959
|
+
const rows = items.map((item) => ({
|
|
1960
|
+
ID: String(item.id ?? ""),
|
|
1961
|
+
NAME: String(item.name ?? "")
|
|
1962
|
+
}));
|
|
1963
|
+
return `${label}:
|
|
1964
|
+
${table({ columns: ["ID", "NAME"], rows })}`;
|
|
1965
|
+
}
|
|
1966
|
+
function registerFinCommand(program) {
|
|
1967
|
+
const fin = program.command("fin").description("Manage Fin SDK resources");
|
|
1968
|
+
fin.command("manifest").description("Show the Fin SDK manifest for this workspace").option("--json", "Output as JSON").option("--jq <expr>", "Filter JSON output with jq expression").option("--verbose", "Show HTTP request/response details").action(async function() {
|
|
1969
|
+
const opts = this.optsWithGlobals();
|
|
1970
|
+
const client = await resolveClient(opts);
|
|
1971
|
+
const result = await client.get("/fin-sdk/manifest");
|
|
1972
|
+
const manifest = result;
|
|
1973
|
+
render(result, opts, () => {
|
|
1974
|
+
const ws = manifest.workspace ?? {};
|
|
1975
|
+
const lines = [];
|
|
1976
|
+
lines.push(detail({
|
|
1977
|
+
"Workspace": ws.name,
|
|
1978
|
+
"App ID": ws.app_id != null ? String(ws.app_id) : void 0,
|
|
1979
|
+
"App ID Code": ws.app_id_code
|
|
1980
|
+
}));
|
|
1981
|
+
lines.push("");
|
|
1982
|
+
lines.push(renderResourceTable("Procedures", manifest.procedures ?? []));
|
|
1983
|
+
lines.push("");
|
|
1984
|
+
lines.push(renderResourceTable("Tasks", manifest.tasks ?? []));
|
|
1985
|
+
lines.push("");
|
|
1986
|
+
lines.push(renderResourceTable("Data Connectors", manifest.data_connectors ?? []));
|
|
1987
|
+
lines.push("");
|
|
1988
|
+
lines.push(renderResourceTable("Guidance", manifest.guidance ?? []));
|
|
1989
|
+
return lines.join("\n");
|
|
1990
|
+
});
|
|
1991
|
+
});
|
|
1992
|
+
registerFinEnableCommand(fin);
|
|
1993
|
+
registerGuidanceCommand(fin);
|
|
1994
|
+
}
|
|
1995
|
+
|
|
1996
|
+
// src/commands/setup.ts
|
|
1997
|
+
import { readdirSync, readFileSync as readFileSync3, statSync } from "fs";
|
|
1998
|
+
import { join as join3, basename } from "path";
|
|
1999
|
+
function isGreenfield(opts) {
|
|
2000
|
+
return !!(opts.email && opts.name && (process.env.INTERCOM_PASSWORD || opts.passwordStdin));
|
|
2001
|
+
}
|
|
2002
|
+
function hasPartialGreenfieldFlags(opts) {
|
|
2003
|
+
const hasPassword = !!(process.env.INTERCOM_PASSWORD || opts.passwordStdin);
|
|
2004
|
+
return !!(opts.email && hasPassword && !opts.name);
|
|
2005
|
+
}
|
|
2006
|
+
function buildPlan(opts) {
|
|
2007
|
+
const steps = [];
|
|
2008
|
+
let stepNum = 1;
|
|
2009
|
+
const greenfield = isGreenfield(opts);
|
|
2010
|
+
if (greenfield) {
|
|
2011
|
+
const company = opts.companyName ?? "<company-name>";
|
|
2012
|
+
steps.push({
|
|
2013
|
+
step: stepNum++,
|
|
2014
|
+
description: "Create a new Intercom workspace",
|
|
2015
|
+
commands: [
|
|
2016
|
+
`INTERCOM_PASSWORD=*** intercom auth signup --email "${opts.email}" --name "${opts.name}" --company "${company}"`
|
|
2017
|
+
]
|
|
2018
|
+
});
|
|
2019
|
+
steps.push({
|
|
2020
|
+
step: stepNum++,
|
|
2021
|
+
description: "Bootstrap authentication token",
|
|
2022
|
+
commands: [
|
|
2023
|
+
`INTERCOM_PASSWORD=*** intercom auth bootstrap --email "${opts.email}" --app-id <workspace_id>`
|
|
2024
|
+
]
|
|
2025
|
+
});
|
|
2026
|
+
}
|
|
2027
|
+
if (opts.companyName) {
|
|
2028
|
+
steps.push({
|
|
2029
|
+
step: stepNum++,
|
|
2030
|
+
description: "Configure Messenger branding",
|
|
2031
|
+
commands: [
|
|
2032
|
+
`intercom messenger update -f name="${opts.companyName}"`
|
|
2033
|
+
]
|
|
2034
|
+
});
|
|
2035
|
+
} else {
|
|
2036
|
+
steps.push({
|
|
2037
|
+
step: stepNum++,
|
|
2038
|
+
description: "Configure Messenger branding",
|
|
2039
|
+
commands: [
|
|
2040
|
+
`intercom messenger update -f name="<your-company-name>"`
|
|
2041
|
+
]
|
|
2042
|
+
});
|
|
2043
|
+
}
|
|
2044
|
+
steps.push({
|
|
2045
|
+
step: stepNum++,
|
|
2046
|
+
description: "Get Messenger install snippet",
|
|
2047
|
+
commands: [
|
|
2048
|
+
`intercom messenger snippet --format html`
|
|
2049
|
+
]
|
|
2050
|
+
});
|
|
2051
|
+
const helpCenterName = opts.companyName ? `${opts.companyName} Help` : "<your-company-name> Help";
|
|
2052
|
+
steps.push({
|
|
2053
|
+
step: stepNum++,
|
|
2054
|
+
description: "Create help center collection",
|
|
2055
|
+
commands: [
|
|
2056
|
+
`intercom help-center create -f name="${helpCenterName}"`
|
|
2057
|
+
]
|
|
2058
|
+
});
|
|
2059
|
+
if (opts.articlesFrom) {
|
|
2060
|
+
let mdFiles = [];
|
|
2061
|
+
try {
|
|
2062
|
+
mdFiles = readdirSync(opts.articlesFrom).filter((f) => f.endsWith(".md"));
|
|
2063
|
+
} catch {
|
|
2064
|
+
mdFiles = [];
|
|
2065
|
+
}
|
|
2066
|
+
if (mdFiles.length > 0) {
|
|
2067
|
+
const articleCommands = mdFiles.map((file) => {
|
|
2068
|
+
const title = basename(file, ".md").replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
2069
|
+
return `intercom articles create -f title="${title}" -F body=@${join3(opts.articlesFrom, file)}`;
|
|
2070
|
+
});
|
|
2071
|
+
steps.push({
|
|
2072
|
+
step: stepNum++,
|
|
2073
|
+
description: `Create ${mdFiles.length} article(s) from ${opts.articlesFrom}`,
|
|
2074
|
+
commands: articleCommands
|
|
2075
|
+
});
|
|
2076
|
+
} else {
|
|
2077
|
+
steps.push({
|
|
2078
|
+
step: stepNum++,
|
|
2079
|
+
description: `Create articles from ${opts.articlesFrom}`,
|
|
2080
|
+
commands: [
|
|
2081
|
+
`intercom articles create -f title="<article-title>" -F body=@${opts.articlesFrom}/<file>.md`
|
|
2082
|
+
]
|
|
2083
|
+
});
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
steps.push({
|
|
2087
|
+
step: stepNum++,
|
|
2088
|
+
description: "Verify Fin SDK access and view configuration",
|
|
2089
|
+
commands: [
|
|
2090
|
+
`intercom fin manifest`
|
|
2091
|
+
]
|
|
2092
|
+
});
|
|
2093
|
+
return steps;
|
|
2094
|
+
}
|
|
2095
|
+
function formatPlanText(steps) {
|
|
2096
|
+
const lines = ["Steps to complete Intercom setup:"];
|
|
2097
|
+
for (const step of steps) {
|
|
2098
|
+
lines.push(`${step.step}. ${step.commands[0]}`);
|
|
2099
|
+
for (let i = 1; i < step.commands.length; i++) {
|
|
2100
|
+
lines.push(` ${step.commands[i]}`);
|
|
2101
|
+
}
|
|
2102
|
+
}
|
|
2103
|
+
return lines.join("\n");
|
|
2104
|
+
}
|
|
2105
|
+
async function runPostAuthSteps(client, opts, results, appId) {
|
|
2106
|
+
const messengerBody = {};
|
|
2107
|
+
if (opts.companyName) messengerBody.name = opts.companyName;
|
|
2108
|
+
await client.put("/messenger", messengerBody, { "Intercom-Version": "Unstable" });
|
|
2109
|
+
results.push(`\u2713 Messenger configured${opts.companyName ? ` (${opts.companyName})` : ""}`);
|
|
2110
|
+
const helpCenterName = opts.companyName ? `${opts.companyName} Help` : "Help Center";
|
|
2111
|
+
const collections = await client.paginate("/help_center/collections");
|
|
2112
|
+
const existingCollection = collections.find(
|
|
2113
|
+
(c) => String(c.name ?? "") === helpCenterName
|
|
2114
|
+
);
|
|
2115
|
+
if (existingCollection) {
|
|
2116
|
+
results.push(`\u2713 Help center already exists (${helpCenterName})`);
|
|
2117
|
+
} else {
|
|
2118
|
+
await client.post("/help_center/collections", { name: helpCenterName });
|
|
2119
|
+
results.push(`\u2713 Help center created (${helpCenterName})`);
|
|
2120
|
+
}
|
|
2121
|
+
if (opts.articlesFrom) {
|
|
2122
|
+
let mdFiles = [];
|
|
2123
|
+
try {
|
|
2124
|
+
mdFiles = readdirSync(opts.articlesFrom).filter((f) => f.endsWith(".md"));
|
|
2125
|
+
} catch (err) {
|
|
2126
|
+
throw err instanceof CLIError ? err : new CLIError(`Failed to read --articles-from directory: ${err.message}`);
|
|
2127
|
+
}
|
|
2128
|
+
const existingArticles = await client.paginate("/articles");
|
|
2129
|
+
const existingTitles = new Set(existingArticles.map((a) => String(a.title ?? "")));
|
|
2130
|
+
let created = 0;
|
|
2131
|
+
let skipped = 0;
|
|
2132
|
+
for (const file of mdFiles) {
|
|
2133
|
+
const title = basename(file, ".md").replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
2134
|
+
const body = readFileSync3(join3(opts.articlesFrom, file), "utf-8");
|
|
2135
|
+
if (existingTitles.has(title)) {
|
|
2136
|
+
skipped++;
|
|
2137
|
+
} else {
|
|
2138
|
+
await client.post("/articles", { title, body, state: "published" });
|
|
2139
|
+
created++;
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2142
|
+
if (created > 0 && skipped > 0) {
|
|
2143
|
+
results.push(`\u2713 ${created} article(s) published, ${skipped} skipped (already exist)`);
|
|
2144
|
+
} else if (created > 0) {
|
|
2145
|
+
results.push(`\u2713 ${created} article(s) published`);
|
|
2146
|
+
} else {
|
|
2147
|
+
results.push(`\u2713 All ${skipped} article(s) already exist`);
|
|
2148
|
+
}
|
|
2149
|
+
}
|
|
2150
|
+
const articleCount = (await client.paginate("/articles")).length;
|
|
2151
|
+
if (articleCount === 0) {
|
|
2152
|
+
results.push(`\u26A0 No help center articles found!`);
|
|
2153
|
+
console.log(results.join("\n"));
|
|
2154
|
+
console.log("");
|
|
2155
|
+
console.log("WARNING: Fin needs help center articles to answer questions.");
|
|
2156
|
+
console.log("Without articles, Fin will have nothing to draw from and cannot help visitors.\n");
|
|
2157
|
+
console.log("To add articles, either:");
|
|
2158
|
+
console.log(" 1. Re-run with: intercom setup --articles-from <directory-of-markdown-files>");
|
|
2159
|
+
console.log(' 2. Create articles manually: intercom articles create -f title="..." -f body="..." -F author_id=<id>');
|
|
2160
|
+
console.log("");
|
|
2161
|
+
console.log("Tip for AI agents: if no articles directory exists, consider creating one by");
|
|
2162
|
+
console.log(" analyzing the project's source code, README, documentation, or any available");
|
|
2163
|
+
console.log(" context about the product. Write markdown files covering key topics that");
|
|
2164
|
+
console.log(" customers would ask about, then re-run setup with --articles-from.\n");
|
|
2165
|
+
console.log("Skipping Fin enablement \u2014 enable Fin after adding articles with: intercom fin enable --email <email>");
|
|
2166
|
+
return;
|
|
2167
|
+
}
|
|
2168
|
+
results.push(`\u2713 ${articleCount} article(s) in help center`);
|
|
2169
|
+
if (opts.enableFin !== false && opts.email) {
|
|
2170
|
+
try {
|
|
2171
|
+
const password = await resolvePassword({ passwordStdin: opts.passwordStdin });
|
|
2172
|
+
const finAppId = appId ?? "";
|
|
2173
|
+
const session = await getEmberSession({
|
|
2174
|
+
email: opts.email,
|
|
2175
|
+
password,
|
|
2176
|
+
appId: finAppId,
|
|
2177
|
+
verifyEmailUrl: opts.verifyEmailUrl
|
|
2178
|
+
});
|
|
2179
|
+
const optIn = await emberRequest(session, "/ember/fin_opt_in", {
|
|
2180
|
+
method: "POST",
|
|
2181
|
+
body: { app_id: finAppId }
|
|
2182
|
+
});
|
|
2183
|
+
if (optIn.status === 200) {
|
|
2184
|
+
results.push(`\u2713 Fin opted in`);
|
|
2185
|
+
} else if (optIn.status === 409) {
|
|
2186
|
+
results.push(`\u2713 Fin already opted in`);
|
|
2187
|
+
} else {
|
|
2188
|
+
results.push(`\u26A0 Fin opt-in failed (${optIn.status}) \u2014 enable manually with: intercom fin enable --email <email>`);
|
|
2189
|
+
console.log(results.join("\n"));
|
|
2190
|
+
console.log("Setup complete (Fin requires manual enablement).");
|
|
2191
|
+
return;
|
|
2192
|
+
}
|
|
2193
|
+
const profile = await emberRequest(
|
|
2194
|
+
session,
|
|
2195
|
+
`/ember/operator_workflows/fin_setup_behavior?app_id=${finAppId}`
|
|
2196
|
+
);
|
|
2197
|
+
if (profile.status === 200) {
|
|
2198
|
+
const profileData = profile.data;
|
|
2199
|
+
if (profileData.state === 1) {
|
|
2200
|
+
results.push(`\u2713 Fin already active`);
|
|
2201
|
+
} else {
|
|
2202
|
+
const activate = await emberRequest(
|
|
2203
|
+
session,
|
|
2204
|
+
`/ember/matching_system/rulesets/${profileData.id}/activate?app_id=${finAppId}`,
|
|
2205
|
+
{ method: "PUT", body: {} }
|
|
2206
|
+
);
|
|
2207
|
+
if (activate.status === 200) {
|
|
2208
|
+
results.push(`\u2713 Fin activated \u2014 answering conversations now`);
|
|
2209
|
+
} else {
|
|
2210
|
+
results.push(`\u26A0 Fin activation failed (${activate.status})`);
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
} else {
|
|
2214
|
+
results.push(`\u26A0 Could not get Fin profile (${profile.status})`);
|
|
2215
|
+
}
|
|
2216
|
+
} catch {
|
|
2217
|
+
results.push(`\u26A0 Fin enablement failed \u2014 enable manually with: intercom fin enable --email <email>`);
|
|
2218
|
+
}
|
|
2219
|
+
} else if (opts.enableFin !== false) {
|
|
2220
|
+
results.push(`\u26A0 Fin not enabled (--email required for Fin activation)`);
|
|
2221
|
+
console.log(" Tip: Re-run with --email to enable Fin, or use: intercom fin enable --email <email>");
|
|
2222
|
+
}
|
|
2223
|
+
try {
|
|
2224
|
+
const manifest = await client.get("/fin-sdk/manifest");
|
|
2225
|
+
const procedures = manifest.procedures ?? [];
|
|
2226
|
+
const connectors = manifest.data_connectors ?? [];
|
|
2227
|
+
const guidance = manifest.guidance ?? [];
|
|
2228
|
+
if (procedures.length > 0 || connectors.length > 0 || guidance.length > 0) {
|
|
2229
|
+
results.push(`\u2713 Fin configured (${procedures.length} procedures, ${connectors.length} data connectors, ${guidance.length} guidance)`);
|
|
2230
|
+
}
|
|
2231
|
+
} catch (err) {
|
|
2232
|
+
if (!(err instanceof PermissionError) && !(err instanceof NotFoundError)) {
|
|
2233
|
+
throw err;
|
|
2234
|
+
}
|
|
2235
|
+
}
|
|
2236
|
+
console.log(results.join("\n"));
|
|
2237
|
+
console.log("Setup complete!");
|
|
2238
|
+
}
|
|
2239
|
+
function registerSetupCommand(program) {
|
|
2240
|
+
program.command("setup").description("Set up a complete Intercom installation").option("--company-name <name>", "Company name for Messenger branding").option("--articles-from <dir>", "Directory of markdown files to create as articles").option("--plan", "Output setup steps as CLI commands without executing").option("--dry-run", "Show what would happen without executing").option("--json", "Output as JSON (for --plan mode)").option("--verbose", "Show HTTP request/response details").option("--email <email>", "Admin email address (for new workspace signup)").option("--password-stdin", "Read password from stdin (or set INTERCOM_PASSWORD env var)").option("--name <name>", "Admin name (for new workspace signup)").option("--plan-tier <tier>", "Trial plan tier: essential, advanced, expert (default: advanced)").option("--enable-fin", "Enable Fin AI Agent (default: true for greenfield setup)", true).option("--no-enable-fin", "Skip Fin enablement").option("--verify-email-url <url>", "Email verification URL (needed for Fin on fresh workspaces)").action(async function() {
|
|
2241
|
+
const opts = this.optsWithGlobals();
|
|
2242
|
+
if (hasPartialGreenfieldFlags(opts)) {
|
|
2243
|
+
throw new CLIError(
|
|
2244
|
+
"The --name flag is required for new workspace setup.\n Usage: intercom setup --email <email> --name <name> --password-stdin ...",
|
|
2245
|
+
1
|
|
2246
|
+
);
|
|
2247
|
+
}
|
|
2248
|
+
if (opts.plan) {
|
|
2249
|
+
const steps = buildPlan(opts);
|
|
2250
|
+
if (opts.json) {
|
|
2251
|
+
console.log(formatJson({ steps }));
|
|
2252
|
+
} else {
|
|
2253
|
+
console.log(formatPlanText(steps));
|
|
2254
|
+
}
|
|
2255
|
+
return;
|
|
2256
|
+
}
|
|
2257
|
+
if (opts.articlesFrom) {
|
|
2258
|
+
try {
|
|
2259
|
+
const stat = statSync(opts.articlesFrom);
|
|
2260
|
+
if (!stat.isDirectory()) {
|
|
2261
|
+
throw new CLIError(`--articles-from path is not a directory: ${opts.articlesFrom}`);
|
|
2262
|
+
}
|
|
2263
|
+
} catch (err) {
|
|
2264
|
+
if (err instanceof CLIError) throw err;
|
|
2265
|
+
throw new CLIError(`--articles-from directory not found: ${opts.articlesFrom}`);
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
2268
|
+
if (opts.dryRun) {
|
|
2269
|
+
const helpCenterName = opts.companyName ? `${opts.companyName} Help` : "Help Center";
|
|
2270
|
+
const lines = [];
|
|
2271
|
+
if (isGreenfield(opts)) {
|
|
2272
|
+
lines.push(`[dry-run] Would create workspace via signup (${opts.email})`);
|
|
2273
|
+
lines.push(`[dry-run] Would bootstrap authentication token`);
|
|
2274
|
+
}
|
|
2275
|
+
lines.push(`[dry-run] Would configure Messenger${opts.companyName ? ` with name "${opts.companyName}"` : ""}`);
|
|
2276
|
+
lines.push(`[dry-run] Would check for existing help center collection "${helpCenterName}"`);
|
|
2277
|
+
if (opts.articlesFrom) {
|
|
2278
|
+
let mdFiles = [];
|
|
2279
|
+
try {
|
|
2280
|
+
mdFiles = readdirSync(opts.articlesFrom).filter((f) => f.endsWith(".md"));
|
|
2281
|
+
} catch (err) {
|
|
2282
|
+
throw err instanceof CLIError ? err : new CLIError(`Failed to read --articles-from directory: ${err.message}`);
|
|
2283
|
+
}
|
|
2284
|
+
lines.push(`[dry-run] Would create ${mdFiles.length} article(s) from ${opts.articlesFrom}`);
|
|
2285
|
+
}
|
|
2286
|
+
lines.push(`[dry-run] Would check Fin SDK manifest and show configuration summary`);
|
|
2287
|
+
console.log(lines.join("\n"));
|
|
2288
|
+
return;
|
|
2289
|
+
}
|
|
2290
|
+
const results = [];
|
|
2291
|
+
if (isGreenfield(opts)) {
|
|
2292
|
+
const password = await resolvePassword({ passwordStdin: opts.passwordStdin });
|
|
2293
|
+
const baseUrl = getAppBaseUrl();
|
|
2294
|
+
const planTier = opts.planTier ?? "advanced";
|
|
2295
|
+
const companyName = opts.companyName ?? opts.name;
|
|
2296
|
+
const workspace = await signup({
|
|
2297
|
+
baseUrl,
|
|
2298
|
+
email: opts.email,
|
|
2299
|
+
name: opts.name,
|
|
2300
|
+
password,
|
|
2301
|
+
companyName,
|
|
2302
|
+
plan: planTier
|
|
2303
|
+
});
|
|
2304
|
+
results.push(`\u2713 Workspace created: ${workspace.name} (${workspace.id})`);
|
|
2305
|
+
let token;
|
|
2306
|
+
try {
|
|
2307
|
+
const result = await getToken({
|
|
2308
|
+
baseUrl,
|
|
2309
|
+
email: opts.email,
|
|
2310
|
+
password,
|
|
2311
|
+
appId: workspace.id
|
|
2312
|
+
});
|
|
2313
|
+
token = result.token;
|
|
2314
|
+
} catch (err) {
|
|
2315
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2316
|
+
console.error(`\u2713 Workspace created: ${workspace.name} (${workspace.id})`);
|
|
2317
|
+
console.error(`
|
|
2318
|
+
Authentication failed after workspace creation: ${message}`);
|
|
2319
|
+
console.error(`
|
|
2320
|
+
Your workspace exists but credentials were not saved.`);
|
|
2321
|
+
console.error(`To complete setup, run:
|
|
2322
|
+
`);
|
|
2323
|
+
console.error(` INTERCOM_PASSWORD=*** intercom auth bootstrap --email ${opts.email} --app-id ${workspace.id}`);
|
|
2324
|
+
console.error(`
|
|
2325
|
+
Then re-run setup without signup flags:
|
|
2326
|
+
`);
|
|
2327
|
+
console.error(` intercom setup --company-name "${companyName}"`);
|
|
2328
|
+
throw new CLIError(
|
|
2329
|
+
`Authentication failed after workspace creation. Run "intercom auth bootstrap --email ${opts.email} --app-id ${workspace.id}" to recover.`,
|
|
2330
|
+
1
|
|
2331
|
+
);
|
|
2332
|
+
}
|
|
2333
|
+
results.push(`\u2713 Authentication token obtained`);
|
|
2334
|
+
const configDir = process.env.INTERCOM_CONFIG_DIR;
|
|
2335
|
+
const store = new CredentialStore(createDefaultBackend(configDir));
|
|
2336
|
+
await store.set(workspace.id, { token, auth_type: "oauth", app_id: workspace.id });
|
|
2337
|
+
await addWorkspace(configDir, { app_id: workspace.id, name: workspace.name });
|
|
2338
|
+
const client2 = new IntercomClient({
|
|
2339
|
+
token,
|
|
2340
|
+
authType: "oauth",
|
|
2341
|
+
appId: workspace.id,
|
|
2342
|
+
baseUrl: process.env.INTERCOM_API_BASE_URL || void 0,
|
|
2343
|
+
verbose: opts.verbose
|
|
2344
|
+
});
|
|
2345
|
+
await runPostAuthSteps(client2, opts, results, workspace.id);
|
|
2346
|
+
return;
|
|
2347
|
+
}
|
|
2348
|
+
const client = await resolveClient(opts);
|
|
2349
|
+
const me = await client.get("/me");
|
|
2350
|
+
const resolvedAppId = me.app?.id_code;
|
|
2351
|
+
await runPostAuthSteps(client, opts, results, resolvedAppId);
|
|
2352
|
+
});
|
|
2353
|
+
}
|
|
2354
|
+
|
|
2355
|
+
// src/index.ts
|
|
2356
|
+
function createProgram() {
|
|
2357
|
+
const program = new Command();
|
|
2358
|
+
program.name("intercom").description("CLI tool for managing Intercom workspaces").version("0.1.0").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");
|
|
2359
|
+
registerApiCommand(program);
|
|
2360
|
+
registerAuthCommand(program);
|
|
2361
|
+
registerMeCommand(program);
|
|
2362
|
+
registerArticlesCommand(program);
|
|
2363
|
+
registerContactsCommand(program);
|
|
2364
|
+
registerConversationsCommand(program);
|
|
2365
|
+
registerHelpCenterCommand(program);
|
|
2366
|
+
registerMessengerCommand(program);
|
|
2367
|
+
registerFinCommand(program);
|
|
2368
|
+
registerSetupCommand(program);
|
|
2369
|
+
return program;
|
|
2370
|
+
}
|
|
2371
|
+
function handleError(err) {
|
|
2372
|
+
const isJson = process.argv.includes("--json");
|
|
2373
|
+
if (err instanceof CLIError) {
|
|
2374
|
+
if (isJson) {
|
|
2375
|
+
console.error(formatJson({
|
|
2376
|
+
error: {
|
|
2377
|
+
code: err instanceof AuthError ? "auth_error" : err instanceof PermissionError ? "permission_denied" : err instanceof RateLimitError ? "rate_limited" : err instanceof NotFoundError ? "not_found" : err instanceof TimeoutError ? "timeout" : "error",
|
|
2378
|
+
message: err.message,
|
|
2379
|
+
...err instanceof RateLimitError ? { retry_after: err.retryAfter } : {}
|
|
2380
|
+
}
|
|
2381
|
+
}));
|
|
2382
|
+
} else {
|
|
2383
|
+
console.error(`Error: ${err.message}`);
|
|
2384
|
+
}
|
|
2385
|
+
process.exit(err.exitCode);
|
|
2386
|
+
}
|
|
2387
|
+
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
2388
|
+
process.exit(1);
|
|
2389
|
+
}
|
|
2390
|
+
var isMainModule = process.argv[1]?.endsWith("index.js") || process.argv[1]?.endsWith("intercom");
|
|
2391
|
+
if (isMainModule) {
|
|
2392
|
+
const program = createProgram();
|
|
2393
|
+
try {
|
|
2394
|
+
await program.parseAsync();
|
|
2395
|
+
} catch (err) {
|
|
2396
|
+
handleError(err);
|
|
2397
|
+
}
|
|
2398
|
+
}
|
|
2399
|
+
export {
|
|
2400
|
+
createProgram,
|
|
2401
|
+
handleError
|
|
2402
|
+
};
|
|
2403
|
+
//# sourceMappingURL=index.js.map
|