attio-cli 0.1.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/README.md +21 -6
- package/dist/attio.js +435 -62
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -11,19 +11,34 @@ npm install -g attio-cli
|
|
|
11
11
|
## Quick Start
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
|
-
|
|
15
|
-
#
|
|
16
|
-
attio config set api-key your_key
|
|
17
|
-
|
|
18
|
-
attio whoami
|
|
19
|
-
attio objects list
|
|
14
|
+
attio init # guided setup — paste your API key, done
|
|
15
|
+
attio whoami # verify connection
|
|
20
16
|
attio people list --limit 5
|
|
21
17
|
```
|
|
22
18
|
|
|
19
|
+
You'll need an API key from [Attio Developer Settings](https://app.attio.com/settings/developers). `attio init` walks you through the rest.
|
|
20
|
+
|
|
21
|
+
For non-interactive environments (CI, scripts), use any of:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
attio init --api-key <key> # validates and saves
|
|
25
|
+
export ATTIO_API_KEY=<key> # env var (takes precedence)
|
|
26
|
+
attio config set api-key <key> # direct config write
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Agent Setup
|
|
30
|
+
|
|
31
|
+
To let AI agents (Claude, etc.) discover this CLI, append the auto-generated snippet to your project's `CLAUDE.md`:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
attio config claude-md >> CLAUDE.md
|
|
35
|
+
```
|
|
36
|
+
|
|
23
37
|
## Command Reference
|
|
24
38
|
|
|
25
39
|
| Command | Description |
|
|
26
40
|
|---------|-------------|
|
|
41
|
+
| `attio init` | Interactive setup wizard — connect to your Attio workspace |
|
|
27
42
|
| `attio whoami` | Show current workspace and user info |
|
|
28
43
|
| **Objects** | |
|
|
29
44
|
| `attio objects list` | List all objects in the workspace |
|
package/dist/attio.js
CHANGED
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
// bin/attio.ts
|
|
4
4
|
import { program } from "commander";
|
|
5
|
-
import
|
|
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 || "";
|
|
@@ -87,11 +98,29 @@ function setApiKey(key) {
|
|
|
87
98
|
function getConfigPath() {
|
|
88
99
|
return CONFIG_FILE;
|
|
89
100
|
}
|
|
101
|
+
function isConfigured() {
|
|
102
|
+
return resolveApiKey() !== "";
|
|
103
|
+
}
|
|
90
104
|
|
|
91
105
|
// src/client.ts
|
|
92
106
|
var BASE_URL = "https://api.attio.com/v2";
|
|
93
107
|
var MAX_RETRIES = 3;
|
|
94
108
|
var INITIAL_BACKOFF_MS = 1e3;
|
|
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
|
+
}
|
|
95
124
|
var AttioClient = class {
|
|
96
125
|
apiKey;
|
|
97
126
|
debug;
|
|
@@ -103,6 +132,9 @@ var AttioClient = class {
|
|
|
103
132
|
const url = `${BASE_URL}${path}`;
|
|
104
133
|
if (this.debug) {
|
|
105
134
|
console.error(chalk2.dim(`\u2192 ${method} ${url}`));
|
|
135
|
+
if (body !== void 0) {
|
|
136
|
+
console.error(chalk2.dim(` body: ${JSON.stringify(body)}`));
|
|
137
|
+
}
|
|
106
138
|
}
|
|
107
139
|
if (!this.apiKey) {
|
|
108
140
|
throw new AttioAuthError("No API key configured");
|
|
@@ -116,13 +148,28 @@ var AttioClient = class {
|
|
|
116
148
|
init.body = JSON.stringify(body);
|
|
117
149
|
}
|
|
118
150
|
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
119
|
-
const
|
|
151
|
+
const controller = new AbortController();
|
|
152
|
+
const timer = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT_MS);
|
|
153
|
+
let response;
|
|
154
|
+
try {
|
|
155
|
+
response = await fetch(url, { ...init, signal: controller.signal });
|
|
156
|
+
} catch (err) {
|
|
157
|
+
clearTimeout(timer);
|
|
158
|
+
if (err?.name === "AbortError") {
|
|
159
|
+
throw new Error(`Request timed out after ${DEFAULT_TIMEOUT_MS / 1e3}s: ${method} ${path}`);
|
|
160
|
+
}
|
|
161
|
+
throw err;
|
|
162
|
+
}
|
|
163
|
+
clearTimeout(timer);
|
|
120
164
|
if (this.debug) {
|
|
121
|
-
console.error(chalk2.dim(`\u2190 ${response.status}`));
|
|
165
|
+
console.error(chalk2.dim(`\u2190 ${response.status} ${response.statusText}`));
|
|
122
166
|
}
|
|
123
167
|
if (response.status === 429) {
|
|
124
168
|
if (attempt < MAX_RETRIES) {
|
|
125
|
-
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
|
+
}
|
|
126
173
|
await new Promise((resolve) => setTimeout(resolve, backoff));
|
|
127
174
|
continue;
|
|
128
175
|
}
|
|
@@ -135,6 +182,9 @@ var AttioClient = class {
|
|
|
135
182
|
return void 0;
|
|
136
183
|
}
|
|
137
184
|
const json = await response.json();
|
|
185
|
+
if (this.debug && !response.ok) {
|
|
186
|
+
console.error(chalk2.dim(` error: ${JSON.stringify(json)}`));
|
|
187
|
+
}
|
|
138
188
|
if (!response.ok) {
|
|
139
189
|
const errorType = json?.type ?? "unknown_error";
|
|
140
190
|
let errorDetail = json?.message ?? json?.detail ?? response.statusText;
|
|
@@ -171,6 +221,43 @@ var AttioClient = class {
|
|
|
171
221
|
import chalk3 from "chalk";
|
|
172
222
|
import Table from "cli-table3";
|
|
173
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
|
+
}
|
|
174
261
|
function detectFormat(opts) {
|
|
175
262
|
if (opts.quiet) return "quiet";
|
|
176
263
|
if (opts.json) return "json";
|
|
@@ -183,8 +270,8 @@ function outputList(items, opts) {
|
|
|
183
270
|
const idField = opts.idField || "id";
|
|
184
271
|
if (format === "quiet") {
|
|
185
272
|
for (const item of items) {
|
|
186
|
-
const raw = typeof item === "string" ? item : item[idField]
|
|
187
|
-
const id =
|
|
273
|
+
const raw = typeof item === "string" ? item : item[idField] ?? "";
|
|
274
|
+
const id = extractOutputId(raw);
|
|
188
275
|
if (id) console.log(id);
|
|
189
276
|
}
|
|
190
277
|
return;
|
|
@@ -223,8 +310,8 @@ function outputList(items, opts) {
|
|
|
223
310
|
}
|
|
224
311
|
function outputSingle(item, opts) {
|
|
225
312
|
if (opts.format === "quiet") {
|
|
226
|
-
const raw = item[opts.idField || "id"]
|
|
227
|
-
const id =
|
|
313
|
+
const raw = item[opts.idField || "id"] ?? "";
|
|
314
|
+
const id = extractOutputId(raw);
|
|
228
315
|
console.log(id || "");
|
|
229
316
|
return;
|
|
230
317
|
}
|
|
@@ -240,6 +327,11 @@ function outputSingle(item, opts) {
|
|
|
240
327
|
console.log(table.toString());
|
|
241
328
|
}
|
|
242
329
|
async function confirm(message) {
|
|
330
|
+
if (!process.stdin.isTTY) {
|
|
331
|
+
console.error(`${message} [y/N]`);
|
|
332
|
+
console.error("Error: Confirmation required but stdin is not a TTY. Use --yes to skip.");
|
|
333
|
+
return false;
|
|
334
|
+
}
|
|
243
335
|
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
244
336
|
return new Promise((resolve) => {
|
|
245
337
|
rl.question(`${message} [y/N] `, (answer) => {
|
|
@@ -416,6 +508,20 @@ function flattenRecord(record) {
|
|
|
416
508
|
}
|
|
417
509
|
return flat;
|
|
418
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
|
+
}
|
|
419
525
|
function parseSets(sets) {
|
|
420
526
|
const values = {};
|
|
421
527
|
for (const set of sets) {
|
|
@@ -423,23 +529,31 @@ function parseSets(sets) {
|
|
|
423
529
|
if (eqIdx === -1) throw new Error(`Invalid --set format: "${set}". Expected: key=value`);
|
|
424
530
|
const key = set.slice(0, eqIdx).trim();
|
|
425
531
|
const raw = set.slice(eqIdx + 1).trim();
|
|
426
|
-
if (raw
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
continue;
|
|
433
|
-
}
|
|
434
|
-
if (/^-?\d+(\.\d+)?$/.test(raw)) {
|
|
435
|
-
values[key] = Number(raw);
|
|
436
|
-
continue;
|
|
532
|
+
if (raw.startsWith("{") && raw.endsWith("}")) {
|
|
533
|
+
try {
|
|
534
|
+
values[key] = JSON.parse(raw);
|
|
535
|
+
continue;
|
|
536
|
+
} catch {
|
|
537
|
+
}
|
|
437
538
|
}
|
|
438
539
|
if (raw.startsWith("[") && raw.endsWith("]")) {
|
|
439
|
-
|
|
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));
|
|
440
554
|
continue;
|
|
441
555
|
}
|
|
442
|
-
values[key] = raw;
|
|
556
|
+
values[key] = parseScalar(raw);
|
|
443
557
|
}
|
|
444
558
|
return values;
|
|
445
559
|
}
|
|
@@ -454,8 +568,8 @@ async function readStdin() {
|
|
|
454
568
|
async function resolveValues(options) {
|
|
455
569
|
if (options.values) {
|
|
456
570
|
if (options.values.startsWith("@")) {
|
|
457
|
-
const { readFileSync:
|
|
458
|
-
return JSON.parse(
|
|
571
|
+
const { readFileSync: readFileSync3 } = await import("fs");
|
|
572
|
+
return JSON.parse(readFileSync3(options.values.slice(1), "utf-8"));
|
|
459
573
|
}
|
|
460
574
|
return JSON.parse(options.values);
|
|
461
575
|
}
|
|
@@ -466,20 +580,31 @@ async function resolveValues(options) {
|
|
|
466
580
|
if (stdin.trim()) return JSON.parse(stdin);
|
|
467
581
|
return {};
|
|
468
582
|
}
|
|
583
|
+
function requireValues(values) {
|
|
584
|
+
if (Object.keys(values).length === 0) {
|
|
585
|
+
throw new Error(`No values provided. Use --set key=value, --values '{"key":"value"}', or pipe JSON to stdin.`);
|
|
586
|
+
}
|
|
587
|
+
return values;
|
|
588
|
+
}
|
|
469
589
|
|
|
470
590
|
// src/pagination.ts
|
|
591
|
+
var MAX_ALL_RECORDS = 1e4;
|
|
471
592
|
async function paginate(fetchPage, options) {
|
|
472
593
|
if (!options.all) {
|
|
473
594
|
return fetchPage(options.limit, options.offset);
|
|
474
595
|
}
|
|
475
596
|
const allResults = [];
|
|
476
|
-
let offset =
|
|
597
|
+
let offset = options.offset;
|
|
477
598
|
const pageSize = 500;
|
|
478
599
|
while (true) {
|
|
479
600
|
const page = await fetchPage(pageSize, offset);
|
|
480
601
|
allResults.push(...page);
|
|
481
602
|
if (page.length < pageSize) break;
|
|
482
603
|
offset += pageSize;
|
|
604
|
+
if (allResults.length >= MAX_ALL_RECORDS) {
|
|
605
|
+
console.error(`Warning: --all stopped after ${MAX_ALL_RECORDS} records. Use --limit and --offset for manual pagination.`);
|
|
606
|
+
break;
|
|
607
|
+
}
|
|
483
608
|
}
|
|
484
609
|
return allResults;
|
|
485
610
|
}
|
|
@@ -548,7 +673,7 @@ async function getRecord(object, recordId, cmdOpts) {
|
|
|
548
673
|
async function createRecord(object, cmdOpts) {
|
|
549
674
|
const client = new AttioClient(cmdOpts.apiKey, cmdOpts.debug);
|
|
550
675
|
const format = detectFormat(cmdOpts);
|
|
551
|
-
const values = await resolveValues(cmdOpts);
|
|
676
|
+
const values = requireValues(await resolveValues(cmdOpts));
|
|
552
677
|
const res = await client.post(
|
|
553
678
|
`/objects/${encodeURIComponent(object)}/records`,
|
|
554
679
|
{ data: { values } }
|
|
@@ -567,7 +692,7 @@ async function createRecord(object, cmdOpts) {
|
|
|
567
692
|
async function updateRecord(object, recordId, cmdOpts) {
|
|
568
693
|
const client = new AttioClient(cmdOpts.apiKey, cmdOpts.debug);
|
|
569
694
|
const format = detectFormat(cmdOpts);
|
|
570
|
-
const values = await resolveValues(cmdOpts);
|
|
695
|
+
const values = requireValues(await resolveValues(cmdOpts));
|
|
571
696
|
const res = await client.patch(
|
|
572
697
|
`/objects/${encodeURIComponent(object)}/records/${encodeURIComponent(recordId)}`,
|
|
573
698
|
{ data: { values } }
|
|
@@ -605,7 +730,7 @@ async function upsertRecord(object, cmdOpts) {
|
|
|
605
730
|
if (!matchAttr) {
|
|
606
731
|
throw new Error("--match <attribute-slug> is required for upsert");
|
|
607
732
|
}
|
|
608
|
-
const values = await resolveValues(cmdOpts);
|
|
733
|
+
const values = requireValues(await resolveValues(cmdOpts));
|
|
609
734
|
const res = await client.put(
|
|
610
735
|
`/objects/${encodeURIComponent(object)}/records?matching_attribute=${encodeURIComponent(matchAttr)}`,
|
|
611
736
|
{ data: { values } }
|
|
@@ -652,7 +777,7 @@ function register4(program2) {
|
|
|
652
777
|
const records = program2.command("records").description("Manage records in any Attio object");
|
|
653
778
|
records.command("list").description("List or query records for an object").argument("<object>", "Object slug or ID (e.g. companies, people)").option(
|
|
654
779
|
"--filter <expr>",
|
|
655
|
-
'Filter
|
|
780
|
+
'Filter: = != ~ !~ ^ > >= < <= ? (e.g. "name~Acme", "revenue>=1000", "email?"). Repeatable',
|
|
656
781
|
(val, prev) => [...prev, val],
|
|
657
782
|
[]
|
|
658
783
|
).option("--filter-json <json>", "Raw JSON filter (overrides --filter)").option(
|
|
@@ -708,7 +833,7 @@ function register4(program2) {
|
|
|
708
833
|
// src/commands/people.ts
|
|
709
834
|
function register5(program2) {
|
|
710
835
|
const cmd = program2.command("people").description("Manage people records (shortcut for: records <cmd> people)");
|
|
711
|
-
cmd.command("list").description("List people").option("--filter <expr>",
|
|
836
|
+
cmd.command("list").description("List people").option("--filter <expr>", 'Filter: = != ~ !~ ^ > >= < <= ? (e.g. "name~Acme"). Repeatable', (v, p) => [...p, v], []).option("--filter-json <json>", "Raw JSON filter").option("--sort <expr>", "Sort expression (repeatable)", (v, p) => [...p, v], []).option("--limit <n>", "Max results per page", "25").option("--offset <n>", "Starting offset", "0").option("--all", "Fetch all pages").action(async (options, command) => {
|
|
712
837
|
await listRecords("people", command.optsWithGlobals());
|
|
713
838
|
});
|
|
714
839
|
cmd.command("get <record-id>").description("Get a person by record ID").action(async (recordId, options, command) => {
|
|
@@ -731,7 +856,7 @@ function register5(program2) {
|
|
|
731
856
|
// src/commands/companies.ts
|
|
732
857
|
function register6(program2) {
|
|
733
858
|
const cmd = program2.command("companies").description("Manage company records (shortcut for: records <cmd> companies)");
|
|
734
|
-
cmd.command("list").description("List companies").option("--filter <expr>",
|
|
859
|
+
cmd.command("list").description("List companies").option("--filter <expr>", 'Filter: = != ~ !~ ^ > >= < <= ? (e.g. "name~Acme"). Repeatable', (v, p) => [...p, v], []).option("--filter-json <json>", "Raw JSON filter").option("--sort <expr>", "Sort expression (repeatable)", (v, p) => [...p, v], []).option("--limit <n>", "Max results per page", "25").option("--offset <n>", "Starting offset", "0").option("--all", "Fetch all pages").action(async (options, command) => {
|
|
735
860
|
await listRecords("companies", command.optsWithGlobals());
|
|
736
861
|
});
|
|
737
862
|
cmd.command("get <record-id>").description("Get a company by record ID").action(async (recordId, options, command) => {
|
|
@@ -760,6 +885,16 @@ function register7(program2) {
|
|
|
760
885
|
const format = detectFormat(opts);
|
|
761
886
|
const res = await client.get("/lists");
|
|
762
887
|
const lists = res.data;
|
|
888
|
+
if (format === "quiet") {
|
|
889
|
+
for (const l of lists) {
|
|
890
|
+
console.log(l.id?.list_id ?? "");
|
|
891
|
+
}
|
|
892
|
+
return;
|
|
893
|
+
}
|
|
894
|
+
if (format === "json") {
|
|
895
|
+
outputList(lists, { format });
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
763
898
|
const flat = lists.map((l) => ({
|
|
764
899
|
id: l.id?.list_id || "",
|
|
765
900
|
api_slug: l.api_slug || "",
|
|
@@ -809,7 +944,7 @@ function flattenEntry(entry) {
|
|
|
809
944
|
}
|
|
810
945
|
function register8(program2) {
|
|
811
946
|
const cmd = program2.command("entries").description("Manage list entries");
|
|
812
|
-
cmd.command("list <list>").description("List entries in a list").option("--filter <expr>",
|
|
947
|
+
cmd.command("list <list>").description("List entries in a list").option("--filter <expr>", 'Filter: = != ~ !~ ^ > >= < <= ? (e.g. "name~Acme"). Repeatable', (v, p) => [...p, v], []).option("--filter-json <json>", "Raw JSON filter").option("--sort <expr>", "Sort expression (repeatable)", (v, p) => [...p, v], []).option("--limit <n>", "Max results per page", "25").option("--offset <n>", "Starting offset", "0").option("--all", "Fetch all pages").action(async (list, _options, command) => {
|
|
813
948
|
const opts = command.optsWithGlobals();
|
|
814
949
|
const client = new AttioClient(opts.apiKey, opts.debug);
|
|
815
950
|
const format = detectFormat(opts);
|
|
@@ -857,7 +992,7 @@ function register8(program2) {
|
|
|
857
992
|
const opts = command.optsWithGlobals();
|
|
858
993
|
const client = new AttioClient(opts.apiKey, opts.debug);
|
|
859
994
|
const format = detectFormat(opts);
|
|
860
|
-
const resolvedValues = await resolveValues({ values: opts.values, set: opts.set });
|
|
995
|
+
const resolvedValues = requireValues(await resolveValues({ values: opts.values, set: opts.set }));
|
|
861
996
|
const body = {
|
|
862
997
|
data: {
|
|
863
998
|
parent_record_id: opts.record,
|
|
@@ -878,7 +1013,7 @@ function register8(program2) {
|
|
|
878
1013
|
const opts = command.optsWithGlobals();
|
|
879
1014
|
const client = new AttioClient(opts.apiKey, opts.debug);
|
|
880
1015
|
const format = detectFormat(opts);
|
|
881
|
-
const resolvedValues = await resolveValues({ values: opts.values, set: opts.set });
|
|
1016
|
+
const resolvedValues = requireValues(await resolveValues({ values: opts.values, set: opts.set }));
|
|
882
1017
|
const body = {
|
|
883
1018
|
data: {
|
|
884
1019
|
entry_values: resolvedValues
|
|
@@ -925,6 +1060,16 @@ function register9(program2) {
|
|
|
925
1060
|
if (opts.sort) params.set("sort", opts.sort);
|
|
926
1061
|
const res = await client.get(`/tasks?${params.toString()}`);
|
|
927
1062
|
const tasksList = res.data;
|
|
1063
|
+
if (format === "quiet") {
|
|
1064
|
+
for (const t of tasksList) {
|
|
1065
|
+
console.log(t.id?.task_id ?? "");
|
|
1066
|
+
}
|
|
1067
|
+
return;
|
|
1068
|
+
}
|
|
1069
|
+
if (format === "json") {
|
|
1070
|
+
outputList(tasksList, { format });
|
|
1071
|
+
return;
|
|
1072
|
+
}
|
|
928
1073
|
const flat = tasksList.map((t) => ({
|
|
929
1074
|
id: t.id?.task_id || "",
|
|
930
1075
|
content: truncate(t.content_plaintext || "", 60),
|
|
@@ -1023,6 +1168,16 @@ function register10(program2) {
|
|
|
1023
1168
|
if (opts.record) params.set("parent_record_id", opts.record);
|
|
1024
1169
|
const res = await client.get(`/notes?${params.toString()}`);
|
|
1025
1170
|
const notesList = res.data;
|
|
1171
|
+
if (format === "quiet") {
|
|
1172
|
+
for (const n of notesList) {
|
|
1173
|
+
console.log(n.id?.note_id ?? "");
|
|
1174
|
+
}
|
|
1175
|
+
return;
|
|
1176
|
+
}
|
|
1177
|
+
if (format === "json") {
|
|
1178
|
+
outputList(notesList, { format });
|
|
1179
|
+
return;
|
|
1180
|
+
}
|
|
1026
1181
|
const flat = notesList.map((n) => ({
|
|
1027
1182
|
id: n.id?.note_id || "",
|
|
1028
1183
|
title: n.title || "",
|
|
@@ -1086,6 +1241,16 @@ function register11(program2) {
|
|
|
1086
1241
|
if (opts.offset) params.set("offset", String(opts.offset));
|
|
1087
1242
|
const res = await client.get(`/threads?${params.toString()}`);
|
|
1088
1243
|
const threads = res.data;
|
|
1244
|
+
if (format === "quiet") {
|
|
1245
|
+
for (const thread of threads) {
|
|
1246
|
+
console.log(thread.id?.thread_id || thread.thread_id || "");
|
|
1247
|
+
}
|
|
1248
|
+
return;
|
|
1249
|
+
}
|
|
1250
|
+
if (format === "json") {
|
|
1251
|
+
outputList(threads, { format });
|
|
1252
|
+
return;
|
|
1253
|
+
}
|
|
1089
1254
|
const flat = [];
|
|
1090
1255
|
for (const thread of threads) {
|
|
1091
1256
|
const threadId = thread.id?.thread_id || thread.thread_id || "";
|
|
@@ -1227,39 +1392,104 @@ function register13(program2) {
|
|
|
1227
1392
|
}
|
|
1228
1393
|
var CLAUDE_MD_SNIPPET = `## Attio CLI (\`attio\`)
|
|
1229
1394
|
|
|
1230
|
-
Use the \`attio\` CLI for all Attio CRM operations.
|
|
1395
|
+
Use the \`attio\` CLI for all Attio CRM operations. Always pass \`--yes\` on delete commands to avoid interactive prompts.
|
|
1396
|
+
|
|
1397
|
+
### Discovery & setup
|
|
1398
|
+
|
|
1399
|
+
\`\`\`
|
|
1400
|
+
attio whoami Show workspace info
|
|
1401
|
+
attio objects list List all objects (people, companies, custom...)
|
|
1402
|
+
attio attributes list <object> List attributes for an object (shows slugs, types)
|
|
1403
|
+
attio lists list List all lists
|
|
1404
|
+
attio members list List workspace members (get member IDs for tasks)
|
|
1405
|
+
\`\`\`
|
|
1406
|
+
|
|
1407
|
+
### Records (CRUD \u2014 works for any object)
|
|
1408
|
+
|
|
1409
|
+
\`\`\`
|
|
1410
|
+
attio records list <object> [--filter <expr>] [--sort <expr>] [--limit N] [--all]
|
|
1411
|
+
attio records get <object> <record-id>
|
|
1412
|
+
attio records create <object> --set key=value [--set key2=value2]
|
|
1413
|
+
attio records update <object> <record-id> --set key=value
|
|
1414
|
+
attio records delete <object> <record-id> --yes
|
|
1415
|
+
attio records upsert <object> --match <attr-slug> --set key=value
|
|
1416
|
+
attio records search <object> <query>
|
|
1417
|
+
\`\`\`
|
|
1418
|
+
|
|
1419
|
+
### Shortcuts
|
|
1420
|
+
|
|
1421
|
+
\`\`\`
|
|
1422
|
+
attio people list|get|create|update|delete|search (same as: records ... people)
|
|
1423
|
+
attio companies list|get|create|update|delete|search (same as: records ... companies)
|
|
1424
|
+
\`\`\`
|
|
1425
|
+
|
|
1426
|
+
### Lists & entries
|
|
1427
|
+
|
|
1428
|
+
\`\`\`
|
|
1429
|
+
attio entries list <list> [--filter <expr>] [--sort <expr>] [--limit N] [--all]
|
|
1430
|
+
attio entries get <list> <entry-id>
|
|
1431
|
+
attio entries create <list> --record <record-id> --object <obj> [--set key=value]
|
|
1432
|
+
attio entries update <list> <entry-id> --set key=value
|
|
1433
|
+
attio entries delete <list> <entry-id> --yes
|
|
1434
|
+
\`\`\`
|
|
1435
|
+
|
|
1436
|
+
### Tasks
|
|
1437
|
+
|
|
1438
|
+
\`\`\`
|
|
1439
|
+
attio tasks list [--assignee <member-id>] [--is-completed] [--limit N]
|
|
1440
|
+
attio tasks get <task-id>
|
|
1441
|
+
attio tasks create --content "..." [--assignee <member-id>] [--deadline <ISO-date>] [--record <object:record-id>]
|
|
1442
|
+
attio tasks update <task-id> [--complete] [--incomplete] [--deadline <ISO-date>] [--content "..."]
|
|
1443
|
+
attio tasks delete <task-id> --yes
|
|
1444
|
+
\`\`\`
|
|
1231
1445
|
|
|
1232
|
-
###
|
|
1446
|
+
### Notes & comments
|
|
1233
1447
|
|
|
1234
1448
|
\`\`\`
|
|
1235
|
-
attio
|
|
1236
|
-
attio
|
|
1237
|
-
attio
|
|
1238
|
-
attio
|
|
1239
|
-
attio
|
|
1240
|
-
attio
|
|
1449
|
+
attio notes list [--object <obj> --record <id>]
|
|
1450
|
+
attio notes get <note-id>
|
|
1451
|
+
attio notes create --object <obj> --record <id> --title "..." --content "..."
|
|
1452
|
+
attio notes delete <note-id> --yes
|
|
1453
|
+
attio comments list --object <obj> --record <id>
|
|
1454
|
+
attio comments create --object <obj> --record <id> --content "..."
|
|
1455
|
+
attio comments delete <comment-id> --yes
|
|
1241
1456
|
\`\`\`
|
|
1242
1457
|
|
|
1243
|
-
### Output
|
|
1458
|
+
### Output modes
|
|
1244
1459
|
|
|
1245
|
-
Auto-detects
|
|
1460
|
+
Auto-detects: table for TTY, JSON when piped. Force with \`--json\`, \`--csv\`, or \`--table\`.
|
|
1461
|
+
Use \`-q\` for IDs only (one per line) \u2014 ideal for chaining:
|
|
1246
1462
|
|
|
1247
|
-
|
|
1463
|
+
\`\`\`bash
|
|
1464
|
+
ID=$(attio records create companies --set name="Acme" -q)
|
|
1465
|
+
attio notes create --object companies --record $ID --title "Note" --content "..."
|
|
1466
|
+
\`\`\`
|
|
1467
|
+
|
|
1468
|
+
### Filter syntax
|
|
1248
1469
|
|
|
1249
|
-
\`--filter
|
|
1250
|
-
Multiple \`--filter\` flags are ANDed
|
|
1470
|
+
\`--filter\` supports: \`=\` (equals), \`!=\` (not equals), \`~\` (contains), \`!~\` (not contains), \`^\` (starts with), \`>\`, \`>=\`, \`<\`, \`<=\`, \`?\` (is set/not empty).
|
|
1471
|
+
Multiple \`--filter\` flags are ANDed. Use \`--filter-json '{...}'\` for raw Attio filter JSON.
|
|
1472
|
+
|
|
1473
|
+
\`\`\`
|
|
1474
|
+
--filter 'name~Acme' --filter 'revenue>=1000000' --sort name:asc
|
|
1475
|
+
\`\`\`
|
|
1476
|
+
|
|
1477
|
+
### Values for create/update
|
|
1478
|
+
|
|
1479
|
+
\`--set key=value\` (repeatable), \`--values '{"key":"value"}'\`, \`--values @file.json\`, or pipe JSON to stdin.`;
|
|
1251
1480
|
|
|
1252
1481
|
// src/commands/open.ts
|
|
1253
|
-
import {
|
|
1482
|
+
import { execFile } from "child_process";
|
|
1254
1483
|
import { platform } from "os";
|
|
1255
1484
|
import chalk5 from "chalk";
|
|
1256
1485
|
function register14(program2) {
|
|
1257
1486
|
program2.command("open <object> [record-id]").description("Open an object or record in the Attio web app").action(async (object, recordId, options, command) => {
|
|
1258
1487
|
const opts = command.optsWithGlobals();
|
|
1488
|
+
const objectSlug = encodeURIComponent(object);
|
|
1259
1489
|
let url;
|
|
1260
1490
|
if (recordId) {
|
|
1261
1491
|
const client = new AttioClient(opts.apiKey, opts.debug);
|
|
1262
|
-
const res = await client.get(`/objects/${
|
|
1492
|
+
const res = await client.get(`/objects/${objectSlug}/records/${encodeURIComponent(recordId)}`);
|
|
1263
1493
|
const record = res.data;
|
|
1264
1494
|
url = record.web_url;
|
|
1265
1495
|
if (!url) {
|
|
@@ -1270,10 +1500,10 @@ function register14(program2) {
|
|
|
1270
1500
|
const client = new AttioClient(opts.apiKey, opts.debug);
|
|
1271
1501
|
const self = await client.get("/self");
|
|
1272
1502
|
const slug = self.workspace_slug || "";
|
|
1273
|
-
url = `https://app.attio.com/${slug}/${
|
|
1503
|
+
url = `https://app.attio.com/${encodeURIComponent(slug)}/${objectSlug}`;
|
|
1274
1504
|
}
|
|
1275
|
-
const cmd = platform() === "darwin" ? "open" : platform() === "win32" ? "
|
|
1276
|
-
|
|
1505
|
+
const cmd = platform() === "darwin" ? "open" : platform() === "win32" ? "explorer" : "xdg-open";
|
|
1506
|
+
execFile(cmd, [url], (err) => {
|
|
1277
1507
|
if (err) {
|
|
1278
1508
|
console.log(url);
|
|
1279
1509
|
}
|
|
@@ -1281,31 +1511,158 @@ function register14(program2) {
|
|
|
1281
1511
|
});
|
|
1282
1512
|
}
|
|
1283
1513
|
|
|
1514
|
+
// src/commands/init.ts
|
|
1515
|
+
import { createInterface as createInterface2 } from "readline";
|
|
1516
|
+
import chalk6 from "chalk";
|
|
1517
|
+
function register15(program2) {
|
|
1518
|
+
program2.command("init").description("Interactive setup wizard \u2014 connect to your Attio workspace").action(async function() {
|
|
1519
|
+
const opts = this.optsWithGlobals();
|
|
1520
|
+
let apiKey = opts.apiKey;
|
|
1521
|
+
if (!apiKey) {
|
|
1522
|
+
if (!process.stdin.isTTY) {
|
|
1523
|
+
console.log("Non-interactive environment detected. To configure manually:\n");
|
|
1524
|
+
console.log(" export ATTIO_API_KEY=your_key");
|
|
1525
|
+
console.log(" # or");
|
|
1526
|
+
console.log(` attio config set api-key your_key`);
|
|
1527
|
+
console.log(` # or`);
|
|
1528
|
+
console.log(` attio init --api-key your_key
|
|
1529
|
+
`);
|
|
1530
|
+
console.log(`Get your API key at: https://app.attio.com/settings/developers`);
|
|
1531
|
+
return;
|
|
1532
|
+
}
|
|
1533
|
+
if (isConfigured()) {
|
|
1534
|
+
const rl2 = createInterface2({ input: process.stdin, output: process.stderr });
|
|
1535
|
+
const overwrite = await new Promise((resolve) => {
|
|
1536
|
+
rl2.question(chalk6.yellow("An API key is already configured. Overwrite? [y/N] "), (answer) => {
|
|
1537
|
+
rl2.close();
|
|
1538
|
+
resolve(answer.trim().toLowerCase() === "y");
|
|
1539
|
+
});
|
|
1540
|
+
});
|
|
1541
|
+
if (!overwrite) {
|
|
1542
|
+
console.error(chalk6.dim("Setup cancelled."));
|
|
1543
|
+
return;
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
console.error("");
|
|
1547
|
+
console.error(chalk6.bold(" Attio CLI Setup"));
|
|
1548
|
+
console.error(chalk6.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
1549
|
+
console.error("");
|
|
1550
|
+
console.error(` You'll need an API key from ${chalk6.cyan("https://app.attio.com/settings/developers")}`);
|
|
1551
|
+
console.error("");
|
|
1552
|
+
const rl = createInterface2({ input: process.stdin, output: process.stderr });
|
|
1553
|
+
apiKey = await new Promise((resolve, reject) => {
|
|
1554
|
+
rl.question(" Paste your API key: ", (answer) => {
|
|
1555
|
+
rl.close();
|
|
1556
|
+
resolve(answer);
|
|
1557
|
+
});
|
|
1558
|
+
rl.on("close", () => reject(new Error("cancelled")));
|
|
1559
|
+
}).catch(() => {
|
|
1560
|
+
console.error("");
|
|
1561
|
+
process.exit(0);
|
|
1562
|
+
});
|
|
1563
|
+
}
|
|
1564
|
+
apiKey = apiKey.trim().replace(/^['"]|['"]$/g, "");
|
|
1565
|
+
if (!apiKey) {
|
|
1566
|
+
console.error(chalk6.red("\n No API key provided."));
|
|
1567
|
+
console.error(` Get one at: ${chalk6.cyan("https://app.attio.com/settings/developers")}`);
|
|
1568
|
+
process.exit(1);
|
|
1569
|
+
}
|
|
1570
|
+
process.stderr.write(chalk6.dim(" Verifying..."));
|
|
1571
|
+
try {
|
|
1572
|
+
const client = new AttioClient(apiKey);
|
|
1573
|
+
const self = await client.get("/self");
|
|
1574
|
+
console.error(chalk6.green(" \u2713"));
|
|
1575
|
+
console.error("");
|
|
1576
|
+
const name = self.workspace_name || "your workspace";
|
|
1577
|
+
const slug = self.workspace_slug;
|
|
1578
|
+
console.error(` Connected to ${chalk6.bold(`"${name}"`)}${slug ? ` (${slug})` : ""}`);
|
|
1579
|
+
setApiKey(apiKey);
|
|
1580
|
+
console.error(` API key saved to ${chalk6.dim(getConfigPath())}`);
|
|
1581
|
+
console.error("");
|
|
1582
|
+
console.error(chalk6.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
1583
|
+
console.error(chalk6.bold(" Agent Setup") + chalk6.dim(" (optional)"));
|
|
1584
|
+
console.error("");
|
|
1585
|
+
console.error(" To let AI agents discover this CLI, run:");
|
|
1586
|
+
console.error("");
|
|
1587
|
+
console.error(` ${chalk6.cyan("attio config claude-md >> CLAUDE.md")}`);
|
|
1588
|
+
console.error("");
|
|
1589
|
+
console.error(` Done! Try ${chalk6.cyan("attio whoami")} or ${chalk6.cyan("attio companies list")} to get started.`);
|
|
1590
|
+
console.error("");
|
|
1591
|
+
} catch (err) {
|
|
1592
|
+
console.error(chalk6.red(" \u2717"));
|
|
1593
|
+
console.error("");
|
|
1594
|
+
if (err?.message?.includes("Invalid or expired") || err?.message?.includes("401") || err?.message?.includes("not recognised")) {
|
|
1595
|
+
console.error(chalk6.red(" Invalid API key."));
|
|
1596
|
+
console.error(` Double-check at: ${chalk6.cyan("https://app.attio.com/settings/developers")}`);
|
|
1597
|
+
} else {
|
|
1598
|
+
console.error(chalk6.red(` Could not connect: ${err?.message || err}`));
|
|
1599
|
+
}
|
|
1600
|
+
process.exit(1);
|
|
1601
|
+
}
|
|
1602
|
+
});
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1284
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
|
+
}
|
|
1285
1618
|
function handleError(err) {
|
|
1619
|
+
const jsonMode = program.opts().json || !process.stdout.isTTY;
|
|
1286
1620
|
if (err instanceof AttioApiError) {
|
|
1287
|
-
|
|
1621
|
+
if (jsonMode) {
|
|
1622
|
+
console.error(JSON.stringify({ error: true, status: err.statusCode, type: err.type, message: err.detail }));
|
|
1623
|
+
} else {
|
|
1624
|
+
console.error(err.display());
|
|
1625
|
+
}
|
|
1288
1626
|
process.exit(err.exitCode);
|
|
1289
1627
|
}
|
|
1290
1628
|
if (err instanceof AttioAuthError) {
|
|
1291
|
-
|
|
1629
|
+
if (jsonMode) {
|
|
1630
|
+
console.error(JSON.stringify({ error: true, status: 401, type: "auth_error", message: err.message }));
|
|
1631
|
+
} else {
|
|
1632
|
+
console.error(err.display());
|
|
1633
|
+
}
|
|
1292
1634
|
process.exit(err.exitCode);
|
|
1293
1635
|
}
|
|
1294
1636
|
if (err instanceof AttioRateLimitError) {
|
|
1295
|
-
|
|
1637
|
+
if (jsonMode) {
|
|
1638
|
+
console.error(JSON.stringify({ error: true, status: 429, type: "rate_limit", message: err.message }));
|
|
1639
|
+
} else {
|
|
1640
|
+
console.error(err.display());
|
|
1641
|
+
}
|
|
1296
1642
|
process.exit(err.exitCode);
|
|
1297
1643
|
}
|
|
1298
1644
|
if (err instanceof Error) {
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1645
|
+
if (jsonMode) {
|
|
1646
|
+
console.error(JSON.stringify({ error: true, type: "unknown_error", message: err.message }));
|
|
1647
|
+
} else {
|
|
1648
|
+
console.error(chalk7.red(`Error: ${err.message}`));
|
|
1649
|
+
if (process.env.ATTIO_DEBUG || program.opts().debug) {
|
|
1650
|
+
console.error(err.stack);
|
|
1651
|
+
}
|
|
1302
1652
|
}
|
|
1303
1653
|
} else {
|
|
1304
|
-
|
|
1654
|
+
if (jsonMode) {
|
|
1655
|
+
console.error(JSON.stringify({ error: true, type: "unknown_error", message: "An unexpected error occurred" }));
|
|
1656
|
+
} else {
|
|
1657
|
+
console.error(chalk7.red("An unexpected error occurred"));
|
|
1658
|
+
}
|
|
1305
1659
|
}
|
|
1306
1660
|
process.exit(1);
|
|
1307
1661
|
}
|
|
1308
|
-
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");
|
|
1663
|
+
if (process.argv.includes("--no-color")) {
|
|
1664
|
+
process.env.NO_COLOR = "1";
|
|
1665
|
+
}
|
|
1309
1666
|
register(program);
|
|
1310
1667
|
register2(program);
|
|
1311
1668
|
register3(program);
|
|
@@ -1320,6 +1677,22 @@ register11(program);
|
|
|
1320
1677
|
register12(program);
|
|
1321
1678
|
register13(program);
|
|
1322
1679
|
register14(program);
|
|
1680
|
+
register15(program);
|
|
1681
|
+
program.action(() => {
|
|
1682
|
+
if (!isConfigured()) {
|
|
1683
|
+
console.error("");
|
|
1684
|
+
console.error(chalk7.bold(" Welcome to attio-cli!"));
|
|
1685
|
+
console.error("");
|
|
1686
|
+
console.error(` You haven't configured an API key yet. Run:`);
|
|
1687
|
+
console.error("");
|
|
1688
|
+
console.error(` ${chalk7.cyan("attio init")}`);
|
|
1689
|
+
console.error("");
|
|
1690
|
+
console.error(" to connect to your Attio workspace.");
|
|
1691
|
+
console.error("");
|
|
1692
|
+
} else {
|
|
1693
|
+
program.outputHelp();
|
|
1694
|
+
}
|
|
1695
|
+
});
|
|
1323
1696
|
program.parseAsync(process.argv).catch(handleError);
|
|
1324
1697
|
process.on("uncaughtException", handleError);
|
|
1325
1698
|
process.on("unhandledRejection", (reason) => handleError(reason));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "attio-cli",
|
|
3
|
-
"version": "0.1
|
|
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",
|