attio-cli 0.2.0 → 0.3.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 +48 -2
  2. package/dist/attio.js +825 -83
  3. package/package.json +3 -2
package/dist/attio.js CHANGED
@@ -3,6 +3,9 @@
3
3
  // bin/attio.ts
4
4
  import { program } from "commander";
5
5
  import chalk7 from "chalk";
6
+ import { readFileSync as readFileSync3 } from "fs";
7
+ import { dirname, join as join2 } from "path";
8
+ import { fileURLToPath } from "url";
6
9
 
7
10
  // src/errors.ts
8
11
  import chalk from "chalk";
@@ -57,7 +60,7 @@ var AttioRateLimitError = class extends Error {
57
60
  import chalk2 from "chalk";
58
61
 
59
62
  // src/config.ts
60
- import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
63
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync } from "fs";
61
64
  import { join } from "path";
62
65
  import { homedir } from "os";
63
66
  import * as dotenv from "dotenv";
@@ -73,8 +76,16 @@ function loadConfig() {
73
76
  }
74
77
  }
75
78
  function saveConfig(config2) {
76
- mkdirSync(CONFIG_DIR, { recursive: true });
77
- writeFileSync(CONFIG_FILE, JSON.stringify(config2, null, 2));
79
+ mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
80
+ try {
81
+ chmodSync(CONFIG_DIR, 448);
82
+ } catch {
83
+ }
84
+ writeFileSync(CONFIG_FILE, JSON.stringify(config2, null, 2), { mode: 384 });
85
+ try {
86
+ chmodSync(CONFIG_FILE, 384);
87
+ } catch {
88
+ }
78
89
  }
79
90
  function resolveApiKey(flagValue) {
80
91
  return flagValue || process.env.ATTIO_API_KEY || loadConfig().apiKey || "";
@@ -96,6 +107,20 @@ var BASE_URL = "https://api.attio.com/v2";
96
107
  var MAX_RETRIES = 3;
97
108
  var INITIAL_BACKOFF_MS = 1e3;
98
109
  var DEFAULT_TIMEOUT_MS = 3e4;
110
+ function getRetryDelayMs(response, attempt) {
111
+ const retryAfter = response.headers.get("retry-after");
112
+ if (retryAfter) {
113
+ const asSeconds = Number(retryAfter);
114
+ if (!Number.isNaN(asSeconds)) {
115
+ return Math.max(0, Math.ceil(asSeconds * 1e3));
116
+ }
117
+ const asDate = Date.parse(retryAfter);
118
+ if (!Number.isNaN(asDate)) {
119
+ return Math.max(0, asDate - Date.now());
120
+ }
121
+ }
122
+ return INITIAL_BACKOFF_MS * Math.pow(2, attempt);
123
+ }
99
124
  var AttioClient = class {
100
125
  apiKey;
101
126
  debug;
@@ -141,7 +166,10 @@ var AttioClient = class {
141
166
  }
142
167
  if (response.status === 429) {
143
168
  if (attempt < MAX_RETRIES) {
144
- const backoff = INITIAL_BACKOFF_MS * Math.pow(2, attempt);
169
+ const backoff = Math.max(100, getRetryDelayMs(response, attempt));
170
+ if (this.debug) {
171
+ console.error(chalk2.dim(` retrying in ${backoff}ms`));
172
+ }
145
173
  await new Promise((resolve) => setTimeout(resolve, backoff));
146
174
  continue;
147
175
  }
@@ -193,6 +221,43 @@ var AttioClient = class {
193
221
  import chalk3 from "chalk";
194
222
  import Table from "cli-table3";
195
223
  import { createInterface } from "readline";
224
+ var ID_KEY_PRIORITY = [
225
+ "record_id",
226
+ "entry_id",
227
+ "task_id",
228
+ "note_id",
229
+ "comment_id",
230
+ "thread_id",
231
+ "workspace_member_id",
232
+ "list_id",
233
+ "object_id",
234
+ "webhook_id",
235
+ "workspace_id"
236
+ ];
237
+ function toIdString(value) {
238
+ if (typeof value === "string" || typeof value === "number" || typeof value === "bigint") {
239
+ const id = String(value);
240
+ return id.length > 0 ? id : "";
241
+ }
242
+ return "";
243
+ }
244
+ function isRecord(value) {
245
+ return typeof value === "object" && value !== null && !Array.isArray(value);
246
+ }
247
+ function extractOutputId(raw) {
248
+ const direct = toIdString(raw);
249
+ if (direct) return direct;
250
+ if (!isRecord(raw)) return "";
251
+ for (const key of ID_KEY_PRIORITY) {
252
+ const id = toIdString(raw[key]);
253
+ if (id) return id;
254
+ }
255
+ for (const value of Object.values(raw)) {
256
+ const fallback = toIdString(value);
257
+ if (fallback) return fallback;
258
+ }
259
+ return "";
260
+ }
196
261
  function detectFormat(opts) {
197
262
  if (opts.quiet) return "quiet";
198
263
  if (opts.json) return "json";
@@ -205,8 +270,8 @@ function outputList(items, opts) {
205
270
  const idField = opts.idField || "id";
206
271
  if (format === "quiet") {
207
272
  for (const item of items) {
208
- const raw = typeof item === "string" ? item : item[idField] || "";
209
- const id = typeof raw === "object" && raw !== null ? Object.values(raw)[0] : raw;
273
+ const raw = typeof item === "string" ? item : item[idField] ?? "";
274
+ const id = extractOutputId(raw);
210
275
  if (id) console.log(id);
211
276
  }
212
277
  return;
@@ -245,8 +310,8 @@ function outputList(items, opts) {
245
310
  }
246
311
  function outputSingle(item, opts) {
247
312
  if (opts.format === "quiet") {
248
- const raw = item[opts.idField || "id"] || "";
249
- const id = typeof raw === "object" && raw !== null ? Object.values(raw)[0] : raw;
313
+ const raw = item[opts.idField || "id"] ?? "";
314
+ const id = extractOutputId(raw);
250
315
  console.log(id || "");
251
316
  return;
252
317
  }
@@ -443,6 +508,20 @@ function flattenRecord(record) {
443
508
  }
444
509
  return flat;
445
510
  }
511
+ function stripWrappingQuotes(raw) {
512
+ if (raw.startsWith('"') && raw.endsWith('"') || raw.startsWith("'") && raw.endsWith("'")) {
513
+ return raw.slice(1, -1);
514
+ }
515
+ return raw;
516
+ }
517
+ function parseScalar(raw) {
518
+ const normalized = stripWrappingQuotes(raw.trim());
519
+ if (normalized === "true") return true;
520
+ if (normalized === "false") return false;
521
+ if (normalized === "null") return null;
522
+ if (/^-?\d+(\.\d+)?$/.test(normalized)) return Number(normalized);
523
+ return normalized;
524
+ }
446
525
  function parseSets(sets) {
447
526
  const values = {};
448
527
  for (const set of sets) {
@@ -450,23 +529,31 @@ function parseSets(sets) {
450
529
  if (eqIdx === -1) throw new Error(`Invalid --set format: "${set}". Expected: key=value`);
451
530
  const key = set.slice(0, eqIdx).trim();
452
531
  const raw = set.slice(eqIdx + 1).trim();
453
- if (raw === "true") {
454
- values[key] = true;
455
- continue;
456
- }
457
- if (raw === "false") {
458
- values[key] = false;
459
- continue;
460
- }
461
- if (/^-?\d+(\.\d+)?$/.test(raw)) {
462
- values[key] = Number(raw);
463
- continue;
532
+ if (raw.startsWith("{") && raw.endsWith("}")) {
533
+ try {
534
+ values[key] = JSON.parse(raw);
535
+ continue;
536
+ } catch {
537
+ }
464
538
  }
465
539
  if (raw.startsWith("[") && raw.endsWith("]")) {
466
- values[key] = raw.slice(1, -1).split(",").map((s) => s.trim());
540
+ try {
541
+ const parsed = JSON.parse(raw);
542
+ if (Array.isArray(parsed)) {
543
+ values[key] = parsed;
544
+ continue;
545
+ }
546
+ } catch {
547
+ }
548
+ const inner = raw.slice(1, -1).trim();
549
+ if (inner === "") {
550
+ values[key] = [];
551
+ continue;
552
+ }
553
+ values[key] = inner.split(",").map((s) => parseScalar(s));
467
554
  continue;
468
555
  }
469
- values[key] = raw;
556
+ values[key] = parseScalar(raw);
470
557
  }
471
558
  return values;
472
559
  }
@@ -481,8 +568,8 @@ async function readStdin() {
481
568
  async function resolveValues(options) {
482
569
  if (options.values) {
483
570
  if (options.values.startsWith("@")) {
484
- const { readFileSync: readFileSync2 } = await import("fs");
485
- return JSON.parse(readFileSync2(options.values.slice(1), "utf-8"));
571
+ const { readFileSync: readFileSync4 } = await import("fs");
572
+ return JSON.parse(readFileSync4(options.values.slice(1), "utf-8"));
486
573
  }
487
574
  return JSON.parse(options.values);
488
575
  }
@@ -507,7 +594,7 @@ async function paginate(fetchPage, options) {
507
594
  return fetchPage(options.limit, options.offset);
508
595
  }
509
596
  const allResults = [];
510
- let offset = 0;
597
+ let offset = options.offset;
511
598
  const pageSize = 500;
512
599
  while (true) {
513
600
  const page = await fetchPage(pageSize, offset);
@@ -523,6 +610,29 @@ async function paginate(fetchPage, options) {
523
610
  }
524
611
 
525
612
  // src/commands/records.ts
613
+ function parseLimit(value, fallback) {
614
+ const parsed = Number(value);
615
+ if (!Number.isFinite(parsed) || parsed <= 0) return fallback;
616
+ return Math.floor(parsed);
617
+ }
618
+ function parseOffset(value) {
619
+ const parsed = Number(value);
620
+ if (!Number.isFinite(parsed) || parsed < 0) return 0;
621
+ return Math.floor(parsed);
622
+ }
623
+ function flattenEntry(entry) {
624
+ const flat = {
625
+ id: entry.id?.entry_id || "",
626
+ list_id: entry.id?.list_id || "",
627
+ parent_record_id: entry.parent_record_id || entry.record_id || "",
628
+ created_at: entry.created_at?.slice(0, 10) || ""
629
+ };
630
+ const values = entry.entry_values || entry.values || {};
631
+ for (const [key, attrValues] of Object.entries(values)) {
632
+ flat[key] = flattenValue(attrValues);
633
+ }
634
+ return flat;
635
+ }
526
636
  async function listRecords(object, cmdOpts) {
527
637
  const client = new AttioClient(cmdOpts.apiKey, cmdOpts.debug);
528
638
  const format = detectFormat(cmdOpts);
@@ -537,8 +647,8 @@ async function listRecords(object, cmdOpts) {
537
647
  if (cmdOpts.sort && cmdOpts.sort.length > 0) {
538
648
  sorts = cmdOpts.sort.map(parseSort);
539
649
  }
540
- const limit = Number(cmdOpts.limit) || 25;
541
- const offset = Number(cmdOpts.offset) || 0;
650
+ const limit = parseLimit(cmdOpts.limit, 25);
651
+ const offset = parseOffset(cmdOpts.offset);
542
652
  const all = cmdOpts.all ?? false;
543
653
  const fetchPage = async (pageLimit, pageOffset) => {
544
654
  const body = {};
@@ -554,8 +664,8 @@ async function listRecords(object, cmdOpts) {
554
664
  };
555
665
  const records = await paginate(fetchPage, { limit, offset, all });
556
666
  if (format === "quiet") {
557
- for (const r of records) {
558
- console.log(r.id?.record_id ?? "");
667
+ for (const record of records) {
668
+ console.log(record.id?.record_id ?? "");
559
669
  }
560
670
  return;
561
671
  }
@@ -636,12 +746,12 @@ async function deleteRecord(object, recordId, cmdOpts) {
636
746
  );
637
747
  console.error("Deleted.");
638
748
  }
639
- async function upsertRecord(object, cmdOpts) {
749
+ async function assertRecord(object, cmdOpts) {
640
750
  const client = new AttioClient(cmdOpts.apiKey, cmdOpts.debug);
641
751
  const format = detectFormat(cmdOpts);
642
752
  const matchAttr = cmdOpts.match;
643
753
  if (!matchAttr) {
644
- throw new Error("--match <attribute-slug> is required for upsert");
754
+ throw new Error("--match <attribute-slug> is required for assert");
645
755
  }
646
756
  const values = requireValues(await resolveValues(cmdOpts));
647
757
  const res = await client.put(
@@ -659,23 +769,32 @@ async function upsertRecord(object, cmdOpts) {
659
769
  }
660
770
  outputSingle(flattenRecord(record), { format });
661
771
  }
662
- async function searchRecords(object, query, cmdOpts) {
772
+ async function searchRecords(query, cmdOpts, objectScope) {
663
773
  const client = new AttioClient(cmdOpts.apiKey, cmdOpts.debug);
664
774
  const format = detectFormat(cmdOpts);
665
- const limit = Number(cmdOpts.limit) || 25;
775
+ const limit = parseLimit(cmdOpts.limit, 25);
776
+ const optionObjects = cmdOpts.object ?? [];
777
+ let objects = objectScope && objectScope.length > 0 ? objectScope : optionObjects;
778
+ if (objects.length === 0) {
779
+ const objectsRes = await client.get("/objects");
780
+ objects = objectsRes.data.map((obj) => obj.api_slug).filter((slug) => typeof slug === "string" && slug.length > 0);
781
+ }
782
+ if (objects.length === 0) {
783
+ throw new Error("No objects available for search.");
784
+ }
666
785
  const res = await client.post(
667
786
  "/objects/records/search",
668
787
  {
669
788
  query,
670
- objects: [object],
789
+ objects,
671
790
  request_as: { type: "workspace" },
672
791
  limit
673
792
  }
674
793
  );
675
794
  const records = res.data;
676
795
  if (format === "quiet") {
677
- for (const r of records) {
678
- console.log(r.id?.record_id ?? "");
796
+ for (const record of records) {
797
+ console.log(record.id?.record_id ?? "");
679
798
  }
680
799
  return;
681
800
  }
@@ -686,6 +805,87 @@ async function searchRecords(object, query, cmdOpts) {
686
805
  const flat = records.map(flattenRecord);
687
806
  outputList(flat, { format });
688
807
  }
808
+ async function listRecordValues(object, recordId, cmdOpts) {
809
+ const client = new AttioClient(cmdOpts.apiKey, cmdOpts.debug);
810
+ const format = detectFormat(cmdOpts);
811
+ const requestedAttributes = cmdOpts.attribute ? [cmdOpts.attribute] : cmdOpts.attributes ?? [];
812
+ let attributes = requestedAttributes;
813
+ if (attributes.length === 0) {
814
+ const attrRes = await client.get(
815
+ `/objects/${encodeURIComponent(object)}/attributes`
816
+ );
817
+ attributes = attrRes.data.map((attr) => attr.api_slug).filter((slug) => typeof slug === "string" && slug.length > 0);
818
+ }
819
+ const showHistoric = cmdOpts.historic !== false;
820
+ const limit = parseLimit(cmdOpts.limit, 25);
821
+ const offset = parseOffset(cmdOpts.offset);
822
+ const all = cmdOpts.all ?? false;
823
+ const allValues = [];
824
+ for (const attribute of attributes) {
825
+ const fetchPage = async (pageLimit, pageOffset) => {
826
+ const params = new URLSearchParams();
827
+ params.set("show_historic", showHistoric ? "true" : "false");
828
+ params.set("limit", String(pageLimit));
829
+ params.set("offset", String(pageOffset));
830
+ const res = await client.get(
831
+ `/objects/${encodeURIComponent(object)}/records/${encodeURIComponent(recordId)}/attributes/${encodeURIComponent(attribute)}/values?${params.toString()}`
832
+ );
833
+ return res.data;
834
+ };
835
+ const attributeValues = await paginate(fetchPage, { limit, offset, all });
836
+ for (const value of attributeValues) {
837
+ allValues.push({ attribute, ...value });
838
+ }
839
+ }
840
+ if (format === "json") {
841
+ outputList(allValues, { format });
842
+ return;
843
+ }
844
+ const flat = allValues.map((value) => ({
845
+ attribute: value.attribute,
846
+ value: flattenValue([value]),
847
+ active_from: value.active_from || "",
848
+ active_until: value.active_until || "",
849
+ created_by_type: value.created_by_actor?.type || "",
850
+ created_by_id: value.created_by_actor?.id || ""
851
+ }));
852
+ outputList(flat, {
853
+ format,
854
+ columns: ["attribute", "value", "active_from", "active_until", "created_by_type", "created_by_id"],
855
+ idField: "attribute"
856
+ });
857
+ }
858
+ async function listRecordEntries(object, recordId, cmdOpts) {
859
+ const client = new AttioClient(cmdOpts.apiKey, cmdOpts.debug);
860
+ const format = detectFormat(cmdOpts);
861
+ const limit = parseLimit(cmdOpts.limit, 25);
862
+ const offset = parseOffset(cmdOpts.offset);
863
+ const all = cmdOpts.all ?? false;
864
+ const entries = await paginate(
865
+ async (pageLimit, pageOffset) => {
866
+ const params = new URLSearchParams();
867
+ params.set("limit", String(pageLimit));
868
+ params.set("offset", String(pageOffset));
869
+ const res = await client.get(
870
+ `/objects/${encodeURIComponent(object)}/records/${encodeURIComponent(recordId)}/entries?${params.toString()}`
871
+ );
872
+ return res.data;
873
+ },
874
+ { limit, offset, all }
875
+ );
876
+ if (format === "quiet") {
877
+ for (const entry of entries) {
878
+ console.log(entry.id?.entry_id ?? "");
879
+ }
880
+ return;
881
+ }
882
+ if (format === "json") {
883
+ outputList(entries, { format, idField: "id" });
884
+ return;
885
+ }
886
+ const flat = entries.map(flattenEntry);
887
+ outputList(flat, { format, idField: "id" });
888
+ }
689
889
  function register4(program2) {
690
890
  const records = program2.command("records").description("Manage records in any Attio object");
691
891
  records.command("list").description("List or query records for an object").argument("<object>", "Object slug or ID (e.g. companies, people)").option(
@@ -699,12 +899,10 @@ function register4(program2) {
699
899
  (val, prev) => [...prev, val],
700
900
  []
701
901
  ).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) => {
702
- const opts = cmd.optsWithGlobals();
703
- await listRecords(object, opts);
902
+ await listRecords(object, cmd.optsWithGlobals());
704
903
  });
705
904
  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) => {
706
- const opts = cmd.optsWithGlobals();
707
- await getRecord(object, recordId, opts);
905
+ await getRecord(object, recordId, cmd.optsWithGlobals());
708
906
  });
709
907
  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(
710
908
  "--set <key=value>",
@@ -712,8 +910,7 @@ function register4(program2) {
712
910
  (val, prev) => [...prev, val],
713
911
  []
714
912
  ).action(async (object, _opts, cmd) => {
715
- const opts = cmd.optsWithGlobals();
716
- await createRecord(object, opts);
913
+ await createRecord(object, cmd.optsWithGlobals());
717
914
  });
718
915
  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(
719
916
  "--set <key=value>",
@@ -721,76 +918,175 @@ function register4(program2) {
721
918
  (val, prev) => [...prev, val],
722
919
  []
723
920
  ).action(async (object, recordId, _opts, cmd) => {
724
- const opts = cmd.optsWithGlobals();
725
- await updateRecord(object, recordId, opts);
921
+ await updateRecord(object, recordId, cmd.optsWithGlobals());
726
922
  });
727
923
  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) => {
728
- const opts = cmd.optsWithGlobals();
729
- await deleteRecord(object, recordId, opts);
924
+ await deleteRecord(object, recordId, cmd.optsWithGlobals());
730
925
  });
731
- 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(
926
+ records.command("assert").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(
732
927
  "--set <key=value>",
733
928
  "Set an attribute value (repeatable)",
734
929
  (val, prev) => [...prev, val],
735
930
  []
736
931
  ).action(async (object, _opts, cmd) => {
737
- const opts = cmd.optsWithGlobals();
738
- await upsertRecord(object, opts);
932
+ await assertRecord(object, cmd.optsWithGlobals());
933
+ });
934
+ records.command("upsert").description("Alias for records assert").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(
935
+ "--set <key=value>",
936
+ "Set an attribute value (repeatable)",
937
+ (val, prev) => [...prev, val],
938
+ []
939
+ ).action(async (object, _opts, cmd) => {
940
+ await assertRecord(object, cmd.optsWithGlobals());
941
+ });
942
+ records.command("search").description("Search records across one or more objects").argument("<query>", "Search query string").option(
943
+ "--object <object>",
944
+ "Object slug or ID to scope search (repeatable). Defaults to all objects",
945
+ (val, prev) => [...prev, val],
946
+ []
947
+ ).option("--limit <n>", "Maximum results to return", "25").action(async (query, _opts, cmd) => {
948
+ await searchRecords(query, cmd.optsWithGlobals());
949
+ });
950
+ records.command("values").description("List current and historic attribute values for a record").argument("<object>", "Object slug or ID").argument("<record-id>", "Record ID").option("--attribute <attribute>", "Only include a single attribute slug").option("--limit <n>", "Max values per page", "25").option("--offset <n>", "Starting offset", "0").option("--all", "Fetch all pages").option("--no-historic", "Exclude historic values and show active values only").action(async (object, recordId, _opts, cmd) => {
951
+ await listRecordValues(object, recordId, cmd.optsWithGlobals());
739
952
  });
740
- 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) => {
741
- const opts = cmd.optsWithGlobals();
742
- await searchRecords(object, query, opts);
953
+ records.command("entries").description("List list entries where this record is the parent").argument("<object>", "Object slug or ID").argument("<record-id>", "Record ID").option("--limit <n>", "Max entries per page", "25").option("--offset <n>", "Starting offset", "0").option("--all", "Fetch all pages").action(async (object, recordId, _opts, cmd) => {
954
+ await listRecordEntries(object, recordId, cmd.optsWithGlobals());
743
955
  });
744
956
  }
745
957
 
746
958
  // src/commands/people.ts
747
959
  function register5(program2) {
748
960
  const cmd = program2.command("people").description("Manage people records (shortcut for: records <cmd> people)");
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) => {
961
+ 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) => {
750
962
  await listRecords("people", command.optsWithGlobals());
751
963
  });
752
- cmd.command("get <record-id>").description("Get a person by record ID").action(async (recordId, options, command) => {
964
+ cmd.command("get <record-id>").description("Get a person by record ID").action(async (recordId, _options, command) => {
753
965
  await getRecord("people", recordId, command.optsWithGlobals());
754
966
  });
755
- 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) => {
967
+ 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) => {
756
968
  await createRecord("people", command.optsWithGlobals());
757
969
  });
758
- 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) => {
970
+ 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) => {
759
971
  await updateRecord("people", recordId, command.optsWithGlobals());
760
972
  });
761
- cmd.command("delete <record-id>").description("Delete a person").option("-y, --yes", "Skip confirmation").action(async (recordId, options, command) => {
973
+ cmd.command("delete <record-id>").description("Delete a person").option("-y, --yes", "Skip confirmation").action(async (recordId, _options, command) => {
762
974
  await deleteRecord("people", recordId, command.optsWithGlobals());
763
975
  });
764
- cmd.command("search <query>").description("Search people by name or email").option("--limit <n>", "Maximum results", "25").action(async (query, options, command) => {
765
- await searchRecords("people", query, command.optsWithGlobals());
976
+ cmd.command("assert").description("Create or update a person by matching attribute").requiredOption("--match <attribute-slug>", "Attribute slug to match on (required)").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) => {
977
+ await assertRecord("people", command.optsWithGlobals());
978
+ });
979
+ cmd.command("search <query>").description("Search people by name or email").option("--limit <n>", "Maximum results", "25").action(async (query, _options, command) => {
980
+ await searchRecords(query, command.optsWithGlobals(), ["people"]);
766
981
  });
767
982
  }
768
983
 
769
984
  // src/commands/companies.ts
770
985
  function register6(program2) {
771
986
  const cmd = program2.command("companies").description("Manage company records (shortcut for: records <cmd> companies)");
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) => {
987
+ 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) => {
773
988
  await listRecords("companies", command.optsWithGlobals());
774
989
  });
775
- cmd.command("get <record-id>").description("Get a company by record ID").action(async (recordId, options, command) => {
990
+ cmd.command("get <record-id>").description("Get a company by record ID").action(async (recordId, _options, command) => {
776
991
  await getRecord("companies", recordId, command.optsWithGlobals());
777
992
  });
778
- 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) => {
993
+ 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) => {
779
994
  await createRecord("companies", command.optsWithGlobals());
780
995
  });
781
- 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) => {
996
+ 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) => {
782
997
  await updateRecord("companies", recordId, command.optsWithGlobals());
783
998
  });
784
- cmd.command("delete <record-id>").description("Delete a company").option("-y, --yes", "Skip confirmation").action(async (recordId, options, command) => {
999
+ cmd.command("delete <record-id>").description("Delete a company").option("-y, --yes", "Skip confirmation").action(async (recordId, _options, command) => {
785
1000
  await deleteRecord("companies", recordId, command.optsWithGlobals());
786
1001
  });
787
- cmd.command("search <query>").description("Search companies by name or domain").option("--limit <n>", "Maximum results", "25").action(async (query, options, command) => {
788
- await searchRecords("companies", query, command.optsWithGlobals());
1002
+ cmd.command("assert").description("Create or update a company by matching attribute").requiredOption("--match <attribute-slug>", "Attribute slug to match on (required)").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) => {
1003
+ await assertRecord("companies", command.optsWithGlobals());
1004
+ });
1005
+ cmd.command("search <query>").description("Search companies by name or domain").option("--limit <n>", "Maximum results", "25").action(async (query, _options, command) => {
1006
+ await searchRecords(query, command.optsWithGlobals(), ["companies"]);
789
1007
  });
790
1008
  }
791
1009
 
792
- // src/commands/lists.ts
1010
+ // src/commands/deals.ts
793
1011
  function register7(program2) {
1012
+ const cmd = program2.command("deals").description("Manage deal records (shortcut for: records <cmd> deals)");
1013
+ cmd.command("list").description("List deals").option("--filter <expr>", "Filter: = != ~ !~ ^ > >= < <= ? (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) => {
1014
+ await listRecords("deals", command.optsWithGlobals());
1015
+ });
1016
+ cmd.command("get <record-id>").description("Get a deal by record ID").action(async (recordId, _options, command) => {
1017
+ await getRecord("deals", recordId, command.optsWithGlobals());
1018
+ });
1019
+ cmd.command("create").description("Create a deal").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) => {
1020
+ await createRecord("deals", command.optsWithGlobals());
1021
+ });
1022
+ cmd.command("update <record-id>").description("Update a deal").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) => {
1023
+ await updateRecord("deals", recordId, command.optsWithGlobals());
1024
+ });
1025
+ cmd.command("delete <record-id>").description("Delete a deal").option("-y, --yes", "Skip confirmation").action(async (recordId, _options, command) => {
1026
+ await deleteRecord("deals", recordId, command.optsWithGlobals());
1027
+ });
1028
+ cmd.command("assert").description("Create or update a deal by matching attribute").requiredOption("--match <attribute-slug>", "Attribute slug to match on (required)").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) => {
1029
+ await assertRecord("deals", command.optsWithGlobals());
1030
+ });
1031
+ cmd.command("search <query>").description("Search deals").option("--limit <n>", "Maximum results", "25").action(async (query, _options, command) => {
1032
+ await searchRecords(query, command.optsWithGlobals(), ["deals"]);
1033
+ });
1034
+ }
1035
+
1036
+ // src/commands/users.ts
1037
+ function register8(program2) {
1038
+ const cmd = program2.command("users").description("Manage user records (shortcut for: records <cmd> users)");
1039
+ cmd.command("list").description("List users").option("--filter <expr>", "Filter: = != ~ !~ ^ > >= < <= ? (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) => {
1040
+ await listRecords("users", command.optsWithGlobals());
1041
+ });
1042
+ cmd.command("get <record-id>").description("Get a user by record ID").action(async (recordId, _options, command) => {
1043
+ await getRecord("users", recordId, command.optsWithGlobals());
1044
+ });
1045
+ cmd.command("create").description("Create a user").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) => {
1046
+ await createRecord("users", command.optsWithGlobals());
1047
+ });
1048
+ cmd.command("update <record-id>").description("Update a user").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) => {
1049
+ await updateRecord("users", recordId, command.optsWithGlobals());
1050
+ });
1051
+ cmd.command("delete <record-id>").description("Delete a user").option("-y, --yes", "Skip confirmation").action(async (recordId, _options, command) => {
1052
+ await deleteRecord("users", recordId, command.optsWithGlobals());
1053
+ });
1054
+ cmd.command("assert").description("Create or update a user by matching attribute").requiredOption("--match <attribute-slug>", "Attribute slug to match on (required)").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) => {
1055
+ await assertRecord("users", command.optsWithGlobals());
1056
+ });
1057
+ cmd.command("search <query>").description("Search users").option("--limit <n>", "Maximum results", "25").action(async (query, _options, command) => {
1058
+ await searchRecords(query, command.optsWithGlobals(), ["users"]);
1059
+ });
1060
+ }
1061
+
1062
+ // src/commands/workspaces.ts
1063
+ function register9(program2) {
1064
+ const cmd = program2.command("workspaces").description("Manage workspace records (shortcut for: records <cmd> workspaces)");
1065
+ cmd.command("list").description("List workspaces").option("--filter <expr>", "Filter: = != ~ !~ ^ > >= < <= ? (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) => {
1066
+ await listRecords("workspaces", command.optsWithGlobals());
1067
+ });
1068
+ cmd.command("get <record-id>").description("Get a workspace by record ID").action(async (recordId, _options, command) => {
1069
+ await getRecord("workspaces", recordId, command.optsWithGlobals());
1070
+ });
1071
+ cmd.command("create").description("Create a workspace").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) => {
1072
+ await createRecord("workspaces", command.optsWithGlobals());
1073
+ });
1074
+ cmd.command("update <record-id>").description("Update a workspace").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) => {
1075
+ await updateRecord("workspaces", recordId, command.optsWithGlobals());
1076
+ });
1077
+ cmd.command("delete <record-id>").description("Delete a workspace").option("-y, --yes", "Skip confirmation").action(async (recordId, _options, command) => {
1078
+ await deleteRecord("workspaces", recordId, command.optsWithGlobals());
1079
+ });
1080
+ cmd.command("assert").description("Create or update a workspace by matching attribute").requiredOption("--match <attribute-slug>", "Attribute slug to match on (required)").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) => {
1081
+ await assertRecord("workspaces", command.optsWithGlobals());
1082
+ });
1083
+ cmd.command("search <query>").description("Search workspaces").option("--limit <n>", "Maximum results", "25").action(async (query, _options, command) => {
1084
+ await searchRecords(query, command.optsWithGlobals(), ["workspaces"]);
1085
+ });
1086
+ }
1087
+
1088
+ // src/commands/lists.ts
1089
+ function register10(program2) {
794
1090
  const cmd = program2.command("lists").description("Manage lists");
795
1091
  cmd.command("list").description("List all lists").action(async (_options, command) => {
796
1092
  const opts = command.optsWithGlobals();
@@ -844,7 +1140,7 @@ function register7(program2) {
844
1140
  }
845
1141
 
846
1142
  // src/commands/entries.ts
847
- function flattenEntry(entry) {
1143
+ function flattenEntry2(entry) {
848
1144
  const flat = {
849
1145
  id: entry.id?.entry_id || "",
850
1146
  record_id: entry.record_id || entry.parent_record_id || "",
@@ -855,7 +1151,7 @@ function flattenEntry(entry) {
855
1151
  }
856
1152
  return flat;
857
1153
  }
858
- function register8(program2) {
1154
+ function register11(program2) {
859
1155
  const cmd = program2.command("entries").description("Manage list entries");
860
1156
  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) => {
861
1157
  const opts = command.optsWithGlobals();
@@ -885,7 +1181,7 @@ function register8(program2) {
885
1181
  outputList(entries, { format, idField: "id" });
886
1182
  return;
887
1183
  }
888
- const flat = entries.map(flattenEntry);
1184
+ const flat = entries.map(flattenEntry2);
889
1185
  outputList(flat, { format, idField: "id" });
890
1186
  });
891
1187
  cmd.command("get <list> <entry-id>").description("Get an entry by ID").action(async (list, entryId, _options, command) => {
@@ -898,7 +1194,7 @@ function register8(program2) {
898
1194
  outputSingle(entry, { format, idField: "id" });
899
1195
  return;
900
1196
  }
901
- const flat = flattenEntry(entry);
1197
+ const flat = flattenEntry2(entry);
902
1198
  outputSingle(flat, { format, idField: "id" });
903
1199
  });
904
1200
  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) => {
@@ -919,7 +1215,28 @@ function register8(program2) {
919
1215
  outputSingle(entry, { format, idField: "id" });
920
1216
  return;
921
1217
  }
922
- const flat = flattenEntry(entry);
1218
+ const flat = flattenEntry2(entry);
1219
+ outputSingle(flat, { format, idField: "id" });
1220
+ });
1221
+ cmd.command("assert <list>").description("Create or update a list entry by parent record").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) => {
1222
+ const opts = command.optsWithGlobals();
1223
+ const client = new AttioClient(opts.apiKey, opts.debug);
1224
+ const format = detectFormat(opts);
1225
+ const resolvedValues = requireValues(await resolveValues({ values: opts.values, set: opts.set }));
1226
+ const body = {
1227
+ data: {
1228
+ parent_record_id: opts.record,
1229
+ parent_object: opts.object,
1230
+ entry_values: resolvedValues
1231
+ }
1232
+ };
1233
+ const res = await client.put(`/lists/${list}/entries`, body);
1234
+ const entry = res.data;
1235
+ if (format === "json") {
1236
+ outputSingle(entry, { format, idField: "id" });
1237
+ return;
1238
+ }
1239
+ const flat = flattenEntry2(entry);
923
1240
  outputSingle(flat, { format, idField: "id" });
924
1241
  });
925
1242
  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) => {
@@ -938,7 +1255,7 @@ function register8(program2) {
938
1255
  outputSingle(entry, { format, idField: "id" });
939
1256
  return;
940
1257
  }
941
- const flat = flattenEntry(entry);
1258
+ const flat = flattenEntry2(entry);
942
1259
  outputSingle(flat, { format, idField: "id" });
943
1260
  });
944
1261
  cmd.command("delete <list> <entry-id>").description("Delete an entry").option("-y, --yes", "Skip confirmation").action(async (list, entryId, _options, command) => {
@@ -957,7 +1274,7 @@ function register8(program2) {
957
1274
  }
958
1275
 
959
1276
  // src/commands/tasks.ts
960
- function register9(program2) {
1277
+ function register12(program2) {
961
1278
  const tasks = program2.command("tasks").description("Manage tasks");
962
1279
  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) => {
963
1280
  const opts = command.optsWithGlobals();
@@ -1068,7 +1385,7 @@ function truncate(str, max) {
1068
1385
  }
1069
1386
 
1070
1387
  // src/commands/notes.ts
1071
- function register10(program2) {
1388
+ function register13(program2) {
1072
1389
  const notes = program2.command("notes").description("Manage notes");
1073
1390
  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) => {
1074
1391
  const opts = command.optsWithGlobals();
@@ -1141,7 +1458,7 @@ function register10(program2) {
1141
1458
  }
1142
1459
 
1143
1460
  // src/commands/comments.ts
1144
- function register11(program2) {
1461
+ function register14(program2) {
1145
1462
  const comments = program2.command("comments").description("Manage comments on records");
1146
1463
  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) => {
1147
1464
  const opts = command.optsWithGlobals();
@@ -1235,8 +1552,413 @@ function truncate2(str, max) {
1235
1552
  return str.slice(0, max - 3) + "...";
1236
1553
  }
1237
1554
 
1555
+ // src/commands/threads.ts
1556
+ function flattenThread(thread) {
1557
+ return {
1558
+ id: thread.id?.thread_id || thread.thread_id || "",
1559
+ object: thread.record?.object || "",
1560
+ record_id: thread.record?.record_id || "",
1561
+ list: thread.entry?.list || "",
1562
+ entry_id: thread.entry?.entry_id || "",
1563
+ comments: Array.isArray(thread.comments) ? thread.comments.length : 0,
1564
+ created_at: thread.created_at || ""
1565
+ };
1566
+ }
1567
+ function register15(program2) {
1568
+ const cmd = program2.command("threads").description("Manage comment threads");
1569
+ cmd.command("list").description("List comment threads").option("--object <object>", "Filter by object slug/ID (requires --record)").option("--record <record-id>", "Filter by parent record ID (requires --object)").option("--list <list>", "Filter by list slug/ID (requires --entry)").option("--entry <entry-id>", "Filter by entry ID (requires --list)").option("--limit <n>", "Maximum threads to return", "25").option("--offset <n>", "Number of threads to skip", "0").action(async (_options, command) => {
1570
+ const opts = command.optsWithGlobals();
1571
+ const client = new AttioClient(opts.apiKey, opts.debug);
1572
+ const format = detectFormat(opts);
1573
+ if (opts.object && !opts.record || !opts.object && opts.record) {
1574
+ throw new Error("--object and --record must be provided together.");
1575
+ }
1576
+ if (opts.list && !opts.entry || !opts.list && opts.entry) {
1577
+ throw new Error("--list and --entry must be provided together.");
1578
+ }
1579
+ const params = new URLSearchParams();
1580
+ params.set("limit", String(Number(opts.limit) || 25));
1581
+ params.set("offset", String(Number(opts.offset) || 0));
1582
+ if (opts.object) params.set("object", opts.object);
1583
+ if (opts.record) params.set("record_id", opts.record);
1584
+ if (opts.list) params.set("list", opts.list);
1585
+ if (opts.entry) params.set("entry_id", opts.entry);
1586
+ const res = await client.get(`/threads?${params.toString()}`);
1587
+ const threads = res.data;
1588
+ if (format === "quiet") {
1589
+ for (const thread of threads) {
1590
+ console.log(thread.id?.thread_id || thread.thread_id || "");
1591
+ }
1592
+ return;
1593
+ }
1594
+ if (format === "json") {
1595
+ outputList(threads, { format, idField: "id" });
1596
+ return;
1597
+ }
1598
+ outputList(threads.map(flattenThread), {
1599
+ format,
1600
+ columns: ["id", "object", "record_id", "list", "entry_id", "comments", "created_at"],
1601
+ idField: "id"
1602
+ });
1603
+ });
1604
+ cmd.command("get <id>").description("Get a thread by ID").action(async (id, _options, command) => {
1605
+ const opts = command.optsWithGlobals();
1606
+ const client = new AttioClient(opts.apiKey, opts.debug);
1607
+ const format = detectFormat(opts);
1608
+ const res = await client.get(`/threads/${encodeURIComponent(id)}`);
1609
+ const thread = res.data;
1610
+ if (format === "json") {
1611
+ outputSingle(thread, { format, idField: "id" });
1612
+ return;
1613
+ }
1614
+ outputSingle(flattenThread(thread), { format, idField: "id" });
1615
+ });
1616
+ }
1617
+
1618
+ // src/commands/meetings.ts
1619
+ function flattenMeeting(meeting) {
1620
+ return {
1621
+ id: meeting.id?.meeting_id || "",
1622
+ title: meeting.title || "",
1623
+ start: meeting.start || "",
1624
+ end: meeting.end || "",
1625
+ is_all_day: meeting.is_all_day ?? false,
1626
+ participants: Array.isArray(meeting.participants) ? meeting.participants.length : 0
1627
+ };
1628
+ }
1629
+ async function listMeetings(opts) {
1630
+ const client = new AttioClient(opts.apiKey, opts.debug);
1631
+ const limit = Number(opts.limit) || 50;
1632
+ const all = !!opts.all;
1633
+ const baseParams = new URLSearchParams();
1634
+ baseParams.set("limit", String(limit));
1635
+ if (opts.linkedObject) baseParams.set("linked_object", opts.linkedObject);
1636
+ if (opts.linkedRecordId) baseParams.set("linked_record_id", opts.linkedRecordId);
1637
+ if (opts.participants) baseParams.set("participants", opts.participants);
1638
+ if (opts.sort) baseParams.set("sort", opts.sort);
1639
+ if (opts.endsFrom) baseParams.set("ends_from", opts.endsFrom);
1640
+ if (opts.startsBefore) baseParams.set("starts_before", opts.startsBefore);
1641
+ if (opts.timezone) baseParams.set("timezone", opts.timezone);
1642
+ const allMeetings = [];
1643
+ let cursor = opts.cursor;
1644
+ while (true) {
1645
+ const params = new URLSearchParams(baseParams);
1646
+ if (cursor) params.set("cursor", cursor);
1647
+ const res = await client.get(
1648
+ `/meetings?${params.toString()}`
1649
+ );
1650
+ allMeetings.push(...res.data);
1651
+ if (!all) break;
1652
+ cursor = res.pagination?.next_cursor ?? void 0;
1653
+ if (!cursor) break;
1654
+ }
1655
+ return allMeetings;
1656
+ }
1657
+ function register16(program2) {
1658
+ const cmd = program2.command("meetings").description("Manage meetings (Beta API)");
1659
+ cmd.command("list").description("List meetings (Beta API)").option("--limit <n>", "Maximum meetings per page", "50").option("--cursor <cursor>", "Pagination cursor").option("--all", "Auto-paginate through all pages").option("--linked-object <object>", "Filter by linked object slug or ID").option("--linked-record-id <id>", "Filter by linked record ID").option("--participants <emails>", "Comma-separated participant email addresses").option("--sort <order>", "Sort order (e.g. start_asc, start_desc)").option("--ends-from <iso-timestamp>", "Only include meetings ending after this time").option("--starts-before <iso-timestamp>", "Only include meetings starting before this time").option("--timezone <tz>", "Timezone used with date filters (defaults to UTC)").action(async (_options, command) => {
1660
+ const opts = command.optsWithGlobals();
1661
+ const format = detectFormat(opts);
1662
+ if (opts.linkedRecordId && !opts.linkedObject) {
1663
+ throw new Error("--linked-record-id requires --linked-object.");
1664
+ }
1665
+ const meetings = await listMeetings(opts);
1666
+ if (format === "quiet") {
1667
+ for (const meeting of meetings) {
1668
+ console.log(meeting.id?.meeting_id || "");
1669
+ }
1670
+ return;
1671
+ }
1672
+ if (format === "json") {
1673
+ outputList(meetings, { format, idField: "id" });
1674
+ return;
1675
+ }
1676
+ outputList(meetings.map(flattenMeeting), {
1677
+ format,
1678
+ columns: ["id", "title", "start", "end", "is_all_day", "participants"],
1679
+ idField: "id"
1680
+ });
1681
+ });
1682
+ cmd.command("get <id>").description("Get a meeting by ID (Beta API)").action(async (id, _options, command) => {
1683
+ const opts = command.optsWithGlobals();
1684
+ const client = new AttioClient(opts.apiKey, opts.debug);
1685
+ const format = detectFormat(opts);
1686
+ const res = await client.get(`/meetings/${encodeURIComponent(id)}`);
1687
+ const meeting = res.data;
1688
+ if (format === "json") {
1689
+ outputSingle(meeting, { format, idField: "id" });
1690
+ return;
1691
+ }
1692
+ outputSingle(flattenMeeting(meeting), { format, idField: "id" });
1693
+ });
1694
+ }
1695
+
1696
+ // src/commands/recordings.ts
1697
+ function flattenRecording(recording) {
1698
+ return {
1699
+ id: recording.id?.call_recording_id || "",
1700
+ meeting_id: recording.id?.meeting_id || "",
1701
+ status: recording.status || "",
1702
+ created_at: recording.created_at || "",
1703
+ web_url: recording.web_url || ""
1704
+ };
1705
+ }
1706
+ async function listRecordingsForMeeting(meetingId, opts) {
1707
+ const client = new AttioClient(opts.apiKey, opts.debug);
1708
+ const all = !!opts.all;
1709
+ const limit = Number(opts.limit) || 50;
1710
+ const allRecordings = [];
1711
+ let cursor = opts.cursor;
1712
+ while (true) {
1713
+ const params = new URLSearchParams();
1714
+ params.set("limit", String(limit));
1715
+ if (cursor) params.set("cursor", cursor);
1716
+ const res = await client.get(
1717
+ `/meetings/${encodeURIComponent(meetingId)}/call_recordings?${params.toString()}`
1718
+ );
1719
+ allRecordings.push(...res.data);
1720
+ if (!all) break;
1721
+ cursor = res.pagination?.next_cursor ?? void 0;
1722
+ if (!cursor) break;
1723
+ }
1724
+ return allRecordings;
1725
+ }
1726
+ async function fetchTranscript(client, meetingId, recordingId, opts) {
1727
+ const transcriptSegments = [];
1728
+ let cursor = opts.cursor;
1729
+ while (true) {
1730
+ const params = new URLSearchParams();
1731
+ if (cursor) params.set("cursor", cursor);
1732
+ const query = params.toString();
1733
+ const path = `/meetings/${encodeURIComponent(meetingId)}/call_recordings/${encodeURIComponent(recordingId)}/transcript${query ? `?${query}` : ""}`;
1734
+ const res = await client.get(path);
1735
+ const segmentData = res.data;
1736
+ if (Array.isArray(segmentData.transcript)) {
1737
+ transcriptSegments.push(...segmentData.transcript);
1738
+ }
1739
+ if (!opts.allTranscript) {
1740
+ return { ...segmentData, transcript: transcriptSegments };
1741
+ }
1742
+ cursor = res.pagination?.next_cursor ?? void 0;
1743
+ if (!cursor) {
1744
+ return { ...segmentData, transcript: transcriptSegments };
1745
+ }
1746
+ }
1747
+ }
1748
+ function register17(program2) {
1749
+ const cmd = program2.command("recordings").description("Manage call recordings (Beta API)");
1750
+ cmd.command("list").description("List call recordings for a meeting (Beta API)").requiredOption("--meeting <meeting-id>", "Meeting ID (required)").option("--limit <n>", "Maximum recordings per page", "50").option("--cursor <cursor>", "Pagination cursor").option("--all", "Auto-paginate through all pages").action(async (_options, command) => {
1751
+ const opts = command.optsWithGlobals();
1752
+ const format = detectFormat(opts);
1753
+ const recordings = await listRecordingsForMeeting(opts.meeting, opts);
1754
+ if (format === "quiet") {
1755
+ for (const recording of recordings) {
1756
+ console.log(recording.id?.call_recording_id || "");
1757
+ }
1758
+ return;
1759
+ }
1760
+ if (format === "json") {
1761
+ outputList(recordings, { format, idField: "id" });
1762
+ return;
1763
+ }
1764
+ outputList(recordings.map(flattenRecording), {
1765
+ format,
1766
+ columns: ["id", "meeting_id", "status", "created_at", "web_url"],
1767
+ idField: "id"
1768
+ });
1769
+ });
1770
+ cmd.command("get <id>").description("Get a call recording by ID (Beta API)").requiredOption("--meeting <meeting-id>", "Meeting ID (required)").option("--transcript", "Include transcript data in the response").option("--cursor <cursor>", "Transcript pagination cursor").option("--all-transcript", "Fetch all transcript pages when using --transcript").action(async (id, _options, command) => {
1771
+ const opts = command.optsWithGlobals();
1772
+ const client = new AttioClient(opts.apiKey, opts.debug);
1773
+ const format = detectFormat(opts);
1774
+ const res = await client.get(
1775
+ `/meetings/${encodeURIComponent(opts.meeting)}/call_recordings/${encodeURIComponent(id)}`
1776
+ );
1777
+ const recording = res.data;
1778
+ if (opts.transcript) {
1779
+ const transcript = await fetchTranscript(client, opts.meeting, id, opts);
1780
+ recording.transcript = transcript;
1781
+ }
1782
+ if (format === "json") {
1783
+ outputSingle(recording, { format, idField: "id" });
1784
+ return;
1785
+ }
1786
+ outputSingle(flattenRecording(recording), { format, idField: "id" });
1787
+ });
1788
+ }
1789
+
1790
+ // src/commands/webhooks.ts
1791
+ import { readFileSync as readFileSync2 } from "fs";
1792
+ var WEBHOOK_EVENT_TYPES = [
1793
+ "call-recording.created",
1794
+ "comment.created",
1795
+ "comment.resolved",
1796
+ "comment.unresolved",
1797
+ "comment.deleted",
1798
+ "list.created",
1799
+ "list.updated",
1800
+ "list.deleted",
1801
+ "list-attribute.created",
1802
+ "list-attribute.updated",
1803
+ "list-entry.created",
1804
+ "list-entry.updated",
1805
+ "list-entry.deleted",
1806
+ "object-attribute.created",
1807
+ "object-attribute.updated",
1808
+ "note.created",
1809
+ "note-content.updated",
1810
+ "note.updated",
1811
+ "note.deleted",
1812
+ "record.created",
1813
+ "record.merged",
1814
+ "record.updated",
1815
+ "record.deleted",
1816
+ "task.created",
1817
+ "task.updated",
1818
+ "task.deleted",
1819
+ "workspace-member.created"
1820
+ ];
1821
+ function parseJsonInput(raw) {
1822
+ if (raw.startsWith("@")) {
1823
+ return JSON.parse(readFileSync2(raw.slice(1), "utf-8"));
1824
+ }
1825
+ return JSON.parse(raw);
1826
+ }
1827
+ function flattenWebhook(item) {
1828
+ return {
1829
+ id: item.id?.webhook_id || "",
1830
+ target_url: item.target_url || "",
1831
+ status: item.status || "",
1832
+ subscriptions: Array.isArray(item.subscriptions) ? item.subscriptions.length : 0,
1833
+ created_at: item.created_at || ""
1834
+ };
1835
+ }
1836
+ function collectEvents(rawEvents) {
1837
+ if (rawEvents.length === 0) return [];
1838
+ const invalid = rawEvents.filter((event) => !WEBHOOK_EVENT_TYPES.includes(event));
1839
+ if (invalid.length > 0) {
1840
+ throw new Error(`Unsupported webhook event type(s): ${invalid.join(", ")}`);
1841
+ }
1842
+ return rawEvents;
1843
+ }
1844
+ function parseSubscriptions(opts) {
1845
+ if (opts.subscriptions) {
1846
+ const parsed = parseJsonInput(opts.subscriptions);
1847
+ if (!Array.isArray(parsed)) {
1848
+ throw new Error("--subscriptions must be a JSON array or @file containing a JSON array.");
1849
+ }
1850
+ return parsed;
1851
+ }
1852
+ const events = collectEvents(opts.event ?? []);
1853
+ if (events.length === 0) {
1854
+ throw new Error("Provide at least one --event or --subscriptions for webhook subscriptions.");
1855
+ }
1856
+ const filter = opts.filterJson ? parseJsonInput(opts.filterJson) : null;
1857
+ return events.map((eventType) => ({ event_type: eventType, filter }));
1858
+ }
1859
+ function register18(program2) {
1860
+ const cmd = program2.command("webhooks").description("Manage webhooks");
1861
+ cmd.command("events").description("List all supported webhook event types").action(async (_options, command) => {
1862
+ const opts = command.optsWithGlobals();
1863
+ const format = detectFormat(opts);
1864
+ const events = WEBHOOK_EVENT_TYPES.map((eventType) => ({ event_type: eventType }));
1865
+ outputList(events, { format, columns: ["event_type"], idField: "event_type" });
1866
+ });
1867
+ cmd.command("list").description("List webhooks").option("--limit <n>", "Maximum webhooks to return", "25").option("--offset <n>", "Number of webhooks to skip", "0").action(async (_options, command) => {
1868
+ const opts = command.optsWithGlobals();
1869
+ const client = new AttioClient(opts.apiKey, opts.debug);
1870
+ const format = detectFormat(opts);
1871
+ const params = new URLSearchParams();
1872
+ params.set("limit", String(Number(opts.limit) || 25));
1873
+ params.set("offset", String(Number(opts.offset) || 0));
1874
+ const res = await client.get(`/webhooks?${params.toString()}`);
1875
+ const webhooks = res.data;
1876
+ if (format === "quiet") {
1877
+ for (const webhook of webhooks) {
1878
+ console.log(webhook.id?.webhook_id || "");
1879
+ }
1880
+ return;
1881
+ }
1882
+ if (format === "json") {
1883
+ outputList(webhooks, { format, idField: "id" });
1884
+ return;
1885
+ }
1886
+ outputList(webhooks.map(flattenWebhook), {
1887
+ format,
1888
+ columns: ["id", "target_url", "status", "subscriptions", "created_at"],
1889
+ idField: "id"
1890
+ });
1891
+ });
1892
+ cmd.command("get <id>").description("Get a webhook by ID").action(async (id, _options, command) => {
1893
+ const opts = command.optsWithGlobals();
1894
+ const client = new AttioClient(opts.apiKey, opts.debug);
1895
+ const format = detectFormat(opts);
1896
+ const res = await client.get(`/webhooks/${encodeURIComponent(id)}`);
1897
+ const webhook = res.data;
1898
+ if (format === "json") {
1899
+ outputSingle(webhook, { format, idField: "id" });
1900
+ return;
1901
+ }
1902
+ outputSingle(flattenWebhook(webhook), { format, idField: "id" });
1903
+ });
1904
+ cmd.command("create").description("Create a webhook").requiredOption("--target-url <url>", "Webhook destination URL (https only)").option(
1905
+ "--event <event-type>",
1906
+ 'Webhook event type (repeatable). Use "webhooks events" to list all',
1907
+ (val, prev) => [...prev, val],
1908
+ []
1909
+ ).option("--filter-json <json>", "JSON filter for all --event subscriptions").option("--subscriptions <json>", "Full subscriptions JSON array or @file (overrides --event)").action(async (_options, command) => {
1910
+ const opts = command.optsWithGlobals();
1911
+ const client = new AttioClient(opts.apiKey, opts.debug);
1912
+ const format = detectFormat(opts);
1913
+ const subscriptions = parseSubscriptions(opts);
1914
+ const res = await client.post("/webhooks", {
1915
+ data: {
1916
+ target_url: opts.targetUrl,
1917
+ subscriptions
1918
+ }
1919
+ });
1920
+ outputSingle(res.data, { format, idField: "id" });
1921
+ });
1922
+ cmd.command("update <id>").description("Update a webhook").option("--target-url <url>", "Webhook destination URL (https only)").option(
1923
+ "--event <event-type>",
1924
+ 'Webhook event type (repeatable). Use "webhooks events" to list all',
1925
+ (val, prev) => [...prev, val],
1926
+ []
1927
+ ).option("--filter-json <json>", "JSON filter for all --event subscriptions").option("--subscriptions <json>", "Full subscriptions JSON array or @file (overrides --event)").action(async (id, _options, command) => {
1928
+ const opts = command.optsWithGlobals();
1929
+ const client = new AttioClient(opts.apiKey, opts.debug);
1930
+ const format = detectFormat(opts);
1931
+ const data = {};
1932
+ if (opts.targetUrl) {
1933
+ data.target_url = opts.targetUrl;
1934
+ }
1935
+ const hasSubscriptionFlags = !!opts.subscriptions || !!opts.filterJson || (opts.event ?? []).length > 0;
1936
+ if (hasSubscriptionFlags) {
1937
+ data.subscriptions = parseSubscriptions(opts);
1938
+ }
1939
+ if (Object.keys(data).length === 0) {
1940
+ throw new Error("Nothing to update. Provide --target-url and/or subscription options.");
1941
+ }
1942
+ const res = await client.patch(`/webhooks/${encodeURIComponent(id)}`, { data });
1943
+ outputSingle(res.data, { format, idField: "id" });
1944
+ });
1945
+ cmd.command("delete <id>").description("Delete a webhook").option("-y, --yes", "Skip confirmation").action(async (id, _options, command) => {
1946
+ const opts = command.optsWithGlobals();
1947
+ const client = new AttioClient(opts.apiKey, opts.debug);
1948
+ if (!opts.yes) {
1949
+ const ok = await confirm(`Delete webhook ${id}?`);
1950
+ if (!ok) {
1951
+ console.error("Aborted.");
1952
+ return;
1953
+ }
1954
+ }
1955
+ await client.delete(`/webhooks/${encodeURIComponent(id)}`);
1956
+ console.error("Deleted.");
1957
+ });
1958
+ }
1959
+
1238
1960
  // src/commands/members.ts
1239
- function register12(program2) {
1961
+ function register19(program2) {
1240
1962
  const members = program2.command("members").description("Manage workspace members");
1241
1963
  members.command("list").description("List all workspace members").action(async function() {
1242
1964
  const opts = this.optsWithGlobals();
@@ -1271,7 +1993,7 @@ function register12(program2) {
1271
1993
 
1272
1994
  // src/commands/config.ts
1273
1995
  import chalk4 from "chalk";
1274
- function register13(program2) {
1996
+ function register20(program2) {
1275
1997
  const cmd = program2.command("config").description("Manage CLI configuration");
1276
1998
  cmd.command("set <key> <value>").description("Set a config value (e.g., attio config set api-key <key>)").action((key, value) => {
1277
1999
  if (key === "api-key") {
@@ -1395,13 +2117,14 @@ Multiple \`--filter\` flags are ANDed. Use \`--filter-json '{...}'\` for raw Att
1395
2117
  import { execFile } from "child_process";
1396
2118
  import { platform } from "os";
1397
2119
  import chalk5 from "chalk";
1398
- function register14(program2) {
2120
+ function register21(program2) {
1399
2121
  program2.command("open <object> [record-id]").description("Open an object or record in the Attio web app").action(async (object, recordId, options, command) => {
1400
2122
  const opts = command.optsWithGlobals();
2123
+ const objectSlug = encodeURIComponent(object);
1401
2124
  let url;
1402
2125
  if (recordId) {
1403
2126
  const client = new AttioClient(opts.apiKey, opts.debug);
1404
- const res = await client.get(`/objects/${object}/records/${recordId}`);
2127
+ const res = await client.get(`/objects/${objectSlug}/records/${encodeURIComponent(recordId)}`);
1405
2128
  const record = res.data;
1406
2129
  url = record.web_url;
1407
2130
  if (!url) {
@@ -1412,9 +2135,9 @@ function register14(program2) {
1412
2135
  const client = new AttioClient(opts.apiKey, opts.debug);
1413
2136
  const self = await client.get("/self");
1414
2137
  const slug = self.workspace_slug || "";
1415
- url = `https://app.attio.com/${slug}/${object}`;
2138
+ url = `https://app.attio.com/${encodeURIComponent(slug)}/${objectSlug}`;
1416
2139
  }
1417
- const cmd = platform() === "darwin" ? "open" : platform() === "win32" ? "start" : "xdg-open";
2140
+ const cmd = platform() === "darwin" ? "open" : platform() === "win32" ? "explorer" : "xdg-open";
1418
2141
  execFile(cmd, [url], (err) => {
1419
2142
  if (err) {
1420
2143
  console.log(url);
@@ -1426,7 +2149,7 @@ function register14(program2) {
1426
2149
  // src/commands/init.ts
1427
2150
  import { createInterface as createInterface2 } from "readline";
1428
2151
  import chalk6 from "chalk";
1429
- function register15(program2) {
2152
+ function register22(program2) {
1430
2153
  program2.command("init").description("Interactive setup wizard \u2014 connect to your Attio workspace").action(async function() {
1431
2154
  const opts = this.optsWithGlobals();
1432
2155
  let apiKey = opts.apiKey;
@@ -1515,6 +2238,18 @@ function register15(program2) {
1515
2238
  }
1516
2239
 
1517
2240
  // bin/attio.ts
2241
+ function loadCliVersion() {
2242
+ try {
2243
+ const here = dirname(fileURLToPath(import.meta.url));
2244
+ const packagePath = join2(here, "..", "package.json");
2245
+ const packageJson = JSON.parse(readFileSync3(packagePath, "utf-8"));
2246
+ if (typeof packageJson.version === "string" && packageJson.version.length > 0) {
2247
+ return packageJson.version;
2248
+ }
2249
+ } catch {
2250
+ }
2251
+ return "0.2.0";
2252
+ }
1518
2253
  function handleError(err) {
1519
2254
  const jsonMode = program.opts().json || !process.stdout.isTTY;
1520
2255
  if (err instanceof AttioApiError) {
@@ -1559,7 +2294,7 @@ function handleError(err) {
1559
2294
  }
1560
2295
  process.exit(1);
1561
2296
  }
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");
2297
+ program.name("attio").version(loadCliVersion()).description("CLI for the Attio CRM API. Built for scripts, agents, and humans who prefer terminals.").option("--api-key <key>", "Override API key").option("--json", "Force JSON output").option("--table", "Force table output").option("--csv", "Force CSV output").option("-q, --quiet", "Only output IDs").option("--no-color", "Disable colors").option("--debug", "Print request/response details to stderr");
1563
2298
  if (process.argv.includes("--no-color")) {
1564
2299
  process.env.NO_COLOR = "1";
1565
2300
  }
@@ -1578,6 +2313,13 @@ register12(program);
1578
2313
  register13(program);
1579
2314
  register14(program);
1580
2315
  register15(program);
2316
+ register16(program);
2317
+ register17(program);
2318
+ register18(program);
2319
+ register19(program);
2320
+ register20(program);
2321
+ register21(program);
2322
+ register22(program);
1581
2323
  program.action(() => {
1582
2324
  if (!isConfigured()) {
1583
2325
  console.error("");