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.
Files changed (3) hide show
  1. package/README.md +21 -6
  2. package/dist/attio.js +307 -34
  3. 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
- export ATTIO_API_KEY=your_key
15
- # or
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 chalk6 from "chalk";
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 response = await fetch(url, init);
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 expression, e.g. "name~Acme" (repeatable)',
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>", "Filter expression (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) => {
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>", "Filter expression (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) => {
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>", "Filter expression (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) => {
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. Run \`attio <command> --help\` for full usage.
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
- ### Common commands
1320
+ ### Records (CRUD \u2014 works for any object)
1233
1321
 
1234
1322
  \`\`\`
1235
- attio companies search <query> Search companies by name/domain
1236
- attio people search <query> Search people by name/email
1237
- attio records list <object> --filter <expr> List/filter records (companies, people, or custom objects)
1238
- attio tasks create --content <text> --deadline <ISO-date> --record <object:record-id>
1239
- attio notes create --object <obj> --record <id> --title <t> --content <text>
1240
- attio open <object> [record-id] Open in browser
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
- ### Output
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
- Auto-detects TTY: table for terminal, JSON when piped. Force with \`--json\`, \`--csv\`, or \`--table\`. Use \`-q\` for IDs only.
1339
+ ### Lists & entries
1246
1340
 
1247
- ### Filters & sorting
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
- \`--filter 'name~Acme'\` \`--filter 'revenue>=1000000'\` \`--sort name:asc\`
1250
- Multiple \`--filter\` flags are ANDed together.`;
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 { exec } from "child_process";
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
- exec(`${cmd} "${url}"`, (err) => {
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
- console.error(err.display());
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
- console.error(err.display());
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
- console.error(err.display());
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
- console.error(chalk6.red(`Error: ${err.message}`));
1300
- if (process.env.ATTIO_DEBUG || program.opts().debug) {
1301
- console.error(err.stack);
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
- console.error(chalk6.red("An unexpected error occurred"));
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));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "attio-cli",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
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",