attio-cli 0.1.0 → 0.2.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/README.md +21 -6
- package/dist/attio.js +307 -34
- package/package.json +1 -1
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,7 @@
|
|
|
2
2
|
|
|
3
3
|
// bin/attio.ts
|
|
4
4
|
import { program } from "commander";
|
|
5
|
-
import
|
|
5
|
+
import chalk7 from "chalk";
|
|
6
6
|
|
|
7
7
|
// src/errors.ts
|
|
8
8
|
import chalk from "chalk";
|
|
@@ -87,11 +87,15 @@ function setApiKey(key) {
|
|
|
87
87
|
function getConfigPath() {
|
|
88
88
|
return CONFIG_FILE;
|
|
89
89
|
}
|
|
90
|
+
function isConfigured() {
|
|
91
|
+
return resolveApiKey() !== "";
|
|
92
|
+
}
|
|
90
93
|
|
|
91
94
|
// src/client.ts
|
|
92
95
|
var BASE_URL = "https://api.attio.com/v2";
|
|
93
96
|
var MAX_RETRIES = 3;
|
|
94
97
|
var INITIAL_BACKOFF_MS = 1e3;
|
|
98
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
95
99
|
var AttioClient = class {
|
|
96
100
|
apiKey;
|
|
97
101
|
debug;
|
|
@@ -103,6 +107,9 @@ var AttioClient = class {
|
|
|
103
107
|
const url = `${BASE_URL}${path}`;
|
|
104
108
|
if (this.debug) {
|
|
105
109
|
console.error(chalk2.dim(`\u2192 ${method} ${url}`));
|
|
110
|
+
if (body !== void 0) {
|
|
111
|
+
console.error(chalk2.dim(` body: ${JSON.stringify(body)}`));
|
|
112
|
+
}
|
|
106
113
|
}
|
|
107
114
|
if (!this.apiKey) {
|
|
108
115
|
throw new AttioAuthError("No API key configured");
|
|
@@ -116,9 +123,21 @@ var AttioClient = class {
|
|
|
116
123
|
init.body = JSON.stringify(body);
|
|
117
124
|
}
|
|
118
125
|
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
119
|
-
const
|
|
126
|
+
const controller = new AbortController();
|
|
127
|
+
const timer = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT_MS);
|
|
128
|
+
let response;
|
|
129
|
+
try {
|
|
130
|
+
response = await fetch(url, { ...init, signal: controller.signal });
|
|
131
|
+
} catch (err) {
|
|
132
|
+
clearTimeout(timer);
|
|
133
|
+
if (err?.name === "AbortError") {
|
|
134
|
+
throw new Error(`Request timed out after ${DEFAULT_TIMEOUT_MS / 1e3}s: ${method} ${path}`);
|
|
135
|
+
}
|
|
136
|
+
throw err;
|
|
137
|
+
}
|
|
138
|
+
clearTimeout(timer);
|
|
120
139
|
if (this.debug) {
|
|
121
|
-
console.error(chalk2.dim(`\u2190 ${response.status}`));
|
|
140
|
+
console.error(chalk2.dim(`\u2190 ${response.status} ${response.statusText}`));
|
|
122
141
|
}
|
|
123
142
|
if (response.status === 429) {
|
|
124
143
|
if (attempt < MAX_RETRIES) {
|
|
@@ -135,6 +154,9 @@ var AttioClient = class {
|
|
|
135
154
|
return void 0;
|
|
136
155
|
}
|
|
137
156
|
const json = await response.json();
|
|
157
|
+
if (this.debug && !response.ok) {
|
|
158
|
+
console.error(chalk2.dim(` error: ${JSON.stringify(json)}`));
|
|
159
|
+
}
|
|
138
160
|
if (!response.ok) {
|
|
139
161
|
const errorType = json?.type ?? "unknown_error";
|
|
140
162
|
let errorDetail = json?.message ?? json?.detail ?? response.statusText;
|
|
@@ -240,6 +262,11 @@ function outputSingle(item, opts) {
|
|
|
240
262
|
console.log(table.toString());
|
|
241
263
|
}
|
|
242
264
|
async function confirm(message) {
|
|
265
|
+
if (!process.stdin.isTTY) {
|
|
266
|
+
console.error(`${message} [y/N]`);
|
|
267
|
+
console.error("Error: Confirmation required but stdin is not a TTY. Use --yes to skip.");
|
|
268
|
+
return false;
|
|
269
|
+
}
|
|
243
270
|
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
244
271
|
return new Promise((resolve) => {
|
|
245
272
|
rl.question(`${message} [y/N] `, (answer) => {
|
|
@@ -466,8 +493,15 @@ async function resolveValues(options) {
|
|
|
466
493
|
if (stdin.trim()) return JSON.parse(stdin);
|
|
467
494
|
return {};
|
|
468
495
|
}
|
|
496
|
+
function requireValues(values) {
|
|
497
|
+
if (Object.keys(values).length === 0) {
|
|
498
|
+
throw new Error(`No values provided. Use --set key=value, --values '{"key":"value"}', or pipe JSON to stdin.`);
|
|
499
|
+
}
|
|
500
|
+
return values;
|
|
501
|
+
}
|
|
469
502
|
|
|
470
503
|
// src/pagination.ts
|
|
504
|
+
var MAX_ALL_RECORDS = 1e4;
|
|
471
505
|
async function paginate(fetchPage, options) {
|
|
472
506
|
if (!options.all) {
|
|
473
507
|
return fetchPage(options.limit, options.offset);
|
|
@@ -480,6 +514,10 @@ async function paginate(fetchPage, options) {
|
|
|
480
514
|
allResults.push(...page);
|
|
481
515
|
if (page.length < pageSize) break;
|
|
482
516
|
offset += pageSize;
|
|
517
|
+
if (allResults.length >= MAX_ALL_RECORDS) {
|
|
518
|
+
console.error(`Warning: --all stopped after ${MAX_ALL_RECORDS} records. Use --limit and --offset for manual pagination.`);
|
|
519
|
+
break;
|
|
520
|
+
}
|
|
483
521
|
}
|
|
484
522
|
return allResults;
|
|
485
523
|
}
|
|
@@ -548,7 +586,7 @@ async function getRecord(object, recordId, cmdOpts) {
|
|
|
548
586
|
async function createRecord(object, cmdOpts) {
|
|
549
587
|
const client = new AttioClient(cmdOpts.apiKey, cmdOpts.debug);
|
|
550
588
|
const format = detectFormat(cmdOpts);
|
|
551
|
-
const values = await resolveValues(cmdOpts);
|
|
589
|
+
const values = requireValues(await resolveValues(cmdOpts));
|
|
552
590
|
const res = await client.post(
|
|
553
591
|
`/objects/${encodeURIComponent(object)}/records`,
|
|
554
592
|
{ data: { values } }
|
|
@@ -567,7 +605,7 @@ async function createRecord(object, cmdOpts) {
|
|
|
567
605
|
async function updateRecord(object, recordId, cmdOpts) {
|
|
568
606
|
const client = new AttioClient(cmdOpts.apiKey, cmdOpts.debug);
|
|
569
607
|
const format = detectFormat(cmdOpts);
|
|
570
|
-
const values = await resolveValues(cmdOpts);
|
|
608
|
+
const values = requireValues(await resolveValues(cmdOpts));
|
|
571
609
|
const res = await client.patch(
|
|
572
610
|
`/objects/${encodeURIComponent(object)}/records/${encodeURIComponent(recordId)}`,
|
|
573
611
|
{ data: { values } }
|
|
@@ -605,7 +643,7 @@ async function upsertRecord(object, cmdOpts) {
|
|
|
605
643
|
if (!matchAttr) {
|
|
606
644
|
throw new Error("--match <attribute-slug> is required for upsert");
|
|
607
645
|
}
|
|
608
|
-
const values = await resolveValues(cmdOpts);
|
|
646
|
+
const values = requireValues(await resolveValues(cmdOpts));
|
|
609
647
|
const res = await client.put(
|
|
610
648
|
`/objects/${encodeURIComponent(object)}/records?matching_attribute=${encodeURIComponent(matchAttr)}`,
|
|
611
649
|
{ data: { values } }
|
|
@@ -652,7 +690,7 @@ function register4(program2) {
|
|
|
652
690
|
const records = program2.command("records").description("Manage records in any Attio object");
|
|
653
691
|
records.command("list").description("List or query records for an object").argument("<object>", "Object slug or ID (e.g. companies, people)").option(
|
|
654
692
|
"--filter <expr>",
|
|
655
|
-
'Filter
|
|
693
|
+
'Filter: = != ~ !~ ^ > >= < <= ? (e.g. "name~Acme", "revenue>=1000", "email?"). Repeatable',
|
|
656
694
|
(val, prev) => [...prev, val],
|
|
657
695
|
[]
|
|
658
696
|
).option("--filter-json <json>", "Raw JSON filter (overrides --filter)").option(
|
|
@@ -708,7 +746,7 @@ function register4(program2) {
|
|
|
708
746
|
// src/commands/people.ts
|
|
709
747
|
function register5(program2) {
|
|
710
748
|
const cmd = program2.command("people").description("Manage people records (shortcut for: records <cmd> people)");
|
|
711
|
-
cmd.command("list").description("List people").option("--filter <expr>",
|
|
749
|
+
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
750
|
await listRecords("people", command.optsWithGlobals());
|
|
713
751
|
});
|
|
714
752
|
cmd.command("get <record-id>").description("Get a person by record ID").action(async (recordId, options, command) => {
|
|
@@ -731,7 +769,7 @@ function register5(program2) {
|
|
|
731
769
|
// src/commands/companies.ts
|
|
732
770
|
function register6(program2) {
|
|
733
771
|
const cmd = program2.command("companies").description("Manage company records (shortcut for: records <cmd> companies)");
|
|
734
|
-
cmd.command("list").description("List companies").option("--filter <expr>",
|
|
772
|
+
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
773
|
await listRecords("companies", command.optsWithGlobals());
|
|
736
774
|
});
|
|
737
775
|
cmd.command("get <record-id>").description("Get a company by record ID").action(async (recordId, options, command) => {
|
|
@@ -760,6 +798,16 @@ function register7(program2) {
|
|
|
760
798
|
const format = detectFormat(opts);
|
|
761
799
|
const res = await client.get("/lists");
|
|
762
800
|
const lists = res.data;
|
|
801
|
+
if (format === "quiet") {
|
|
802
|
+
for (const l of lists) {
|
|
803
|
+
console.log(l.id?.list_id ?? "");
|
|
804
|
+
}
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
807
|
+
if (format === "json") {
|
|
808
|
+
outputList(lists, { format });
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
763
811
|
const flat = lists.map((l) => ({
|
|
764
812
|
id: l.id?.list_id || "",
|
|
765
813
|
api_slug: l.api_slug || "",
|
|
@@ -809,7 +857,7 @@ function flattenEntry(entry) {
|
|
|
809
857
|
}
|
|
810
858
|
function register8(program2) {
|
|
811
859
|
const cmd = program2.command("entries").description("Manage list entries");
|
|
812
|
-
cmd.command("list <list>").description("List entries in a list").option("--filter <expr>",
|
|
860
|
+
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
861
|
const opts = command.optsWithGlobals();
|
|
814
862
|
const client = new AttioClient(opts.apiKey, opts.debug);
|
|
815
863
|
const format = detectFormat(opts);
|
|
@@ -857,7 +905,7 @@ function register8(program2) {
|
|
|
857
905
|
const opts = command.optsWithGlobals();
|
|
858
906
|
const client = new AttioClient(opts.apiKey, opts.debug);
|
|
859
907
|
const format = detectFormat(opts);
|
|
860
|
-
const resolvedValues = await resolveValues({ values: opts.values, set: opts.set });
|
|
908
|
+
const resolvedValues = requireValues(await resolveValues({ values: opts.values, set: opts.set }));
|
|
861
909
|
const body = {
|
|
862
910
|
data: {
|
|
863
911
|
parent_record_id: opts.record,
|
|
@@ -878,7 +926,7 @@ function register8(program2) {
|
|
|
878
926
|
const opts = command.optsWithGlobals();
|
|
879
927
|
const client = new AttioClient(opts.apiKey, opts.debug);
|
|
880
928
|
const format = detectFormat(opts);
|
|
881
|
-
const resolvedValues = await resolveValues({ values: opts.values, set: opts.set });
|
|
929
|
+
const resolvedValues = requireValues(await resolveValues({ values: opts.values, set: opts.set }));
|
|
882
930
|
const body = {
|
|
883
931
|
data: {
|
|
884
932
|
entry_values: resolvedValues
|
|
@@ -925,6 +973,16 @@ function register9(program2) {
|
|
|
925
973
|
if (opts.sort) params.set("sort", opts.sort);
|
|
926
974
|
const res = await client.get(`/tasks?${params.toString()}`);
|
|
927
975
|
const tasksList = res.data;
|
|
976
|
+
if (format === "quiet") {
|
|
977
|
+
for (const t of tasksList) {
|
|
978
|
+
console.log(t.id?.task_id ?? "");
|
|
979
|
+
}
|
|
980
|
+
return;
|
|
981
|
+
}
|
|
982
|
+
if (format === "json") {
|
|
983
|
+
outputList(tasksList, { format });
|
|
984
|
+
return;
|
|
985
|
+
}
|
|
928
986
|
const flat = tasksList.map((t) => ({
|
|
929
987
|
id: t.id?.task_id || "",
|
|
930
988
|
content: truncate(t.content_plaintext || "", 60),
|
|
@@ -1023,6 +1081,16 @@ function register10(program2) {
|
|
|
1023
1081
|
if (opts.record) params.set("parent_record_id", opts.record);
|
|
1024
1082
|
const res = await client.get(`/notes?${params.toString()}`);
|
|
1025
1083
|
const notesList = res.data;
|
|
1084
|
+
if (format === "quiet") {
|
|
1085
|
+
for (const n of notesList) {
|
|
1086
|
+
console.log(n.id?.note_id ?? "");
|
|
1087
|
+
}
|
|
1088
|
+
return;
|
|
1089
|
+
}
|
|
1090
|
+
if (format === "json") {
|
|
1091
|
+
outputList(notesList, { format });
|
|
1092
|
+
return;
|
|
1093
|
+
}
|
|
1026
1094
|
const flat = notesList.map((n) => ({
|
|
1027
1095
|
id: n.id?.note_id || "",
|
|
1028
1096
|
title: n.title || "",
|
|
@@ -1086,6 +1154,16 @@ function register11(program2) {
|
|
|
1086
1154
|
if (opts.offset) params.set("offset", String(opts.offset));
|
|
1087
1155
|
const res = await client.get(`/threads?${params.toString()}`);
|
|
1088
1156
|
const threads = res.data;
|
|
1157
|
+
if (format === "quiet") {
|
|
1158
|
+
for (const thread of threads) {
|
|
1159
|
+
console.log(thread.id?.thread_id || thread.thread_id || "");
|
|
1160
|
+
}
|
|
1161
|
+
return;
|
|
1162
|
+
}
|
|
1163
|
+
if (format === "json") {
|
|
1164
|
+
outputList(threads, { format });
|
|
1165
|
+
return;
|
|
1166
|
+
}
|
|
1089
1167
|
const flat = [];
|
|
1090
1168
|
for (const thread of threads) {
|
|
1091
1169
|
const threadId = thread.id?.thread_id || thread.thread_id || "";
|
|
@@ -1227,30 +1305,94 @@ function register13(program2) {
|
|
|
1227
1305
|
}
|
|
1228
1306
|
var CLAUDE_MD_SNIPPET = `## Attio CLI (\`attio\`)
|
|
1229
1307
|
|
|
1230
|
-
Use the \`attio\` CLI for all Attio CRM operations.
|
|
1308
|
+
Use the \`attio\` CLI for all Attio CRM operations. Always pass \`--yes\` on delete commands to avoid interactive prompts.
|
|
1309
|
+
|
|
1310
|
+
### Discovery & setup
|
|
1311
|
+
|
|
1312
|
+
\`\`\`
|
|
1313
|
+
attio whoami Show workspace info
|
|
1314
|
+
attio objects list List all objects (people, companies, custom...)
|
|
1315
|
+
attio attributes list <object> List attributes for an object (shows slugs, types)
|
|
1316
|
+
attio lists list List all lists
|
|
1317
|
+
attio members list List workspace members (get member IDs for tasks)
|
|
1318
|
+
\`\`\`
|
|
1231
1319
|
|
|
1232
|
-
###
|
|
1320
|
+
### Records (CRUD \u2014 works for any object)
|
|
1233
1321
|
|
|
1234
1322
|
\`\`\`
|
|
1235
|
-
attio
|
|
1236
|
-
attio
|
|
1237
|
-
attio records
|
|
1238
|
-
attio
|
|
1239
|
-
attio
|
|
1240
|
-
attio
|
|
1323
|
+
attio records list <object> [--filter <expr>] [--sort <expr>] [--limit N] [--all]
|
|
1324
|
+
attio records get <object> <record-id>
|
|
1325
|
+
attio records create <object> --set key=value [--set key2=value2]
|
|
1326
|
+
attio records update <object> <record-id> --set key=value
|
|
1327
|
+
attio records delete <object> <record-id> --yes
|
|
1328
|
+
attio records upsert <object> --match <attr-slug> --set key=value
|
|
1329
|
+
attio records search <object> <query>
|
|
1241
1330
|
\`\`\`
|
|
1242
1331
|
|
|
1243
|
-
###
|
|
1332
|
+
### Shortcuts
|
|
1333
|
+
|
|
1334
|
+
\`\`\`
|
|
1335
|
+
attio people list|get|create|update|delete|search (same as: records ... people)
|
|
1336
|
+
attio companies list|get|create|update|delete|search (same as: records ... companies)
|
|
1337
|
+
\`\`\`
|
|
1244
1338
|
|
|
1245
|
-
|
|
1339
|
+
### Lists & entries
|
|
1246
1340
|
|
|
1247
|
-
|
|
1341
|
+
\`\`\`
|
|
1342
|
+
attio entries list <list> [--filter <expr>] [--sort <expr>] [--limit N] [--all]
|
|
1343
|
+
attio entries get <list> <entry-id>
|
|
1344
|
+
attio entries create <list> --record <record-id> --object <obj> [--set key=value]
|
|
1345
|
+
attio entries update <list> <entry-id> --set key=value
|
|
1346
|
+
attio entries delete <list> <entry-id> --yes
|
|
1347
|
+
\`\`\`
|
|
1248
1348
|
|
|
1249
|
-
|
|
1250
|
-
|
|
1349
|
+
### Tasks
|
|
1350
|
+
|
|
1351
|
+
\`\`\`
|
|
1352
|
+
attio tasks list [--assignee <member-id>] [--is-completed] [--limit N]
|
|
1353
|
+
attio tasks get <task-id>
|
|
1354
|
+
attio tasks create --content "..." [--assignee <member-id>] [--deadline <ISO-date>] [--record <object:record-id>]
|
|
1355
|
+
attio tasks update <task-id> [--complete] [--incomplete] [--deadline <ISO-date>] [--content "..."]
|
|
1356
|
+
attio tasks delete <task-id> --yes
|
|
1357
|
+
\`\`\`
|
|
1358
|
+
|
|
1359
|
+
### Notes & comments
|
|
1360
|
+
|
|
1361
|
+
\`\`\`
|
|
1362
|
+
attio notes list [--object <obj> --record <id>]
|
|
1363
|
+
attio notes get <note-id>
|
|
1364
|
+
attio notes create --object <obj> --record <id> --title "..." --content "..."
|
|
1365
|
+
attio notes delete <note-id> --yes
|
|
1366
|
+
attio comments list --object <obj> --record <id>
|
|
1367
|
+
attio comments create --object <obj> --record <id> --content "..."
|
|
1368
|
+
attio comments delete <comment-id> --yes
|
|
1369
|
+
\`\`\`
|
|
1370
|
+
|
|
1371
|
+
### Output modes
|
|
1372
|
+
|
|
1373
|
+
Auto-detects: table for TTY, JSON when piped. Force with \`--json\`, \`--csv\`, or \`--table\`.
|
|
1374
|
+
Use \`-q\` for IDs only (one per line) \u2014 ideal for chaining:
|
|
1375
|
+
|
|
1376
|
+
\`\`\`bash
|
|
1377
|
+
ID=$(attio records create companies --set name="Acme" -q)
|
|
1378
|
+
attio notes create --object companies --record $ID --title "Note" --content "..."
|
|
1379
|
+
\`\`\`
|
|
1380
|
+
|
|
1381
|
+
### Filter syntax
|
|
1382
|
+
|
|
1383
|
+
\`--filter\` supports: \`=\` (equals), \`!=\` (not equals), \`~\` (contains), \`!~\` (not contains), \`^\` (starts with), \`>\`, \`>=\`, \`<\`, \`<=\`, \`?\` (is set/not empty).
|
|
1384
|
+
Multiple \`--filter\` flags are ANDed. Use \`--filter-json '{...}'\` for raw Attio filter JSON.
|
|
1385
|
+
|
|
1386
|
+
\`\`\`
|
|
1387
|
+
--filter 'name~Acme' --filter 'revenue>=1000000' --sort name:asc
|
|
1388
|
+
\`\`\`
|
|
1389
|
+
|
|
1390
|
+
### Values for create/update
|
|
1391
|
+
|
|
1392
|
+
\`--set key=value\` (repeatable), \`--values '{"key":"value"}'\`, \`--values @file.json\`, or pipe JSON to stdin.`;
|
|
1251
1393
|
|
|
1252
1394
|
// src/commands/open.ts
|
|
1253
|
-
import {
|
|
1395
|
+
import { execFile } from "child_process";
|
|
1254
1396
|
import { platform } from "os";
|
|
1255
1397
|
import chalk5 from "chalk";
|
|
1256
1398
|
function register14(program2) {
|
|
@@ -1273,7 +1415,7 @@ function register14(program2) {
|
|
|
1273
1415
|
url = `https://app.attio.com/${slug}/${object}`;
|
|
1274
1416
|
}
|
|
1275
1417
|
const cmd = platform() === "darwin" ? "open" : platform() === "win32" ? "start" : "xdg-open";
|
|
1276
|
-
|
|
1418
|
+
execFile(cmd, [url], (err) => {
|
|
1277
1419
|
if (err) {
|
|
1278
1420
|
console.log(url);
|
|
1279
1421
|
}
|
|
@@ -1281,31 +1423,146 @@ function register14(program2) {
|
|
|
1281
1423
|
});
|
|
1282
1424
|
}
|
|
1283
1425
|
|
|
1426
|
+
// src/commands/init.ts
|
|
1427
|
+
import { createInterface as createInterface2 } from "readline";
|
|
1428
|
+
import chalk6 from "chalk";
|
|
1429
|
+
function register15(program2) {
|
|
1430
|
+
program2.command("init").description("Interactive setup wizard \u2014 connect to your Attio workspace").action(async function() {
|
|
1431
|
+
const opts = this.optsWithGlobals();
|
|
1432
|
+
let apiKey = opts.apiKey;
|
|
1433
|
+
if (!apiKey) {
|
|
1434
|
+
if (!process.stdin.isTTY) {
|
|
1435
|
+
console.log("Non-interactive environment detected. To configure manually:\n");
|
|
1436
|
+
console.log(" export ATTIO_API_KEY=your_key");
|
|
1437
|
+
console.log(" # or");
|
|
1438
|
+
console.log(` attio config set api-key your_key`);
|
|
1439
|
+
console.log(` # or`);
|
|
1440
|
+
console.log(` attio init --api-key your_key
|
|
1441
|
+
`);
|
|
1442
|
+
console.log(`Get your API key at: https://app.attio.com/settings/developers`);
|
|
1443
|
+
return;
|
|
1444
|
+
}
|
|
1445
|
+
if (isConfigured()) {
|
|
1446
|
+
const rl2 = createInterface2({ input: process.stdin, output: process.stderr });
|
|
1447
|
+
const overwrite = await new Promise((resolve) => {
|
|
1448
|
+
rl2.question(chalk6.yellow("An API key is already configured. Overwrite? [y/N] "), (answer) => {
|
|
1449
|
+
rl2.close();
|
|
1450
|
+
resolve(answer.trim().toLowerCase() === "y");
|
|
1451
|
+
});
|
|
1452
|
+
});
|
|
1453
|
+
if (!overwrite) {
|
|
1454
|
+
console.error(chalk6.dim("Setup cancelled."));
|
|
1455
|
+
return;
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
console.error("");
|
|
1459
|
+
console.error(chalk6.bold(" Attio CLI Setup"));
|
|
1460
|
+
console.error(chalk6.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
1461
|
+
console.error("");
|
|
1462
|
+
console.error(` You'll need an API key from ${chalk6.cyan("https://app.attio.com/settings/developers")}`);
|
|
1463
|
+
console.error("");
|
|
1464
|
+
const rl = createInterface2({ input: process.stdin, output: process.stderr });
|
|
1465
|
+
apiKey = await new Promise((resolve, reject) => {
|
|
1466
|
+
rl.question(" Paste your API key: ", (answer) => {
|
|
1467
|
+
rl.close();
|
|
1468
|
+
resolve(answer);
|
|
1469
|
+
});
|
|
1470
|
+
rl.on("close", () => reject(new Error("cancelled")));
|
|
1471
|
+
}).catch(() => {
|
|
1472
|
+
console.error("");
|
|
1473
|
+
process.exit(0);
|
|
1474
|
+
});
|
|
1475
|
+
}
|
|
1476
|
+
apiKey = apiKey.trim().replace(/^['"]|['"]$/g, "");
|
|
1477
|
+
if (!apiKey) {
|
|
1478
|
+
console.error(chalk6.red("\n No API key provided."));
|
|
1479
|
+
console.error(` Get one at: ${chalk6.cyan("https://app.attio.com/settings/developers")}`);
|
|
1480
|
+
process.exit(1);
|
|
1481
|
+
}
|
|
1482
|
+
process.stderr.write(chalk6.dim(" Verifying..."));
|
|
1483
|
+
try {
|
|
1484
|
+
const client = new AttioClient(apiKey);
|
|
1485
|
+
const self = await client.get("/self");
|
|
1486
|
+
console.error(chalk6.green(" \u2713"));
|
|
1487
|
+
console.error("");
|
|
1488
|
+
const name = self.workspace_name || "your workspace";
|
|
1489
|
+
const slug = self.workspace_slug;
|
|
1490
|
+
console.error(` Connected to ${chalk6.bold(`"${name}"`)}${slug ? ` (${slug})` : ""}`);
|
|
1491
|
+
setApiKey(apiKey);
|
|
1492
|
+
console.error(` API key saved to ${chalk6.dim(getConfigPath())}`);
|
|
1493
|
+
console.error("");
|
|
1494
|
+
console.error(chalk6.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
1495
|
+
console.error(chalk6.bold(" Agent Setup") + chalk6.dim(" (optional)"));
|
|
1496
|
+
console.error("");
|
|
1497
|
+
console.error(" To let AI agents discover this CLI, run:");
|
|
1498
|
+
console.error("");
|
|
1499
|
+
console.error(` ${chalk6.cyan("attio config claude-md >> CLAUDE.md")}`);
|
|
1500
|
+
console.error("");
|
|
1501
|
+
console.error(` Done! Try ${chalk6.cyan("attio whoami")} or ${chalk6.cyan("attio companies list")} to get started.`);
|
|
1502
|
+
console.error("");
|
|
1503
|
+
} catch (err) {
|
|
1504
|
+
console.error(chalk6.red(" \u2717"));
|
|
1505
|
+
console.error("");
|
|
1506
|
+
if (err?.message?.includes("Invalid or expired") || err?.message?.includes("401") || err?.message?.includes("not recognised")) {
|
|
1507
|
+
console.error(chalk6.red(" Invalid API key."));
|
|
1508
|
+
console.error(` Double-check at: ${chalk6.cyan("https://app.attio.com/settings/developers")}`);
|
|
1509
|
+
} else {
|
|
1510
|
+
console.error(chalk6.red(` Could not connect: ${err?.message || err}`));
|
|
1511
|
+
}
|
|
1512
|
+
process.exit(1);
|
|
1513
|
+
}
|
|
1514
|
+
});
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1284
1517
|
// bin/attio.ts
|
|
1285
1518
|
function handleError(err) {
|
|
1519
|
+
const jsonMode = program.opts().json || !process.stdout.isTTY;
|
|
1286
1520
|
if (err instanceof AttioApiError) {
|
|
1287
|
-
|
|
1521
|
+
if (jsonMode) {
|
|
1522
|
+
console.error(JSON.stringify({ error: true, status: err.statusCode, type: err.type, message: err.detail }));
|
|
1523
|
+
} else {
|
|
1524
|
+
console.error(err.display());
|
|
1525
|
+
}
|
|
1288
1526
|
process.exit(err.exitCode);
|
|
1289
1527
|
}
|
|
1290
1528
|
if (err instanceof AttioAuthError) {
|
|
1291
|
-
|
|
1529
|
+
if (jsonMode) {
|
|
1530
|
+
console.error(JSON.stringify({ error: true, status: 401, type: "auth_error", message: err.message }));
|
|
1531
|
+
} else {
|
|
1532
|
+
console.error(err.display());
|
|
1533
|
+
}
|
|
1292
1534
|
process.exit(err.exitCode);
|
|
1293
1535
|
}
|
|
1294
1536
|
if (err instanceof AttioRateLimitError) {
|
|
1295
|
-
|
|
1537
|
+
if (jsonMode) {
|
|
1538
|
+
console.error(JSON.stringify({ error: true, status: 429, type: "rate_limit", message: err.message }));
|
|
1539
|
+
} else {
|
|
1540
|
+
console.error(err.display());
|
|
1541
|
+
}
|
|
1296
1542
|
process.exit(err.exitCode);
|
|
1297
1543
|
}
|
|
1298
1544
|
if (err instanceof Error) {
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1545
|
+
if (jsonMode) {
|
|
1546
|
+
console.error(JSON.stringify({ error: true, type: "unknown_error", message: err.message }));
|
|
1547
|
+
} else {
|
|
1548
|
+
console.error(chalk7.red(`Error: ${err.message}`));
|
|
1549
|
+
if (process.env.ATTIO_DEBUG || program.opts().debug) {
|
|
1550
|
+
console.error(err.stack);
|
|
1551
|
+
}
|
|
1302
1552
|
}
|
|
1303
1553
|
} else {
|
|
1304
|
-
|
|
1554
|
+
if (jsonMode) {
|
|
1555
|
+
console.error(JSON.stringify({ error: true, type: "unknown_error", message: "An unexpected error occurred" }));
|
|
1556
|
+
} else {
|
|
1557
|
+
console.error(chalk7.red("An unexpected error occurred"));
|
|
1558
|
+
}
|
|
1305
1559
|
}
|
|
1306
1560
|
process.exit(1);
|
|
1307
1561
|
}
|
|
1308
1562
|
program.name("attio").version("0.1.0").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
|
+
if (process.argv.includes("--no-color")) {
|
|
1564
|
+
process.env.NO_COLOR = "1";
|
|
1565
|
+
}
|
|
1309
1566
|
register(program);
|
|
1310
1567
|
register2(program);
|
|
1311
1568
|
register3(program);
|
|
@@ -1320,6 +1577,22 @@ register11(program);
|
|
|
1320
1577
|
register12(program);
|
|
1321
1578
|
register13(program);
|
|
1322
1579
|
register14(program);
|
|
1580
|
+
register15(program);
|
|
1581
|
+
program.action(() => {
|
|
1582
|
+
if (!isConfigured()) {
|
|
1583
|
+
console.error("");
|
|
1584
|
+
console.error(chalk7.bold(" Welcome to attio-cli!"));
|
|
1585
|
+
console.error("");
|
|
1586
|
+
console.error(` You haven't configured an API key yet. Run:`);
|
|
1587
|
+
console.error("");
|
|
1588
|
+
console.error(` ${chalk7.cyan("attio init")}`);
|
|
1589
|
+
console.error("");
|
|
1590
|
+
console.error(" to connect to your Attio workspace.");
|
|
1591
|
+
console.error("");
|
|
1592
|
+
} else {
|
|
1593
|
+
program.outputHelp();
|
|
1594
|
+
}
|
|
1595
|
+
});
|
|
1323
1596
|
program.parseAsync(process.argv).catch(handleError);
|
|
1324
1597
|
process.on("uncaughtException", handleError);
|
|
1325
1598
|
process.on("unhandledRejection", (reason) => handleError(reason));
|