attio-cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +175 -0
  3. package/dist/attio.js +1325 -0
  4. package/package.json +32 -0
package/dist/attio.js ADDED
@@ -0,0 +1,1325 @@
1
+ #!/usr/bin/env node
2
+
3
+ // bin/attio.ts
4
+ import { program } from "commander";
5
+ import chalk6 from "chalk";
6
+
7
+ // src/errors.ts
8
+ import chalk from "chalk";
9
+ var AttioApiError = class extends Error {
10
+ constructor(statusCode, type, detail) {
11
+ super(`Attio API Error: ${detail} (${statusCode})`);
12
+ this.statusCode = statusCode;
13
+ this.type = type;
14
+ this.detail = detail;
15
+ }
16
+ display() {
17
+ return [
18
+ chalk.red(`Error: ${this.detail} (${this.statusCode})`),
19
+ chalk.dim(` Type: ${this.type}`)
20
+ ].join("\n");
21
+ }
22
+ get exitCode() {
23
+ if (this.statusCode === 401 || this.statusCode === 403) return 2;
24
+ if (this.statusCode === 404) return 3;
25
+ if (this.statusCode === 400 || this.statusCode === 409) return 4;
26
+ if (this.statusCode === 429) return 5;
27
+ return 1;
28
+ }
29
+ };
30
+ var AttioAuthError = class extends Error {
31
+ display() {
32
+ return [
33
+ chalk.red("Error: Authentication failed"),
34
+ "",
35
+ " No valid API key found. Set one of:",
36
+ ` 1. ${chalk.cyan("ATTIO_API_KEY")} environment variable`,
37
+ ` 2. ${chalk.cyan("attio config set api-key <key>")}`,
38
+ ` 3. ${chalk.cyan("--api-key <key>")} flag`,
39
+ "",
40
+ ` Get your API key at: ${chalk.underline("https://app.attio.com/settings/developers")}`
41
+ ].join("\n");
42
+ }
43
+ get exitCode() {
44
+ return 2;
45
+ }
46
+ };
47
+ var AttioRateLimitError = class extends Error {
48
+ display() {
49
+ return chalk.red("Error: Rate limited after 3 retries. Try again in a few seconds.");
50
+ }
51
+ get exitCode() {
52
+ return 5;
53
+ }
54
+ };
55
+
56
+ // src/client.ts
57
+ import chalk2 from "chalk";
58
+
59
+ // src/config.ts
60
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
61
+ import { join } from "path";
62
+ import { homedir } from "os";
63
+ import * as dotenv from "dotenv";
64
+ dotenv.config();
65
+ var CONFIG_DIR = join(homedir(), ".config", "attio");
66
+ var CONFIG_FILE = join(CONFIG_DIR, "config.json");
67
+ function loadConfig() {
68
+ if (!existsSync(CONFIG_FILE)) return {};
69
+ try {
70
+ return JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
71
+ } catch {
72
+ return {};
73
+ }
74
+ }
75
+ function saveConfig(config2) {
76
+ mkdirSync(CONFIG_DIR, { recursive: true });
77
+ writeFileSync(CONFIG_FILE, JSON.stringify(config2, null, 2));
78
+ }
79
+ function resolveApiKey(flagValue) {
80
+ return flagValue || process.env.ATTIO_API_KEY || loadConfig().apiKey || "";
81
+ }
82
+ function setApiKey(key) {
83
+ const config2 = loadConfig();
84
+ config2.apiKey = key;
85
+ saveConfig(config2);
86
+ }
87
+ function getConfigPath() {
88
+ return CONFIG_FILE;
89
+ }
90
+
91
+ // src/client.ts
92
+ var BASE_URL = "https://api.attio.com/v2";
93
+ var MAX_RETRIES = 3;
94
+ var INITIAL_BACKOFF_MS = 1e3;
95
+ var AttioClient = class {
96
+ apiKey;
97
+ debug;
98
+ constructor(apiKey, debug) {
99
+ this.apiKey = resolveApiKey(apiKey);
100
+ this.debug = debug ?? false;
101
+ }
102
+ async request(method, path, body) {
103
+ const url = `${BASE_URL}${path}`;
104
+ if (this.debug) {
105
+ console.error(chalk2.dim(`\u2192 ${method} ${url}`));
106
+ }
107
+ if (!this.apiKey) {
108
+ throw new AttioAuthError("No API key configured");
109
+ }
110
+ const headers = {
111
+ "Authorization": `Bearer ${this.apiKey}`,
112
+ "Content-Type": "application/json"
113
+ };
114
+ const init = { method, headers };
115
+ if (body !== void 0) {
116
+ init.body = JSON.stringify(body);
117
+ }
118
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
119
+ const response = await fetch(url, init);
120
+ if (this.debug) {
121
+ console.error(chalk2.dim(`\u2190 ${response.status}`));
122
+ }
123
+ if (response.status === 429) {
124
+ if (attempt < MAX_RETRIES) {
125
+ const backoff = INITIAL_BACKOFF_MS * Math.pow(2, attempt);
126
+ await new Promise((resolve) => setTimeout(resolve, backoff));
127
+ continue;
128
+ }
129
+ throw new AttioRateLimitError("Rate limited after 3 retries");
130
+ }
131
+ if (response.status === 401) {
132
+ throw new AttioAuthError("Invalid or expired API key");
133
+ }
134
+ if (response.status === 204) {
135
+ return void 0;
136
+ }
137
+ const json = await response.json();
138
+ if (!response.ok) {
139
+ const errorType = json?.type ?? "unknown_error";
140
+ let errorDetail = json?.message ?? json?.detail ?? response.statusText;
141
+ if (json?.validation_errors?.length) {
142
+ const details = json.validation_errors.map(
143
+ (e) => `${e.path?.join(".") || "?"}: ${e.message}`
144
+ ).join("; ");
145
+ errorDetail += ` [${details}]`;
146
+ }
147
+ throw new AttioApiError(response.status, errorType, errorDetail);
148
+ }
149
+ return json;
150
+ }
151
+ throw new AttioRateLimitError("Rate limited after 3 retries");
152
+ }
153
+ async get(path) {
154
+ return this.request("GET", path);
155
+ }
156
+ async post(path, body) {
157
+ return this.request("POST", path, body);
158
+ }
159
+ async put(path, body) {
160
+ return this.request("PUT", path, body);
161
+ }
162
+ async patch(path, body) {
163
+ return this.request("PATCH", path, body);
164
+ }
165
+ async delete(path) {
166
+ return this.request("DELETE", path);
167
+ }
168
+ };
169
+
170
+ // src/output.ts
171
+ import chalk3 from "chalk";
172
+ import Table from "cli-table3";
173
+ import { createInterface } from "readline";
174
+ function detectFormat(opts) {
175
+ if (opts.quiet) return "quiet";
176
+ if (opts.json) return "json";
177
+ if (opts.csv) return "csv";
178
+ if (opts.table) return "table";
179
+ return process.stdout.isTTY ? "table" : "json";
180
+ }
181
+ function outputList(items, opts) {
182
+ const format = opts.format;
183
+ const idField = opts.idField || "id";
184
+ if (format === "quiet") {
185
+ for (const item of items) {
186
+ const raw = typeof item === "string" ? item : item[idField] || "";
187
+ const id = typeof raw === "object" && raw !== null ? Object.values(raw)[0] : raw;
188
+ if (id) console.log(id);
189
+ }
190
+ return;
191
+ }
192
+ if (format === "json") {
193
+ console.log(JSON.stringify(items, null, 2));
194
+ return;
195
+ }
196
+ if (items.length === 0) {
197
+ if (format === "table") console.log(chalk3.dim("No results."));
198
+ return;
199
+ }
200
+ const columns = opts.columns || Object.keys(items[0]).slice(0, 8);
201
+ if (format === "csv") {
202
+ console.log(columns.join(","));
203
+ for (const row of items) {
204
+ console.log(columns.map((c) => {
205
+ const v = String(row[c] ?? "");
206
+ return v.includes(",") || v.includes('"') || v.includes("\n") ? `"${v.replace(/"/g, '""')}"` : v;
207
+ }).join(","));
208
+ }
209
+ return;
210
+ }
211
+ const table = new Table({
212
+ head: columns.map((c) => chalk3.cyan(c)),
213
+ style: { head: [], border: [] },
214
+ wordWrap: true
215
+ });
216
+ for (const row of items) {
217
+ table.push(columns.map((c) => {
218
+ const v = String(row[c] ?? "");
219
+ return v.length > 60 ? v.slice(0, 57) + "..." : v;
220
+ }));
221
+ }
222
+ console.log(table.toString());
223
+ }
224
+ function outputSingle(item, opts) {
225
+ if (opts.format === "quiet") {
226
+ const raw = item[opts.idField || "id"] || "";
227
+ const id = typeof raw === "object" && raw !== null ? Object.values(raw)[0] : raw;
228
+ console.log(id || "");
229
+ return;
230
+ }
231
+ if (opts.format === "json") {
232
+ console.log(JSON.stringify(item, null, 2));
233
+ return;
234
+ }
235
+ const table = new Table({ style: { head: [], border: [] } });
236
+ for (const [key, value] of Object.entries(item)) {
237
+ const display = typeof value === "object" ? JSON.stringify(value) : String(value ?? "");
238
+ table.push({ [chalk3.cyan(key)]: display.length > 80 ? display.slice(0, 77) + "..." : display });
239
+ }
240
+ console.log(table.toString());
241
+ }
242
+ async function confirm(message) {
243
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
244
+ return new Promise((resolve) => {
245
+ rl.question(`${message} [y/N] `, (answer) => {
246
+ rl.close();
247
+ resolve(answer.toLowerCase() === "y");
248
+ });
249
+ });
250
+ }
251
+
252
+ // src/commands/whoami.ts
253
+ function register(program2) {
254
+ program2.command("whoami").description("Show current workspace and authenticated user info").action(async function() {
255
+ const opts = this.optsWithGlobals();
256
+ const client = new AttioClient(opts.apiKey, opts.debug);
257
+ const format = detectFormat(opts);
258
+ const data = await client.get("/self");
259
+ if (format === "json") {
260
+ outputSingle(data, { format });
261
+ return;
262
+ }
263
+ if (format === "quiet") {
264
+ console.log(data.workspace_id || "");
265
+ return;
266
+ }
267
+ const flat = {
268
+ workspace_id: data.workspace_id || "",
269
+ workspace_name: data.workspace_name || "",
270
+ workspace_slug: data.workspace_slug || "",
271
+ authorized_by: data.authorized_by_workspace_member_id || "",
272
+ scope: data.scope || ""
273
+ };
274
+ outputSingle(flat, { format, idField: "workspace_id" });
275
+ });
276
+ }
277
+
278
+ // src/commands/objects.ts
279
+ function register2(program2) {
280
+ const objects = program2.command("objects").description("Manage workspace objects");
281
+ objects.command("list").description("List all objects in the workspace").action(async function() {
282
+ const opts = this.optsWithGlobals();
283
+ const client = new AttioClient(opts.apiKey, opts.debug);
284
+ const format = detectFormat(opts);
285
+ const response = await client.get("/objects");
286
+ const items = response.data;
287
+ outputList(items, {
288
+ format,
289
+ columns: ["api_slug", "singular_noun", "plural_noun"],
290
+ idField: "api_slug"
291
+ });
292
+ });
293
+ objects.command("get").description("Get details of a specific object").argument("<slug>", "Object API slug (e.g. people, companies)").action(async function(slug) {
294
+ const opts = this.optsWithGlobals();
295
+ const client = new AttioClient(opts.apiKey, opts.debug);
296
+ const format = detectFormat(opts);
297
+ const response = await client.get(`/objects/${encodeURIComponent(slug)}`);
298
+ const obj = response.data;
299
+ outputSingle(obj, { format, idField: "api_slug" });
300
+ });
301
+ }
302
+
303
+ // src/commands/attributes.ts
304
+ function register3(program2) {
305
+ const attributes = program2.command("attributes").description("Manage object attributes");
306
+ attributes.command("list").description("List attributes for an object").argument("<object>", "Object API slug (e.g. people, companies)").action(async function(object) {
307
+ const opts = this.optsWithGlobals();
308
+ const client = new AttioClient(opts.apiKey, opts.debug);
309
+ const format = detectFormat(opts);
310
+ const response = await client.get(
311
+ `/objects/${encodeURIComponent(object)}/attributes`
312
+ );
313
+ const items = response.data;
314
+ outputList(items, {
315
+ format,
316
+ columns: ["api_slug", "title", "type", "is_required", "is_unique", "is_multiselect"],
317
+ idField: "api_slug"
318
+ });
319
+ });
320
+ }
321
+
322
+ // src/filters.ts
323
+ var OPERATORS = [
324
+ { cli: ">=", attio: "$gte", numeric: true },
325
+ { cli: "<=", attio: "$lte", numeric: true },
326
+ { cli: "!=", attio: "$not", wrapEq: true },
327
+ { cli: ">", attio: "$gt", numeric: true },
328
+ { cli: "<", attio: "$lt", numeric: true },
329
+ { cli: "!~", attio: "$not", wrapContains: true },
330
+ { cli: "~", attio: "$contains" },
331
+ { cli: "^", attio: "$starts_with" },
332
+ { cli: "?", attio: "$not_empty", unary: true },
333
+ { cli: "=", attio: "$eq" }
334
+ ];
335
+ function parseFilterFlag(filterStr) {
336
+ if (filterStr.endsWith("?")) {
337
+ const attr = filterStr.slice(0, -1);
338
+ return { [attr]: { "$not_empty": true } };
339
+ }
340
+ for (const op of OPERATORS) {
341
+ if ("unary" in op && op.unary) continue;
342
+ const idx = filterStr.indexOf(op.cli);
343
+ if (idx === -1) continue;
344
+ const attribute = filterStr.slice(0, idx).trim();
345
+ const rawValue = filterStr.slice(idx + op.cli.length).trim();
346
+ const value = "numeric" in op && op.numeric && !isNaN(Number(rawValue)) ? Number(rawValue) : rawValue;
347
+ if (op.cli === "=") return { [attribute]: value };
348
+ if (op.cli === "!=") return { "$not": { [attribute]: value } };
349
+ if (op.cli === "!~") return { "$not": { [attribute]: { "$contains": value } } };
350
+ return { [attribute]: { [op.attio]: value } };
351
+ }
352
+ throw new Error(`Invalid filter syntax: "${filterStr}". Expected: attribute<operator>value`);
353
+ }
354
+ function combineFilters(filters) {
355
+ if (filters.length === 0) return {};
356
+ if (filters.length === 1) return filters[0];
357
+ return { "$and": filters };
358
+ }
359
+ function parseSort(sortStr) {
360
+ const [attrPart, direction = "asc"] = sortStr.split(":");
361
+ const [attribute, field] = attrPart.split(".");
362
+ return { attribute, ...field ? { field } : {}, direction };
363
+ }
364
+
365
+ // src/values.ts
366
+ function flattenValue(attrValues) {
367
+ if (!attrValues || attrValues.length === 0) return "";
368
+ if (attrValues.length > 1) return attrValues.map((v) => flattenSingleValue(v)).join(", ");
369
+ return flattenSingleValue(attrValues[0]);
370
+ }
371
+ function flattenSingleValue(val) {
372
+ if (!val) return "";
373
+ const type = val.attribute_type;
374
+ switch (type) {
375
+ case "text":
376
+ case "number":
377
+ case "checkbox":
378
+ case "date":
379
+ case "timestamp":
380
+ case "rating":
381
+ return String(val.value ?? "");
382
+ case "currency":
383
+ return String(val.currency_value ?? val.value ?? "");
384
+ case "personal-name":
385
+ return val.full_name || `${val.first_name || ""} ${val.last_name || ""}`.trim();
386
+ case "email-address":
387
+ return val.email_address || val.original_email_address || "";
388
+ case "phone-number":
389
+ return val.original_phone_number || val.phone_number || "";
390
+ case "domain":
391
+ return val.domain || "";
392
+ case "select":
393
+ return val.option?.title || "";
394
+ case "status":
395
+ return val.status?.title || "";
396
+ case "location":
397
+ return [val.locality, val.region, val.country_code].filter(Boolean).join(", ");
398
+ case "record-reference":
399
+ return `${val.target_object}:${val.target_record_id}`;
400
+ case "actor-reference":
401
+ return `member:${val.referenced_actor_id}`;
402
+ case "interaction":
403
+ return `${val.interaction_type} @ ${val.interacted_at?.slice(0, 10) || ""}`;
404
+ default:
405
+ return String(val.value ?? val.title ?? JSON.stringify(val));
406
+ }
407
+ }
408
+ function flattenRecord(record) {
409
+ const flat = {
410
+ id: record.id?.record_id || record.id?.entry_id || "",
411
+ created_at: record.created_at?.slice(0, 10) || ""
412
+ };
413
+ if (record.web_url) flat.web_url = record.web_url;
414
+ for (const [key, values] of Object.entries(record.values || {})) {
415
+ flat[key] = flattenValue(values);
416
+ }
417
+ return flat;
418
+ }
419
+ function parseSets(sets) {
420
+ const values = {};
421
+ for (const set of sets) {
422
+ const eqIdx = set.indexOf("=");
423
+ if (eqIdx === -1) throw new Error(`Invalid --set format: "${set}". Expected: key=value`);
424
+ const key = set.slice(0, eqIdx).trim();
425
+ const raw = set.slice(eqIdx + 1).trim();
426
+ if (raw === "true") {
427
+ values[key] = true;
428
+ continue;
429
+ }
430
+ if (raw === "false") {
431
+ values[key] = false;
432
+ continue;
433
+ }
434
+ if (/^-?\d+(\.\d+)?$/.test(raw)) {
435
+ values[key] = Number(raw);
436
+ continue;
437
+ }
438
+ if (raw.startsWith("[") && raw.endsWith("]")) {
439
+ values[key] = raw.slice(1, -1).split(",").map((s) => s.trim());
440
+ continue;
441
+ }
442
+ values[key] = raw;
443
+ }
444
+ return values;
445
+ }
446
+ async function readStdin() {
447
+ if (process.stdin.isTTY) return "";
448
+ const chunks = [];
449
+ for await (const chunk of process.stdin) {
450
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
451
+ }
452
+ return Buffer.concat(chunks).toString("utf8");
453
+ }
454
+ async function resolveValues(options) {
455
+ if (options.values) {
456
+ if (options.values.startsWith("@")) {
457
+ const { readFileSync: readFileSync2 } = await import("fs");
458
+ return JSON.parse(readFileSync2(options.values.slice(1), "utf-8"));
459
+ }
460
+ return JSON.parse(options.values);
461
+ }
462
+ if (options.set && options.set.length > 0) {
463
+ return parseSets(options.set);
464
+ }
465
+ const stdin = await readStdin();
466
+ if (stdin.trim()) return JSON.parse(stdin);
467
+ return {};
468
+ }
469
+
470
+ // src/pagination.ts
471
+ async function paginate(fetchPage, options) {
472
+ if (!options.all) {
473
+ return fetchPage(options.limit, options.offset);
474
+ }
475
+ const allResults = [];
476
+ let offset = 0;
477
+ const pageSize = 500;
478
+ while (true) {
479
+ const page = await fetchPage(pageSize, offset);
480
+ allResults.push(...page);
481
+ if (page.length < pageSize) break;
482
+ offset += pageSize;
483
+ }
484
+ return allResults;
485
+ }
486
+
487
+ // src/commands/records.ts
488
+ async function listRecords(object, cmdOpts) {
489
+ const client = new AttioClient(cmdOpts.apiKey, cmdOpts.debug);
490
+ const format = detectFormat(cmdOpts);
491
+ let filter;
492
+ if (cmdOpts.filterJson) {
493
+ filter = JSON.parse(cmdOpts.filterJson);
494
+ } else if (cmdOpts.filter && cmdOpts.filter.length > 0) {
495
+ const parsed = cmdOpts.filter.map(parseFilterFlag);
496
+ filter = combineFilters(parsed);
497
+ }
498
+ let sorts;
499
+ if (cmdOpts.sort && cmdOpts.sort.length > 0) {
500
+ sorts = cmdOpts.sort.map(parseSort);
501
+ }
502
+ const limit = Number(cmdOpts.limit) || 25;
503
+ const offset = Number(cmdOpts.offset) || 0;
504
+ const all = cmdOpts.all ?? false;
505
+ const fetchPage = async (pageLimit, pageOffset) => {
506
+ const body = {};
507
+ if (filter && Object.keys(filter).length > 0) body.filter = filter;
508
+ if (sorts && sorts.length > 0) body.sorts = sorts;
509
+ body.limit = pageLimit;
510
+ body.offset = pageOffset;
511
+ const res = await client.post(
512
+ `/objects/${encodeURIComponent(object)}/records/query`,
513
+ body
514
+ );
515
+ return res.data;
516
+ };
517
+ const records = await paginate(fetchPage, { limit, offset, all });
518
+ if (format === "quiet") {
519
+ for (const r of records) {
520
+ console.log(r.id?.record_id ?? "");
521
+ }
522
+ return;
523
+ }
524
+ if (format === "json") {
525
+ outputList(records, { format });
526
+ return;
527
+ }
528
+ const flat = records.map(flattenRecord);
529
+ outputList(flat, { format });
530
+ }
531
+ async function getRecord(object, recordId, cmdOpts) {
532
+ const client = new AttioClient(cmdOpts.apiKey, cmdOpts.debug);
533
+ const format = detectFormat(cmdOpts);
534
+ const res = await client.get(
535
+ `/objects/${encodeURIComponent(object)}/records/${encodeURIComponent(recordId)}`
536
+ );
537
+ const record = res.data;
538
+ if (format === "quiet") {
539
+ console.log(record.id?.record_id ?? "");
540
+ return;
541
+ }
542
+ if (format === "json") {
543
+ outputSingle(record, { format });
544
+ return;
545
+ }
546
+ outputSingle(flattenRecord(record), { format });
547
+ }
548
+ async function createRecord(object, cmdOpts) {
549
+ const client = new AttioClient(cmdOpts.apiKey, cmdOpts.debug);
550
+ const format = detectFormat(cmdOpts);
551
+ const values = await resolveValues(cmdOpts);
552
+ const res = await client.post(
553
+ `/objects/${encodeURIComponent(object)}/records`,
554
+ { data: { values } }
555
+ );
556
+ const record = res.data;
557
+ if (format === "quiet") {
558
+ console.log(record.id?.record_id ?? "");
559
+ return;
560
+ }
561
+ if (format === "json") {
562
+ outputSingle(record, { format });
563
+ return;
564
+ }
565
+ outputSingle(flattenRecord(record), { format });
566
+ }
567
+ async function updateRecord(object, recordId, cmdOpts) {
568
+ const client = new AttioClient(cmdOpts.apiKey, cmdOpts.debug);
569
+ const format = detectFormat(cmdOpts);
570
+ const values = await resolveValues(cmdOpts);
571
+ const res = await client.patch(
572
+ `/objects/${encodeURIComponent(object)}/records/${encodeURIComponent(recordId)}`,
573
+ { data: { values } }
574
+ );
575
+ const record = res.data;
576
+ if (format === "quiet") {
577
+ console.log(record.id?.record_id ?? "");
578
+ return;
579
+ }
580
+ if (format === "json") {
581
+ outputSingle(record, { format });
582
+ return;
583
+ }
584
+ outputSingle(flattenRecord(record), { format });
585
+ }
586
+ async function deleteRecord(object, recordId, cmdOpts) {
587
+ const yes = cmdOpts.yes ?? false;
588
+ if (!yes) {
589
+ const ok = await confirm(`Delete record ${recordId} from ${object}?`);
590
+ if (!ok) {
591
+ console.error("Aborted.");
592
+ return;
593
+ }
594
+ }
595
+ const client = new AttioClient(cmdOpts.apiKey, cmdOpts.debug);
596
+ await client.delete(
597
+ `/objects/${encodeURIComponent(object)}/records/${encodeURIComponent(recordId)}`
598
+ );
599
+ console.error("Deleted.");
600
+ }
601
+ async function upsertRecord(object, cmdOpts) {
602
+ const client = new AttioClient(cmdOpts.apiKey, cmdOpts.debug);
603
+ const format = detectFormat(cmdOpts);
604
+ const matchAttr = cmdOpts.match;
605
+ if (!matchAttr) {
606
+ throw new Error("--match <attribute-slug> is required for upsert");
607
+ }
608
+ const values = await resolveValues(cmdOpts);
609
+ const res = await client.put(
610
+ `/objects/${encodeURIComponent(object)}/records?matching_attribute=${encodeURIComponent(matchAttr)}`,
611
+ { data: { values } }
612
+ );
613
+ const record = res.data;
614
+ if (format === "quiet") {
615
+ console.log(record.id?.record_id ?? "");
616
+ return;
617
+ }
618
+ if (format === "json") {
619
+ outputSingle(record, { format });
620
+ return;
621
+ }
622
+ outputSingle(flattenRecord(record), { format });
623
+ }
624
+ async function searchRecords(object, query, cmdOpts) {
625
+ const client = new AttioClient(cmdOpts.apiKey, cmdOpts.debug);
626
+ const format = detectFormat(cmdOpts);
627
+ const limit = Number(cmdOpts.limit) || 25;
628
+ const res = await client.post(
629
+ "/objects/records/search",
630
+ {
631
+ query,
632
+ objects: [object],
633
+ request_as: { type: "workspace" },
634
+ limit
635
+ }
636
+ );
637
+ const records = res.data;
638
+ if (format === "quiet") {
639
+ for (const r of records) {
640
+ console.log(r.id?.record_id ?? "");
641
+ }
642
+ return;
643
+ }
644
+ if (format === "json") {
645
+ outputList(records, { format });
646
+ return;
647
+ }
648
+ const flat = records.map(flattenRecord);
649
+ outputList(flat, { format });
650
+ }
651
+ function register4(program2) {
652
+ const records = program2.command("records").description("Manage records in any Attio object");
653
+ records.command("list").description("List or query records for an object").argument("<object>", "Object slug or ID (e.g. companies, people)").option(
654
+ "--filter <expr>",
655
+ 'Filter expression, e.g. "name~Acme" (repeatable)',
656
+ (val, prev) => [...prev, val],
657
+ []
658
+ ).option("--filter-json <json>", "Raw JSON filter (overrides --filter)").option(
659
+ "--sort <expr>",
660
+ 'Sort expression, e.g. "name:asc" (repeatable)',
661
+ (val, prev) => [...prev, val],
662
+ []
663
+ ).option("--limit <n>", "Maximum records to return", "25").option("--offset <n>", "Number of records to skip", "0").option("--all", "Auto-paginate to fetch all records").action(async (object, _opts, cmd) => {
664
+ const opts = cmd.optsWithGlobals();
665
+ await listRecords(object, opts);
666
+ });
667
+ records.command("get").description("Get a single record by ID").argument("<object>", "Object slug or ID").argument("<record-id>", "Record ID").action(async (object, recordId, _opts, cmd) => {
668
+ const opts = cmd.optsWithGlobals();
669
+ await getRecord(object, recordId, opts);
670
+ });
671
+ records.command("create").description("Create a new record").argument("<object>", "Object slug or ID").option("--values <json>", "JSON string or @file of attribute values").option(
672
+ "--set <key=value>",
673
+ "Set an attribute value (repeatable)",
674
+ (val, prev) => [...prev, val],
675
+ []
676
+ ).action(async (object, _opts, cmd) => {
677
+ const opts = cmd.optsWithGlobals();
678
+ await createRecord(object, opts);
679
+ });
680
+ records.command("update").description("Update an existing record").argument("<object>", "Object slug or ID").argument("<record-id>", "Record ID").option("--values <json>", "JSON string or @file of attribute values").option(
681
+ "--set <key=value>",
682
+ "Set an attribute value (repeatable)",
683
+ (val, prev) => [...prev, val],
684
+ []
685
+ ).action(async (object, recordId, _opts, cmd) => {
686
+ const opts = cmd.optsWithGlobals();
687
+ await updateRecord(object, recordId, opts);
688
+ });
689
+ records.command("delete").description("Delete a record").argument("<object>", "Object slug or ID").argument("<record-id>", "Record ID").option("-y, --yes", "Skip confirmation prompt").action(async (object, recordId, _opts, cmd) => {
690
+ const opts = cmd.optsWithGlobals();
691
+ await deleteRecord(object, recordId, opts);
692
+ });
693
+ records.command("upsert").description("Create or update a record by matching attribute").argument("<object>", "Object slug or ID").requiredOption("--match <attribute-slug>", "Attribute slug to match on (required)").option("--values <json>", "JSON string or @file of attribute values").option(
694
+ "--set <key=value>",
695
+ "Set an attribute value (repeatable)",
696
+ (val, prev) => [...prev, val],
697
+ []
698
+ ).action(async (object, _opts, cmd) => {
699
+ const opts = cmd.optsWithGlobals();
700
+ await upsertRecord(object, opts);
701
+ });
702
+ records.command("search").description("Full-text search for records within an object").argument("<object>", "Object slug or ID (e.g. companies, people)").argument("<query>", "Search query string").option("--limit <n>", "Maximum results to return", "25").action(async (object, query, _opts, cmd) => {
703
+ const opts = cmd.optsWithGlobals();
704
+ await searchRecords(object, query, opts);
705
+ });
706
+ }
707
+
708
+ // src/commands/people.ts
709
+ function register5(program2) {
710
+ 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) => {
712
+ await listRecords("people", command.optsWithGlobals());
713
+ });
714
+ cmd.command("get <record-id>").description("Get a person by record ID").action(async (recordId, options, command) => {
715
+ await getRecord("people", recordId, command.optsWithGlobals());
716
+ });
717
+ cmd.command("create").description("Create a person").option("--values <json>", "Values as JSON string or @file").option("--set <key=value>", "Set a field value (repeatable)", (v, p) => [...p, v], []).action(async (options, command) => {
718
+ await createRecord("people", command.optsWithGlobals());
719
+ });
720
+ cmd.command("update <record-id>").description("Update a person").option("--values <json>", "Values as JSON string or @file").option("--set <key=value>", "Set a field value (repeatable)", (v, p) => [...p, v], []).action(async (recordId, options, command) => {
721
+ await updateRecord("people", recordId, command.optsWithGlobals());
722
+ });
723
+ cmd.command("delete <record-id>").description("Delete a person").option("-y, --yes", "Skip confirmation").action(async (recordId, options, command) => {
724
+ await deleteRecord("people", recordId, command.optsWithGlobals());
725
+ });
726
+ cmd.command("search <query>").description("Search people by name or email").option("--limit <n>", "Maximum results", "25").action(async (query, options, command) => {
727
+ await searchRecords("people", query, command.optsWithGlobals());
728
+ });
729
+ }
730
+
731
+ // src/commands/companies.ts
732
+ function register6(program2) {
733
+ 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) => {
735
+ await listRecords("companies", command.optsWithGlobals());
736
+ });
737
+ cmd.command("get <record-id>").description("Get a company by record ID").action(async (recordId, options, command) => {
738
+ await getRecord("companies", recordId, command.optsWithGlobals());
739
+ });
740
+ cmd.command("create").description("Create a company").option("--values <json>", "Values as JSON string or @file").option("--set <key=value>", "Set a field value (repeatable)", (v, p) => [...p, v], []).action(async (options, command) => {
741
+ await createRecord("companies", command.optsWithGlobals());
742
+ });
743
+ cmd.command("update <record-id>").description("Update a company").option("--values <json>", "Values as JSON string or @file").option("--set <key=value>", "Set a field value (repeatable)", (v, p) => [...p, v], []).action(async (recordId, options, command) => {
744
+ await updateRecord("companies", recordId, command.optsWithGlobals());
745
+ });
746
+ cmd.command("delete <record-id>").description("Delete a company").option("-y, --yes", "Skip confirmation").action(async (recordId, options, command) => {
747
+ await deleteRecord("companies", recordId, command.optsWithGlobals());
748
+ });
749
+ cmd.command("search <query>").description("Search companies by name or domain").option("--limit <n>", "Maximum results", "25").action(async (query, options, command) => {
750
+ await searchRecords("companies", query, command.optsWithGlobals());
751
+ });
752
+ }
753
+
754
+ // src/commands/lists.ts
755
+ function register7(program2) {
756
+ const cmd = program2.command("lists").description("Manage lists");
757
+ cmd.command("list").description("List all lists").action(async (_options, command) => {
758
+ const opts = command.optsWithGlobals();
759
+ const client = new AttioClient(opts.apiKey, opts.debug);
760
+ const format = detectFormat(opts);
761
+ const res = await client.get("/lists");
762
+ const lists = res.data;
763
+ const flat = lists.map((l) => ({
764
+ id: l.id?.list_id || "",
765
+ api_slug: l.api_slug || "",
766
+ name: l.name || "",
767
+ parent_object: l.parent_object || ""
768
+ }));
769
+ outputList(flat, {
770
+ format,
771
+ columns: ["id", "api_slug", "name", "parent_object"],
772
+ idField: "id"
773
+ });
774
+ });
775
+ cmd.command("get <list>").description("Get a list by ID or slug").action(async (list, _options, command) => {
776
+ const opts = command.optsWithGlobals();
777
+ const client = new AttioClient(opts.apiKey, opts.debug);
778
+ const format = detectFormat(opts);
779
+ const res = await client.get(`/lists/${list}`);
780
+ const listData = res.data;
781
+ if (format === "json") {
782
+ outputSingle(listData, { format, idField: "id" });
783
+ return;
784
+ }
785
+ const flat = {
786
+ id: listData.id?.list_id || "",
787
+ api_slug: listData.api_slug || "",
788
+ name: listData.name || "",
789
+ parent_object: listData.parent_object || "",
790
+ workspace_access: listData.workspace_access || "",
791
+ created_by_actor_type: listData.created_by_actor?.type || "",
792
+ created_by_actor_id: listData.created_by_actor?.id || ""
793
+ };
794
+ outputSingle(flat, { format, idField: "id" });
795
+ });
796
+ }
797
+
798
+ // src/commands/entries.ts
799
+ function flattenEntry(entry) {
800
+ const flat = {
801
+ id: entry.id?.entry_id || "",
802
+ record_id: entry.record_id || entry.parent_record_id || "",
803
+ created_at: entry.created_at?.slice(0, 10) || ""
804
+ };
805
+ for (const [key, values] of Object.entries(entry.entry_values || {})) {
806
+ flat[key] = flattenValue(values);
807
+ }
808
+ return flat;
809
+ }
810
+ function register8(program2) {
811
+ 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) => {
813
+ const opts = command.optsWithGlobals();
814
+ const client = new AttioClient(opts.apiKey, opts.debug);
815
+ const format = detectFormat(opts);
816
+ let filter;
817
+ if (opts.filterJson) {
818
+ filter = JSON.parse(opts.filterJson);
819
+ } else if (opts.filter && opts.filter.length > 0) {
820
+ const parsed = opts.filter.map(parseFilterFlag);
821
+ filter = combineFilters(parsed);
822
+ }
823
+ const sorts = (opts.sort || []).map((s) => parseSort(s));
824
+ const limit = parseInt(opts.limit, 10) || 25;
825
+ const offset = parseInt(opts.offset, 10) || 0;
826
+ const entries = await paginate(
827
+ async (pageLimit, pageOffset) => {
828
+ const body = { limit: pageLimit, offset: pageOffset };
829
+ if (filter && Object.keys(filter).length > 0) body.filter = filter;
830
+ if (sorts.length > 0) body.sorts = sorts;
831
+ const res = await client.post(`/lists/${list}/entries/query`, body);
832
+ return res.data;
833
+ },
834
+ { limit, offset, all: !!opts.all }
835
+ );
836
+ if (format === "json") {
837
+ outputList(entries, { format, idField: "id" });
838
+ return;
839
+ }
840
+ const flat = entries.map(flattenEntry);
841
+ outputList(flat, { format, idField: "id" });
842
+ });
843
+ cmd.command("get <list> <entry-id>").description("Get an entry by ID").action(async (list, entryId, _options, command) => {
844
+ const opts = command.optsWithGlobals();
845
+ const client = new AttioClient(opts.apiKey, opts.debug);
846
+ const format = detectFormat(opts);
847
+ const res = await client.get(`/lists/${list}/entries/${entryId}`);
848
+ const entry = res.data;
849
+ if (format === "json") {
850
+ outputSingle(entry, { format, idField: "id" });
851
+ return;
852
+ }
853
+ const flat = flattenEntry(entry);
854
+ outputSingle(flat, { format, idField: "id" });
855
+ });
856
+ cmd.command("create <list>").description("Create a new entry in a list").requiredOption("--record <record-id>", "Parent record ID (required)").requiredOption("--object <parent-object>", "Parent object slug (required)").option("--values <json>", "Entry values as JSON string or @file").option("--set <key=value>", "Set a field value (repeatable)", (v, p) => [...p, v], []).action(async (list, _options, command) => {
857
+ const opts = command.optsWithGlobals();
858
+ const client = new AttioClient(opts.apiKey, opts.debug);
859
+ const format = detectFormat(opts);
860
+ const resolvedValues = await resolveValues({ values: opts.values, set: opts.set });
861
+ const body = {
862
+ data: {
863
+ parent_record_id: opts.record,
864
+ parent_object: opts.object,
865
+ entry_values: resolvedValues
866
+ }
867
+ };
868
+ const res = await client.post(`/lists/${list}/entries`, body);
869
+ const entry = res.data;
870
+ if (format === "json") {
871
+ outputSingle(entry, { format, idField: "id" });
872
+ return;
873
+ }
874
+ const flat = flattenEntry(entry);
875
+ outputSingle(flat, { format, idField: "id" });
876
+ });
877
+ cmd.command("update <list> <entry-id>").description("Update an entry").option("--values <json>", "Entry values as JSON string or @file").option("--set <key=value>", "Set a field value (repeatable)", (v, p) => [...p, v], []).action(async (list, entryId, _options, command) => {
878
+ const opts = command.optsWithGlobals();
879
+ const client = new AttioClient(opts.apiKey, opts.debug);
880
+ const format = detectFormat(opts);
881
+ const resolvedValues = await resolveValues({ values: opts.values, set: opts.set });
882
+ const body = {
883
+ data: {
884
+ entry_values: resolvedValues
885
+ }
886
+ };
887
+ const res = await client.patch(`/lists/${list}/entries/${entryId}`, body);
888
+ const entry = res.data;
889
+ if (format === "json") {
890
+ outputSingle(entry, { format, idField: "id" });
891
+ return;
892
+ }
893
+ const flat = flattenEntry(entry);
894
+ outputSingle(flat, { format, idField: "id" });
895
+ });
896
+ cmd.command("delete <list> <entry-id>").description("Delete an entry").option("-y, --yes", "Skip confirmation").action(async (list, entryId, _options, command) => {
897
+ const opts = command.optsWithGlobals();
898
+ const client = new AttioClient(opts.apiKey, opts.debug);
899
+ if (!opts.yes) {
900
+ const ok = await confirm(`Delete entry ${entryId} from list ${list}?`);
901
+ if (!ok) {
902
+ console.error("Aborted.");
903
+ return;
904
+ }
905
+ }
906
+ await client.delete(`/lists/${list}/entries/${entryId}`);
907
+ console.error(`Deleted entry ${entryId}.`);
908
+ });
909
+ }
910
+
911
+ // src/commands/tasks.ts
912
+ function register9(program2) {
913
+ const tasks = program2.command("tasks").description("Manage tasks");
914
+ tasks.command("list").description("List tasks").option("--assignee <member-id>", "Filter by assignee workspace member ID").option("--is-completed", "Filter to only completed tasks").option("--linked-object <obj>", "Filter by linked object slug").option("--linked-record-id <id>", "Filter by linked record ID").option("--limit <n>", "Maximum tasks to return", "25").option("--offset <n>", "Number of tasks to skip", "0").option("--sort <expr>", "Sort expression").action(async (_options, command) => {
915
+ const opts = command.optsWithGlobals();
916
+ const client = new AttioClient(opts.apiKey, opts.debug);
917
+ const format = detectFormat(opts);
918
+ const params = new URLSearchParams();
919
+ params.set("limit", String(opts.limit ?? 25));
920
+ params.set("offset", String(opts.offset ?? 0));
921
+ if (opts.assignee) params.set("assignee", opts.assignee);
922
+ if (opts.isCompleted) params.set("is_completed", "true");
923
+ if (opts.linkedObject) params.set("linked_object", opts.linkedObject);
924
+ if (opts.linkedRecordId) params.set("linked_record_id", opts.linkedRecordId);
925
+ if (opts.sort) params.set("sort", opts.sort);
926
+ const res = await client.get(`/tasks?${params.toString()}`);
927
+ const tasksList = res.data;
928
+ const flat = tasksList.map((t) => ({
929
+ id: t.id?.task_id || "",
930
+ content: truncate(t.content_plaintext || "", 60),
931
+ deadline: t.deadline_at || "",
932
+ completed: t.is_completed ?? false
933
+ }));
934
+ outputList(flat, {
935
+ format,
936
+ columns: ["id", "content", "deadline", "completed"],
937
+ idField: "id"
938
+ });
939
+ });
940
+ tasks.command("get <task-id>").description("Get a task by ID").action(async (taskId, _options, command) => {
941
+ const opts = command.optsWithGlobals();
942
+ const client = new AttioClient(opts.apiKey, opts.debug);
943
+ const format = detectFormat(opts);
944
+ const res = await client.get(`/tasks/${encodeURIComponent(taskId)}`);
945
+ outputSingle(res.data, { format, idField: "id" });
946
+ });
947
+ tasks.command("create").description("Create a new task").requiredOption("--content <text>", "Task content (required)").option(
948
+ "--assignee <member-id>",
949
+ "Assignee workspace member ID (repeatable)",
950
+ (val, prev) => [...prev, val],
951
+ []
952
+ ).option("--deadline <ISO-date>", "Deadline in ISO-8601 format").option(
953
+ "--record <object:record-id>",
954
+ "Link a record as object:record-id (repeatable)",
955
+ (val, prev) => [...prev, val],
956
+ []
957
+ ).action(async (_options, command) => {
958
+ const opts = command.optsWithGlobals();
959
+ const client = new AttioClient(opts.apiKey, opts.debug);
960
+ const format = detectFormat(opts);
961
+ const body = {
962
+ content: opts.content,
963
+ format: "plaintext",
964
+ is_completed: false,
965
+ deadline_at: opts.deadline || null,
966
+ assignees: opts.assignee && opts.assignee.length > 0 ? opts.assignee.map((id) => ({
967
+ referenced_actor_type: "workspace-member",
968
+ referenced_actor_id: id
969
+ })) : [],
970
+ linked_records: opts.record && opts.record.length > 0 ? opts.record.map((r) => {
971
+ const [targetObject, targetRecordId] = r.split(":");
972
+ return { target_object: targetObject, target_record_id: targetRecordId };
973
+ }) : []
974
+ };
975
+ const res = await client.post("/tasks", { data: body });
976
+ outputSingle(res.data, { format, idField: "id" });
977
+ });
978
+ tasks.command("update <task-id>").description("Update an existing task").option("--complete", "Mark task as completed").option("--incomplete", "Mark task as not completed").option("--deadline <ISO-date>", "Update deadline").option("--content <text>", "Update task content").action(async (taskId, _options, command) => {
979
+ const opts = command.optsWithGlobals();
980
+ const client = new AttioClient(opts.apiKey, opts.debug);
981
+ const format = detectFormat(opts);
982
+ const body = {};
983
+ if (opts.complete) body.is_completed = true;
984
+ if (opts.incomplete) body.is_completed = false;
985
+ if (opts.deadline) body.deadline_at = opts.deadline;
986
+ if (opts.content) body.content = opts.content;
987
+ const res = await client.patch(
988
+ `/tasks/${encodeURIComponent(taskId)}`,
989
+ { data: body }
990
+ );
991
+ outputSingle(res.data, { format, idField: "id" });
992
+ });
993
+ tasks.command("delete <task-id>").description("Delete a task").option("-y, --yes", "Skip confirmation prompt").action(async (taskId, _options, command) => {
994
+ const opts = command.optsWithGlobals();
995
+ if (!opts.yes) {
996
+ const ok = await confirm(`Delete task ${taskId}?`);
997
+ if (!ok) {
998
+ console.error("Aborted.");
999
+ return;
1000
+ }
1001
+ }
1002
+ const client = new AttioClient(opts.apiKey, opts.debug);
1003
+ await client.delete(`/tasks/${encodeURIComponent(taskId)}`);
1004
+ console.error("Deleted.");
1005
+ });
1006
+ }
1007
+ function truncate(str, max) {
1008
+ if (str.length <= max) return str;
1009
+ return str.slice(0, max - 3) + "...";
1010
+ }
1011
+
1012
+ // src/commands/notes.ts
1013
+ function register10(program2) {
1014
+ const notes = program2.command("notes").description("Manage notes");
1015
+ notes.command("list").description("List notes").option("--object <obj>", "Filter by parent object slug").option("--record <id>", "Filter by parent record ID").option("--limit <n>", "Maximum notes to return", "25").option("--offset <n>", "Number of notes to skip", "0").action(async (_options, command) => {
1016
+ const opts = command.optsWithGlobals();
1017
+ const client = new AttioClient(opts.apiKey, opts.debug);
1018
+ const format = detectFormat(opts);
1019
+ const params = new URLSearchParams();
1020
+ params.set("limit", String(opts.limit ?? 25));
1021
+ params.set("offset", String(opts.offset ?? 0));
1022
+ if (opts.object) params.set("parent_object", opts.object);
1023
+ if (opts.record) params.set("parent_record_id", opts.record);
1024
+ const res = await client.get(`/notes?${params.toString()}`);
1025
+ const notesList = res.data;
1026
+ const flat = notesList.map((n) => ({
1027
+ id: n.id?.note_id || "",
1028
+ title: n.title || "",
1029
+ parent_object: n.parent_object || "",
1030
+ parent_record_id: n.parent_record_id || "",
1031
+ created_at: n.created_at || ""
1032
+ }));
1033
+ outputList(flat, {
1034
+ format,
1035
+ columns: ["id", "title", "parent_object", "parent_record_id", "created_at"],
1036
+ idField: "id"
1037
+ });
1038
+ });
1039
+ notes.command("get <note-id>").description("Get a note by ID").action(async (noteId, _options, command) => {
1040
+ const opts = command.optsWithGlobals();
1041
+ const client = new AttioClient(opts.apiKey, opts.debug);
1042
+ const format = detectFormat(opts);
1043
+ const res = await client.get(`/notes/${encodeURIComponent(noteId)}`);
1044
+ outputSingle(res.data, { format, idField: "id" });
1045
+ });
1046
+ notes.command("create").description("Create a new note").requiredOption("--object <obj>", "Parent object slug (required)").requiredOption("--record <id>", "Parent record ID (required)").requiredOption("--title <text>", "Note title (required)").requiredOption("--content <text>", "Note content (required)").option("--format <fmt>", "Content format: plaintext or markdown", "plaintext").action(async (_options, command) => {
1047
+ const opts = command.optsWithGlobals();
1048
+ const client = new AttioClient(opts.apiKey, opts.debug);
1049
+ const format = detectFormat(opts);
1050
+ const body = {
1051
+ parent_object: opts.object,
1052
+ parent_record_id: opts.record,
1053
+ title: opts.title,
1054
+ format: opts.format ?? "plaintext",
1055
+ content: opts.content
1056
+ };
1057
+ const res = await client.post("/notes", { data: body });
1058
+ outputSingle(res.data, { format, idField: "id" });
1059
+ });
1060
+ notes.command("delete <note-id>").description("Delete a note").option("-y, --yes", "Skip confirmation prompt").action(async (noteId, _options, command) => {
1061
+ const opts = command.optsWithGlobals();
1062
+ if (!opts.yes) {
1063
+ const ok = await confirm(`Delete note ${noteId}?`);
1064
+ if (!ok) {
1065
+ console.error("Aborted.");
1066
+ return;
1067
+ }
1068
+ }
1069
+ const client = new AttioClient(opts.apiKey, opts.debug);
1070
+ await client.delete(`/notes/${encodeURIComponent(noteId)}`);
1071
+ console.error("Deleted.");
1072
+ });
1073
+ }
1074
+
1075
+ // src/commands/comments.ts
1076
+ function register11(program2) {
1077
+ const comments = program2.command("comments").description("Manage comments on records");
1078
+ comments.command("list").description("List comment threads on a record").requiredOption("--object <obj>", "Object slug (required)").requiredOption("--record <id>", "Record ID (required)").option("--limit <n>", "Maximum threads to return", "25").option("--offset <n>", "Number of threads to skip", "0").action(async (_options, command) => {
1079
+ const opts = command.optsWithGlobals();
1080
+ const client = new AttioClient(opts.apiKey, opts.debug);
1081
+ const format = detectFormat(opts);
1082
+ const params = new URLSearchParams();
1083
+ params.set("object", opts.object);
1084
+ params.set("record_id", opts.record);
1085
+ if (opts.limit) params.set("limit", String(opts.limit));
1086
+ if (opts.offset) params.set("offset", String(opts.offset));
1087
+ const res = await client.get(`/threads?${params.toString()}`);
1088
+ const threads = res.data;
1089
+ const flat = [];
1090
+ for (const thread of threads) {
1091
+ const threadId = thread.id?.thread_id || thread.thread_id || "";
1092
+ if (thread.comments && Array.isArray(thread.comments)) {
1093
+ for (const c of thread.comments) {
1094
+ flat.push({
1095
+ thread_id: threadId,
1096
+ author: c.author?.id || c.author?.name || "",
1097
+ content: truncate2(c.content_plaintext || c.content || "", 60),
1098
+ created_at: c.created_at || ""
1099
+ });
1100
+ }
1101
+ } else {
1102
+ flat.push({
1103
+ thread_id: threadId,
1104
+ author: thread.created_by_actor?.id || "",
1105
+ content: "",
1106
+ created_at: thread.created_at || ""
1107
+ });
1108
+ }
1109
+ }
1110
+ outputList(flat, {
1111
+ format,
1112
+ columns: ["thread_id", "author", "content", "created_at"],
1113
+ idField: "thread_id"
1114
+ });
1115
+ });
1116
+ comments.command("create").description("Create a comment on a record").requiredOption("--object <obj>", "Object slug (required)").requiredOption("--record <id>", "Record ID (required)").requiredOption("--content <text>", "Comment content (required)").option("--thread <thread-id>", "Thread ID to reply to (creates new thread if omitted)").action(async (_options, command) => {
1117
+ const opts = command.optsWithGlobals();
1118
+ const client = new AttioClient(opts.apiKey, opts.debug);
1119
+ const format = detectFormat(opts);
1120
+ const selfRes = await client.get("/self");
1121
+ const memberId = selfRes.authorized_by_workspace_member_id || "";
1122
+ const body = {
1123
+ format: "plaintext",
1124
+ content: opts.content,
1125
+ author: {
1126
+ type: "workspace-member",
1127
+ id: memberId
1128
+ }
1129
+ };
1130
+ if (opts.thread) {
1131
+ body.thread_id = opts.thread;
1132
+ } else {
1133
+ body.record = {
1134
+ object: opts.object,
1135
+ record_id: opts.record
1136
+ };
1137
+ }
1138
+ const res = await client.post("/comments", { data: body });
1139
+ outputSingle(res.data, { format, idField: "id" });
1140
+ });
1141
+ comments.command("delete <comment-id>").description("Delete a comment").option("-y, --yes", "Skip confirmation prompt").action(async (commentId, _options, command) => {
1142
+ const opts = command.optsWithGlobals();
1143
+ if (!opts.yes) {
1144
+ const ok = await confirm(`Delete comment ${commentId}?`);
1145
+ if (!ok) {
1146
+ console.error("Aborted.");
1147
+ return;
1148
+ }
1149
+ }
1150
+ const client = new AttioClient(opts.apiKey, opts.debug);
1151
+ await client.delete(`/comments/${encodeURIComponent(commentId)}`);
1152
+ console.error("Deleted.");
1153
+ });
1154
+ }
1155
+ function truncate2(str, max) {
1156
+ if (str.length <= max) return str;
1157
+ return str.slice(0, max - 3) + "...";
1158
+ }
1159
+
1160
+ // src/commands/members.ts
1161
+ function register12(program2) {
1162
+ const members = program2.command("members").description("Manage workspace members");
1163
+ members.command("list").description("List all workspace members").action(async function() {
1164
+ const opts = this.optsWithGlobals();
1165
+ const client = new AttioClient(opts.apiKey, opts.debug);
1166
+ const format = detectFormat(opts);
1167
+ const response = await client.get("/workspace_members");
1168
+ const rawItems = response.data;
1169
+ if (format === "json") {
1170
+ console.log(JSON.stringify(rawItems, null, 2));
1171
+ return;
1172
+ }
1173
+ if (format === "quiet") {
1174
+ for (const member of rawItems) {
1175
+ console.log(member.id.workspace_member_id);
1176
+ }
1177
+ return;
1178
+ }
1179
+ const flattened = rawItems.map((m) => ({
1180
+ id: m.id.workspace_member_id,
1181
+ first_name: m.first_name,
1182
+ last_name: m.last_name,
1183
+ email_address: m.email_address,
1184
+ access_level: m.access_level
1185
+ }));
1186
+ outputList(flattened, {
1187
+ format,
1188
+ columns: ["id", "first_name", "last_name", "email_address", "access_level"],
1189
+ idField: "id"
1190
+ });
1191
+ });
1192
+ }
1193
+
1194
+ // src/commands/config.ts
1195
+ import chalk4 from "chalk";
1196
+ function register13(program2) {
1197
+ const cmd = program2.command("config").description("Manage CLI configuration");
1198
+ cmd.command("set <key> <value>").description("Set a config value (e.g., attio config set api-key <key>)").action((key, value) => {
1199
+ if (key === "api-key") {
1200
+ setApiKey(value);
1201
+ console.error(chalk4.green(`API key saved to ${getConfigPath()}`));
1202
+ } else {
1203
+ console.error(chalk4.red(`Unknown config key: ${key}. Supported: api-key`));
1204
+ process.exit(1);
1205
+ }
1206
+ });
1207
+ cmd.command("get <key>").description("Get a config value (e.g., attio config get api-key)").action((key) => {
1208
+ if (key === "api-key") {
1209
+ const apiKey = resolveApiKey();
1210
+ if (apiKey) {
1211
+ const masked = "\u2022".repeat(Math.max(0, apiKey.length - 4)) + apiKey.slice(-4);
1212
+ console.log(masked);
1213
+ } else {
1214
+ console.error(chalk4.dim("No API key configured."));
1215
+ }
1216
+ } else {
1217
+ console.error(chalk4.red(`Unknown config key: ${key}. Supported: api-key`));
1218
+ process.exit(1);
1219
+ }
1220
+ });
1221
+ cmd.command("path").description("Print config file location").action(() => {
1222
+ console.log(getConfigPath());
1223
+ });
1224
+ cmd.command("claude-md").description("Print a CLAUDE.md snippet for AI agent discovery").action(() => {
1225
+ console.log(CLAUDE_MD_SNIPPET);
1226
+ });
1227
+ }
1228
+ var CLAUDE_MD_SNIPPET = `## Attio CLI (\`attio\`)
1229
+
1230
+ Use the \`attio\` CLI for all Attio CRM operations. Run \`attio <command> --help\` for full usage.
1231
+
1232
+ ### Common commands
1233
+
1234
+ \`\`\`
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
1241
+ \`\`\`
1242
+
1243
+ ### Output
1244
+
1245
+ Auto-detects TTY: table for terminal, JSON when piped. Force with \`--json\`, \`--csv\`, or \`--table\`. Use \`-q\` for IDs only.
1246
+
1247
+ ### Filters & sorting
1248
+
1249
+ \`--filter 'name~Acme'\` \`--filter 'revenue>=1000000'\` \`--sort name:asc\`
1250
+ Multiple \`--filter\` flags are ANDed together.`;
1251
+
1252
+ // src/commands/open.ts
1253
+ import { exec } from "child_process";
1254
+ import { platform } from "os";
1255
+ import chalk5 from "chalk";
1256
+ function register14(program2) {
1257
+ program2.command("open <object> [record-id]").description("Open an object or record in the Attio web app").action(async (object, recordId, options, command) => {
1258
+ const opts = command.optsWithGlobals();
1259
+ let url;
1260
+ if (recordId) {
1261
+ const client = new AttioClient(opts.apiKey, opts.debug);
1262
+ const res = await client.get(`/objects/${object}/records/${recordId}`);
1263
+ const record = res.data;
1264
+ url = record.web_url;
1265
+ if (!url) {
1266
+ console.error(chalk5.red("Record has no web_url."));
1267
+ process.exit(1);
1268
+ }
1269
+ } else {
1270
+ const client = new AttioClient(opts.apiKey, opts.debug);
1271
+ const self = await client.get("/self");
1272
+ const slug = self.workspace_slug || "";
1273
+ url = `https://app.attio.com/${slug}/${object}`;
1274
+ }
1275
+ const cmd = platform() === "darwin" ? "open" : platform() === "win32" ? "start" : "xdg-open";
1276
+ exec(`${cmd} "${url}"`, (err) => {
1277
+ if (err) {
1278
+ console.log(url);
1279
+ }
1280
+ });
1281
+ });
1282
+ }
1283
+
1284
+ // bin/attio.ts
1285
+ function handleError(err) {
1286
+ if (err instanceof AttioApiError) {
1287
+ console.error(err.display());
1288
+ process.exit(err.exitCode);
1289
+ }
1290
+ if (err instanceof AttioAuthError) {
1291
+ console.error(err.display());
1292
+ process.exit(err.exitCode);
1293
+ }
1294
+ if (err instanceof AttioRateLimitError) {
1295
+ console.error(err.display());
1296
+ process.exit(err.exitCode);
1297
+ }
1298
+ 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);
1302
+ }
1303
+ } else {
1304
+ console.error(chalk6.red("An unexpected error occurred"));
1305
+ }
1306
+ process.exit(1);
1307
+ }
1308
+ 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");
1309
+ register(program);
1310
+ register2(program);
1311
+ register3(program);
1312
+ register4(program);
1313
+ register5(program);
1314
+ register6(program);
1315
+ register7(program);
1316
+ register8(program);
1317
+ register9(program);
1318
+ register10(program);
1319
+ register11(program);
1320
+ register12(program);
1321
+ register13(program);
1322
+ register14(program);
1323
+ program.parseAsync(process.argv).catch(handleError);
1324
+ process.on("uncaughtException", handleError);
1325
+ process.on("unhandledRejection", (reason) => handleError(reason));