attio-cli 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/attio.js +128 -28
- package/package.json +3 -2
package/dist/attio.js
CHANGED
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
// bin/attio.ts
|
|
4
4
|
import { program } from "commander";
|
|
5
5
|
import chalk7 from "chalk";
|
|
6
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
7
|
+
import { dirname, join as join2 } from "path";
|
|
8
|
+
import { fileURLToPath } from "url";
|
|
6
9
|
|
|
7
10
|
// src/errors.ts
|
|
8
11
|
import chalk from "chalk";
|
|
@@ -57,7 +60,7 @@ var AttioRateLimitError = class extends Error {
|
|
|
57
60
|
import chalk2 from "chalk";
|
|
58
61
|
|
|
59
62
|
// src/config.ts
|
|
60
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
63
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync } from "fs";
|
|
61
64
|
import { join } from "path";
|
|
62
65
|
import { homedir } from "os";
|
|
63
66
|
import * as dotenv from "dotenv";
|
|
@@ -73,8 +76,16 @@ function loadConfig() {
|
|
|
73
76
|
}
|
|
74
77
|
}
|
|
75
78
|
function saveConfig(config2) {
|
|
76
|
-
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
77
|
-
|
|
79
|
+
mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
|
|
80
|
+
try {
|
|
81
|
+
chmodSync(CONFIG_DIR, 448);
|
|
82
|
+
} catch {
|
|
83
|
+
}
|
|
84
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(config2, null, 2), { mode: 384 });
|
|
85
|
+
try {
|
|
86
|
+
chmodSync(CONFIG_FILE, 384);
|
|
87
|
+
} catch {
|
|
88
|
+
}
|
|
78
89
|
}
|
|
79
90
|
function resolveApiKey(flagValue) {
|
|
80
91
|
return flagValue || process.env.ATTIO_API_KEY || loadConfig().apiKey || "";
|
|
@@ -96,6 +107,20 @@ var BASE_URL = "https://api.attio.com/v2";
|
|
|
96
107
|
var MAX_RETRIES = 3;
|
|
97
108
|
var INITIAL_BACKOFF_MS = 1e3;
|
|
98
109
|
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
110
|
+
function getRetryDelayMs(response, attempt) {
|
|
111
|
+
const retryAfter = response.headers.get("retry-after");
|
|
112
|
+
if (retryAfter) {
|
|
113
|
+
const asSeconds = Number(retryAfter);
|
|
114
|
+
if (!Number.isNaN(asSeconds)) {
|
|
115
|
+
return Math.max(0, Math.ceil(asSeconds * 1e3));
|
|
116
|
+
}
|
|
117
|
+
const asDate = Date.parse(retryAfter);
|
|
118
|
+
if (!Number.isNaN(asDate)) {
|
|
119
|
+
return Math.max(0, asDate - Date.now());
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return INITIAL_BACKOFF_MS * Math.pow(2, attempt);
|
|
123
|
+
}
|
|
99
124
|
var AttioClient = class {
|
|
100
125
|
apiKey;
|
|
101
126
|
debug;
|
|
@@ -141,7 +166,10 @@ var AttioClient = class {
|
|
|
141
166
|
}
|
|
142
167
|
if (response.status === 429) {
|
|
143
168
|
if (attempt < MAX_RETRIES) {
|
|
144
|
-
const backoff =
|
|
169
|
+
const backoff = Math.max(100, getRetryDelayMs(response, attempt));
|
|
170
|
+
if (this.debug) {
|
|
171
|
+
console.error(chalk2.dim(` retrying in ${backoff}ms`));
|
|
172
|
+
}
|
|
145
173
|
await new Promise((resolve) => setTimeout(resolve, backoff));
|
|
146
174
|
continue;
|
|
147
175
|
}
|
|
@@ -193,6 +221,43 @@ var AttioClient = class {
|
|
|
193
221
|
import chalk3 from "chalk";
|
|
194
222
|
import Table from "cli-table3";
|
|
195
223
|
import { createInterface } from "readline";
|
|
224
|
+
var ID_KEY_PRIORITY = [
|
|
225
|
+
"record_id",
|
|
226
|
+
"entry_id",
|
|
227
|
+
"task_id",
|
|
228
|
+
"note_id",
|
|
229
|
+
"comment_id",
|
|
230
|
+
"thread_id",
|
|
231
|
+
"workspace_member_id",
|
|
232
|
+
"list_id",
|
|
233
|
+
"object_id",
|
|
234
|
+
"webhook_id",
|
|
235
|
+
"workspace_id"
|
|
236
|
+
];
|
|
237
|
+
function toIdString(value) {
|
|
238
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "bigint") {
|
|
239
|
+
const id = String(value);
|
|
240
|
+
return id.length > 0 ? id : "";
|
|
241
|
+
}
|
|
242
|
+
return "";
|
|
243
|
+
}
|
|
244
|
+
function isRecord(value) {
|
|
245
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
246
|
+
}
|
|
247
|
+
function extractOutputId(raw) {
|
|
248
|
+
const direct = toIdString(raw);
|
|
249
|
+
if (direct) return direct;
|
|
250
|
+
if (!isRecord(raw)) return "";
|
|
251
|
+
for (const key of ID_KEY_PRIORITY) {
|
|
252
|
+
const id = toIdString(raw[key]);
|
|
253
|
+
if (id) return id;
|
|
254
|
+
}
|
|
255
|
+
for (const value of Object.values(raw)) {
|
|
256
|
+
const fallback = toIdString(value);
|
|
257
|
+
if (fallback) return fallback;
|
|
258
|
+
}
|
|
259
|
+
return "";
|
|
260
|
+
}
|
|
196
261
|
function detectFormat(opts) {
|
|
197
262
|
if (opts.quiet) return "quiet";
|
|
198
263
|
if (opts.json) return "json";
|
|
@@ -205,8 +270,8 @@ function outputList(items, opts) {
|
|
|
205
270
|
const idField = opts.idField || "id";
|
|
206
271
|
if (format === "quiet") {
|
|
207
272
|
for (const item of items) {
|
|
208
|
-
const raw = typeof item === "string" ? item : item[idField]
|
|
209
|
-
const id =
|
|
273
|
+
const raw = typeof item === "string" ? item : item[idField] ?? "";
|
|
274
|
+
const id = extractOutputId(raw);
|
|
210
275
|
if (id) console.log(id);
|
|
211
276
|
}
|
|
212
277
|
return;
|
|
@@ -245,8 +310,8 @@ function outputList(items, opts) {
|
|
|
245
310
|
}
|
|
246
311
|
function outputSingle(item, opts) {
|
|
247
312
|
if (opts.format === "quiet") {
|
|
248
|
-
const raw = item[opts.idField || "id"]
|
|
249
|
-
const id =
|
|
313
|
+
const raw = item[opts.idField || "id"] ?? "";
|
|
314
|
+
const id = extractOutputId(raw);
|
|
250
315
|
console.log(id || "");
|
|
251
316
|
return;
|
|
252
317
|
}
|
|
@@ -443,6 +508,20 @@ function flattenRecord(record) {
|
|
|
443
508
|
}
|
|
444
509
|
return flat;
|
|
445
510
|
}
|
|
511
|
+
function stripWrappingQuotes(raw) {
|
|
512
|
+
if (raw.startsWith('"') && raw.endsWith('"') || raw.startsWith("'") && raw.endsWith("'")) {
|
|
513
|
+
return raw.slice(1, -1);
|
|
514
|
+
}
|
|
515
|
+
return raw;
|
|
516
|
+
}
|
|
517
|
+
function parseScalar(raw) {
|
|
518
|
+
const normalized = stripWrappingQuotes(raw.trim());
|
|
519
|
+
if (normalized === "true") return true;
|
|
520
|
+
if (normalized === "false") return false;
|
|
521
|
+
if (normalized === "null") return null;
|
|
522
|
+
if (/^-?\d+(\.\d+)?$/.test(normalized)) return Number(normalized);
|
|
523
|
+
return normalized;
|
|
524
|
+
}
|
|
446
525
|
function parseSets(sets) {
|
|
447
526
|
const values = {};
|
|
448
527
|
for (const set of sets) {
|
|
@@ -450,23 +529,31 @@ function parseSets(sets) {
|
|
|
450
529
|
if (eqIdx === -1) throw new Error(`Invalid --set format: "${set}". Expected: key=value`);
|
|
451
530
|
const key = set.slice(0, eqIdx).trim();
|
|
452
531
|
const raw = set.slice(eqIdx + 1).trim();
|
|
453
|
-
if (raw
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
continue;
|
|
460
|
-
}
|
|
461
|
-
if (/^-?\d+(\.\d+)?$/.test(raw)) {
|
|
462
|
-
values[key] = Number(raw);
|
|
463
|
-
continue;
|
|
532
|
+
if (raw.startsWith("{") && raw.endsWith("}")) {
|
|
533
|
+
try {
|
|
534
|
+
values[key] = JSON.parse(raw);
|
|
535
|
+
continue;
|
|
536
|
+
} catch {
|
|
537
|
+
}
|
|
464
538
|
}
|
|
465
539
|
if (raw.startsWith("[") && raw.endsWith("]")) {
|
|
466
|
-
|
|
540
|
+
try {
|
|
541
|
+
const parsed = JSON.parse(raw);
|
|
542
|
+
if (Array.isArray(parsed)) {
|
|
543
|
+
values[key] = parsed;
|
|
544
|
+
continue;
|
|
545
|
+
}
|
|
546
|
+
} catch {
|
|
547
|
+
}
|
|
548
|
+
const inner = raw.slice(1, -1).trim();
|
|
549
|
+
if (inner === "") {
|
|
550
|
+
values[key] = [];
|
|
551
|
+
continue;
|
|
552
|
+
}
|
|
553
|
+
values[key] = inner.split(",").map((s) => parseScalar(s));
|
|
467
554
|
continue;
|
|
468
555
|
}
|
|
469
|
-
values[key] = raw;
|
|
556
|
+
values[key] = parseScalar(raw);
|
|
470
557
|
}
|
|
471
558
|
return values;
|
|
472
559
|
}
|
|
@@ -481,8 +568,8 @@ async function readStdin() {
|
|
|
481
568
|
async function resolveValues(options) {
|
|
482
569
|
if (options.values) {
|
|
483
570
|
if (options.values.startsWith("@")) {
|
|
484
|
-
const { readFileSync:
|
|
485
|
-
return JSON.parse(
|
|
571
|
+
const { readFileSync: readFileSync3 } = await import("fs");
|
|
572
|
+
return JSON.parse(readFileSync3(options.values.slice(1), "utf-8"));
|
|
486
573
|
}
|
|
487
574
|
return JSON.parse(options.values);
|
|
488
575
|
}
|
|
@@ -507,7 +594,7 @@ async function paginate(fetchPage, options) {
|
|
|
507
594
|
return fetchPage(options.limit, options.offset);
|
|
508
595
|
}
|
|
509
596
|
const allResults = [];
|
|
510
|
-
let offset =
|
|
597
|
+
let offset = options.offset;
|
|
511
598
|
const pageSize = 500;
|
|
512
599
|
while (true) {
|
|
513
600
|
const page = await fetchPage(pageSize, offset);
|
|
@@ -1398,10 +1485,11 @@ import chalk5 from "chalk";
|
|
|
1398
1485
|
function register14(program2) {
|
|
1399
1486
|
program2.command("open <object> [record-id]").description("Open an object or record in the Attio web app").action(async (object, recordId, options, command) => {
|
|
1400
1487
|
const opts = command.optsWithGlobals();
|
|
1488
|
+
const objectSlug = encodeURIComponent(object);
|
|
1401
1489
|
let url;
|
|
1402
1490
|
if (recordId) {
|
|
1403
1491
|
const client = new AttioClient(opts.apiKey, opts.debug);
|
|
1404
|
-
const res = await client.get(`/objects/${
|
|
1492
|
+
const res = await client.get(`/objects/${objectSlug}/records/${encodeURIComponent(recordId)}`);
|
|
1405
1493
|
const record = res.data;
|
|
1406
1494
|
url = record.web_url;
|
|
1407
1495
|
if (!url) {
|
|
@@ -1412,9 +1500,9 @@ function register14(program2) {
|
|
|
1412
1500
|
const client = new AttioClient(opts.apiKey, opts.debug);
|
|
1413
1501
|
const self = await client.get("/self");
|
|
1414
1502
|
const slug = self.workspace_slug || "";
|
|
1415
|
-
url = `https://app.attio.com/${slug}/${
|
|
1503
|
+
url = `https://app.attio.com/${encodeURIComponent(slug)}/${objectSlug}`;
|
|
1416
1504
|
}
|
|
1417
|
-
const cmd = platform() === "darwin" ? "open" : platform() === "win32" ? "
|
|
1505
|
+
const cmd = platform() === "darwin" ? "open" : platform() === "win32" ? "explorer" : "xdg-open";
|
|
1418
1506
|
execFile(cmd, [url], (err) => {
|
|
1419
1507
|
if (err) {
|
|
1420
1508
|
console.log(url);
|
|
@@ -1515,6 +1603,18 @@ function register15(program2) {
|
|
|
1515
1603
|
}
|
|
1516
1604
|
|
|
1517
1605
|
// bin/attio.ts
|
|
1606
|
+
function loadCliVersion() {
|
|
1607
|
+
try {
|
|
1608
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
1609
|
+
const packagePath = join2(here, "..", "package.json");
|
|
1610
|
+
const packageJson = JSON.parse(readFileSync2(packagePath, "utf-8"));
|
|
1611
|
+
if (typeof packageJson.version === "string" && packageJson.version.length > 0) {
|
|
1612
|
+
return packageJson.version;
|
|
1613
|
+
}
|
|
1614
|
+
} catch {
|
|
1615
|
+
}
|
|
1616
|
+
return "0.2.0";
|
|
1617
|
+
}
|
|
1518
1618
|
function handleError(err) {
|
|
1519
1619
|
const jsonMode = program.opts().json || !process.stdout.isTTY;
|
|
1520
1620
|
if (err instanceof AttioApiError) {
|
|
@@ -1559,7 +1659,7 @@ function handleError(err) {
|
|
|
1559
1659
|
}
|
|
1560
1660
|
process.exit(1);
|
|
1561
1661
|
}
|
|
1562
|
-
program.name("attio").version(
|
|
1662
|
+
program.name("attio").version(loadCliVersion()).description("CLI for the Attio CRM API. Built for scripts, agents, and humans who prefer terminals.").option("--api-key <key>", "Override API key").option("--json", "Force JSON output").option("--table", "Force table output").option("--csv", "Force CSV output").option("-q, --quiet", "Only output IDs").option("--no-color", "Disable colors").option("--debug", "Print request/response details to stderr");
|
|
1563
1663
|
if (process.argv.includes("--no-color")) {
|
|
1564
1664
|
process.env.NO_COLOR = "1";
|
|
1565
1665
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "attio-cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "CLI for the Attio CRM API. Built for scripts, agents, and humans who prefer terminals.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
],
|
|
13
13
|
"scripts": {
|
|
14
14
|
"build": "tsup",
|
|
15
|
-
"dev": "tsx bin/attio.ts"
|
|
15
|
+
"dev": "tsx bin/attio.ts",
|
|
16
|
+
"test": "node --import tsx --test tests/**/*.test.ts"
|
|
16
17
|
},
|
|
17
18
|
"dependencies": {
|
|
18
19
|
"commander": "^12.0.0",
|