attio-cli 0.2.1 → 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 +701 -59
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -51,14 +51,18 @@ attio config claude-md >> CLAUDE.md
51
51
  | `attio records create <object>` | Create a new record |
52
52
  | `attio records update <object> <id>` | Update an existing record |
53
53
  | `attio records delete <object> <id>` | Delete a record |
54
- | `attio records upsert <object>` | Create or update a record by matching attribute |
55
- | `attio records search <object>` | Full-text search across records |
54
+ | `attio records assert <object>` | Create or update a record by matching attribute |
55
+ | `attio records upsert <object>` | Alias for `records assert` |
56
+ | `attio records search <query>` | Full-text search across one or more objects |
57
+ | `attio records values <object> <id>` | List current and historic attribute values |
58
+ | `attio records entries <object> <id>` | List list entries where this record is the parent |
56
59
  | **People** | |
57
60
  | `attio people list` | List people |
58
61
  | `attio people get <id>` | Get a person by ID |
59
62
  | `attio people create` | Create a person |
60
63
  | `attio people update <id>` | Update a person |
61
64
  | `attio people delete <id>` | Delete a person |
65
+ | `attio people assert` | Assert (upsert) a person by matching attribute |
62
66
  | `attio people search <query>` | Search people by name or email |
63
67
  | **Companies** | |
64
68
  | `attio companies list` | List companies |
@@ -66,7 +70,32 @@ attio config claude-md >> CLAUDE.md
66
70
  | `attio companies create` | Create a company |
67
71
  | `attio companies update <id>` | Update a company |
68
72
  | `attio companies delete <id>` | Delete a company |
73
+ | `attio companies assert` | Assert (upsert) a company by matching attribute |
69
74
  | `attio companies search <query>` | Search companies by name or domain |
75
+ | **Deals** | |
76
+ | `attio deals list` | List deals |
77
+ | `attio deals get <id>` | Get a deal by ID |
78
+ | `attio deals create` | Create a deal |
79
+ | `attio deals update <id>` | Update a deal |
80
+ | `attio deals delete <id>` | Delete a deal |
81
+ | `attio deals assert` | Assert (upsert) a deal by matching attribute |
82
+ | `attio deals search <query>` | Search deals |
83
+ | **Users** | |
84
+ | `attio users list` | List users |
85
+ | `attio users get <id>` | Get a user by ID |
86
+ | `attio users create` | Create a user |
87
+ | `attio users update <id>` | Update a user |
88
+ | `attio users delete <id>` | Delete a user |
89
+ | `attio users assert` | Assert (upsert) a user by matching attribute |
90
+ | `attio users search <query>` | Search users |
91
+ | **Workspaces (Standard Object)** | |
92
+ | `attio workspaces list` | List workspace records |
93
+ | `attio workspaces get <id>` | Get a workspace record by ID |
94
+ | `attio workspaces create` | Create a workspace record |
95
+ | `attio workspaces update <id>` | Update a workspace record |
96
+ | `attio workspaces delete <id>` | Delete a workspace record |
97
+ | `attio workspaces assert` | Assert (upsert) a workspace record by matching attribute |
98
+ | `attio workspaces search <query>` | Search workspace records |
70
99
  | **Lists** | |
71
100
  | `attio lists list` | List all lists |
72
101
  | `attio lists get <id>` | Get a specific list |
@@ -74,6 +103,7 @@ attio config claude-md >> CLAUDE.md
74
103
  | `attio entries list <list>` | List entries in a list |
75
104
  | `attio entries get <list> <id>` | Get a specific entry |
76
105
  | `attio entries create <list>` | Add an entry to a list |
106
+ | `attio entries assert <list>` | Assert (upsert) an entry by parent record |
77
107
  | `attio entries update <list> <id>` | Update a list entry |
78
108
  | `attio entries delete <list> <id>` | Remove an entry from a list |
79
109
  | **Tasks** | |
@@ -91,6 +121,22 @@ attio config claude-md >> CLAUDE.md
91
121
  | `attio comments list` | List comments on a thread |
92
122
  | `attio comments create` | Create a comment |
93
123
  | `attio comments delete <id>` | Delete a comment |
124
+ | **Threads** | |
125
+ | `attio threads list` | List threaded conversations |
126
+ | `attio threads get <id>` | Get a thread by ID |
127
+ | **Meetings (Beta)** | |
128
+ | `attio meetings list` | List meetings (beta endpoint) |
129
+ | `attio meetings get <id>` | Get a meeting (beta endpoint) |
130
+ | **Recordings (Beta)** | |
131
+ | `attio recordings list --meeting <id>` | List call recordings for a meeting |
132
+ | `attio recordings get <id> --meeting <id>` | Get a call recording, optionally with transcript |
133
+ | **Webhooks** | |
134
+ | `attio webhooks events` | List supported webhook event types |
135
+ | `attio webhooks list` | List webhooks |
136
+ | `attio webhooks get <id>` | Get a webhook by ID |
137
+ | `attio webhooks create` | Create a webhook |
138
+ | `attio webhooks update <id>` | Update a webhook |
139
+ | `attio webhooks delete <id>` | Delete a webhook |
94
140
  | **Members** | |
95
141
  | `attio members list` | List workspace members |
96
142
  | **Config** | |
package/dist/attio.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // bin/attio.ts
4
4
  import { program } from "commander";
5
5
  import chalk7 from "chalk";
6
- import { readFileSync as readFileSync2 } from "fs";
6
+ import { readFileSync as readFileSync3 } from "fs";
7
7
  import { dirname, join as join2 } from "path";
8
8
  import { fileURLToPath } from "url";
9
9
 
@@ -568,8 +568,8 @@ async function readStdin() {
568
568
  async function resolveValues(options) {
569
569
  if (options.values) {
570
570
  if (options.values.startsWith("@")) {
571
- const { readFileSync: readFileSync3 } = await import("fs");
572
- return JSON.parse(readFileSync3(options.values.slice(1), "utf-8"));
571
+ const { readFileSync: readFileSync4 } = await import("fs");
572
+ return JSON.parse(readFileSync4(options.values.slice(1), "utf-8"));
573
573
  }
574
574
  return JSON.parse(options.values);
575
575
  }
@@ -610,6 +610,29 @@ async function paginate(fetchPage, options) {
610
610
  }
611
611
 
612
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
+ }
613
636
  async function listRecords(object, cmdOpts) {
614
637
  const client = new AttioClient(cmdOpts.apiKey, cmdOpts.debug);
615
638
  const format = detectFormat(cmdOpts);
@@ -624,8 +647,8 @@ async function listRecords(object, cmdOpts) {
624
647
  if (cmdOpts.sort && cmdOpts.sort.length > 0) {
625
648
  sorts = cmdOpts.sort.map(parseSort);
626
649
  }
627
- const limit = Number(cmdOpts.limit) || 25;
628
- const offset = Number(cmdOpts.offset) || 0;
650
+ const limit = parseLimit(cmdOpts.limit, 25);
651
+ const offset = parseOffset(cmdOpts.offset);
629
652
  const all = cmdOpts.all ?? false;
630
653
  const fetchPage = async (pageLimit, pageOffset) => {
631
654
  const body = {};
@@ -641,8 +664,8 @@ async function listRecords(object, cmdOpts) {
641
664
  };
642
665
  const records = await paginate(fetchPage, { limit, offset, all });
643
666
  if (format === "quiet") {
644
- for (const r of records) {
645
- console.log(r.id?.record_id ?? "");
667
+ for (const record of records) {
668
+ console.log(record.id?.record_id ?? "");
646
669
  }
647
670
  return;
648
671
  }
@@ -723,12 +746,12 @@ async function deleteRecord(object, recordId, cmdOpts) {
723
746
  );
724
747
  console.error("Deleted.");
725
748
  }
726
- async function upsertRecord(object, cmdOpts) {
749
+ async function assertRecord(object, cmdOpts) {
727
750
  const client = new AttioClient(cmdOpts.apiKey, cmdOpts.debug);
728
751
  const format = detectFormat(cmdOpts);
729
752
  const matchAttr = cmdOpts.match;
730
753
  if (!matchAttr) {
731
- throw new Error("--match <attribute-slug> is required for upsert");
754
+ throw new Error("--match <attribute-slug> is required for assert");
732
755
  }
733
756
  const values = requireValues(await resolveValues(cmdOpts));
734
757
  const res = await client.put(
@@ -746,23 +769,32 @@ async function upsertRecord(object, cmdOpts) {
746
769
  }
747
770
  outputSingle(flattenRecord(record), { format });
748
771
  }
749
- async function searchRecords(object, query, cmdOpts) {
772
+ async function searchRecords(query, cmdOpts, objectScope) {
750
773
  const client = new AttioClient(cmdOpts.apiKey, cmdOpts.debug);
751
774
  const format = detectFormat(cmdOpts);
752
- 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
+ }
753
785
  const res = await client.post(
754
786
  "/objects/records/search",
755
787
  {
756
788
  query,
757
- objects: [object],
789
+ objects,
758
790
  request_as: { type: "workspace" },
759
791
  limit
760
792
  }
761
793
  );
762
794
  const records = res.data;
763
795
  if (format === "quiet") {
764
- for (const r of records) {
765
- console.log(r.id?.record_id ?? "");
796
+ for (const record of records) {
797
+ console.log(record.id?.record_id ?? "");
766
798
  }
767
799
  return;
768
800
  }
@@ -773,6 +805,87 @@ async function searchRecords(object, query, cmdOpts) {
773
805
  const flat = records.map(flattenRecord);
774
806
  outputList(flat, { format });
775
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
+ }
776
889
  function register4(program2) {
777
890
  const records = program2.command("records").description("Manage records in any Attio object");
778
891
  records.command("list").description("List or query records for an object").argument("<object>", "Object slug or ID (e.g. companies, people)").option(
@@ -786,12 +899,10 @@ function register4(program2) {
786
899
  (val, prev) => [...prev, val],
787
900
  []
788
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) => {
789
- const opts = cmd.optsWithGlobals();
790
- await listRecords(object, opts);
902
+ await listRecords(object, cmd.optsWithGlobals());
791
903
  });
792
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) => {
793
- const opts = cmd.optsWithGlobals();
794
- await getRecord(object, recordId, opts);
905
+ await getRecord(object, recordId, cmd.optsWithGlobals());
795
906
  });
796
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(
797
908
  "--set <key=value>",
@@ -799,8 +910,7 @@ function register4(program2) {
799
910
  (val, prev) => [...prev, val],
800
911
  []
801
912
  ).action(async (object, _opts, cmd) => {
802
- const opts = cmd.optsWithGlobals();
803
- await createRecord(object, opts);
913
+ await createRecord(object, cmd.optsWithGlobals());
804
914
  });
805
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(
806
916
  "--set <key=value>",
@@ -808,76 +918,175 @@ function register4(program2) {
808
918
  (val, prev) => [...prev, val],
809
919
  []
810
920
  ).action(async (object, recordId, _opts, cmd) => {
811
- const opts = cmd.optsWithGlobals();
812
- await updateRecord(object, recordId, opts);
921
+ await updateRecord(object, recordId, cmd.optsWithGlobals());
813
922
  });
814
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) => {
815
- const opts = cmd.optsWithGlobals();
816
- await deleteRecord(object, recordId, opts);
924
+ await deleteRecord(object, recordId, cmd.optsWithGlobals());
925
+ });
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(
927
+ "--set <key=value>",
928
+ "Set an attribute value (repeatable)",
929
+ (val, prev) => [...prev, val],
930
+ []
931
+ ).action(async (object, _opts, cmd) => {
932
+ await assertRecord(object, cmd.optsWithGlobals());
817
933
  });
818
- 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(
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(
819
935
  "--set <key=value>",
820
936
  "Set an attribute value (repeatable)",
821
937
  (val, prev) => [...prev, val],
822
938
  []
823
939
  ).action(async (object, _opts, cmd) => {
824
- const opts = cmd.optsWithGlobals();
825
- await upsertRecord(object, opts);
940
+ await assertRecord(object, cmd.optsWithGlobals());
826
941
  });
827
- 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) => {
828
- const opts = cmd.optsWithGlobals();
829
- await searchRecords(object, query, opts);
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());
952
+ });
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());
830
955
  });
831
956
  }
832
957
 
833
958
  // src/commands/people.ts
834
959
  function register5(program2) {
835
960
  const cmd = program2.command("people").description("Manage people records (shortcut for: records <cmd> people)");
836
- cmd.command("list").description("List people").option("--filter <expr>", 'Filter: = != ~ !~ ^ > >= < <= ? (e.g. "name~Acme"). Repeatable', (v, p) => [...p, v], []).option("--filter-json <json>", "Raw JSON filter").option("--sort <expr>", "Sort expression (repeatable)", (v, p) => [...p, v], []).option("--limit <n>", "Max results per page", "25").option("--offset <n>", "Starting offset", "0").option("--all", "Fetch all pages").action(async (options, command) => {
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) => {
837
962
  await listRecords("people", command.optsWithGlobals());
838
963
  });
839
- 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) => {
840
965
  await getRecord("people", recordId, command.optsWithGlobals());
841
966
  });
842
- 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) => {
843
968
  await createRecord("people", command.optsWithGlobals());
844
969
  });
845
- 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) => {
846
971
  await updateRecord("people", recordId, command.optsWithGlobals());
847
972
  });
848
- 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) => {
849
974
  await deleteRecord("people", recordId, command.optsWithGlobals());
850
975
  });
851
- cmd.command("search <query>").description("Search people by name or email").option("--limit <n>", "Maximum results", "25").action(async (query, options, command) => {
852
- 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"]);
853
981
  });
854
982
  }
855
983
 
856
984
  // src/commands/companies.ts
857
985
  function register6(program2) {
858
986
  const cmd = program2.command("companies").description("Manage company records (shortcut for: records <cmd> companies)");
859
- cmd.command("list").description("List companies").option("--filter <expr>", 'Filter: = != ~ !~ ^ > >= < <= ? (e.g. "name~Acme"). Repeatable', (v, p) => [...p, v], []).option("--filter-json <json>", "Raw JSON filter").option("--sort <expr>", "Sort expression (repeatable)", (v, p) => [...p, v], []).option("--limit <n>", "Max results per page", "25").option("--offset <n>", "Starting offset", "0").option("--all", "Fetch all pages").action(async (options, command) => {
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) => {
860
988
  await listRecords("companies", command.optsWithGlobals());
861
989
  });
862
- 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) => {
863
991
  await getRecord("companies", recordId, command.optsWithGlobals());
864
992
  });
865
- 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) => {
866
994
  await createRecord("companies", command.optsWithGlobals());
867
995
  });
868
- 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) => {
869
997
  await updateRecord("companies", recordId, command.optsWithGlobals());
870
998
  });
871
- 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) => {
872
1000
  await deleteRecord("companies", recordId, command.optsWithGlobals());
873
1001
  });
874
- cmd.command("search <query>").description("Search companies by name or domain").option("--limit <n>", "Maximum results", "25").action(async (query, options, command) => {
875
- 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"]);
876
1007
  });
877
1008
  }
878
1009
 
879
- // src/commands/lists.ts
1010
+ // src/commands/deals.ts
880
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) {
881
1090
  const cmd = program2.command("lists").description("Manage lists");
882
1091
  cmd.command("list").description("List all lists").action(async (_options, command) => {
883
1092
  const opts = command.optsWithGlobals();
@@ -931,7 +1140,7 @@ function register7(program2) {
931
1140
  }
932
1141
 
933
1142
  // src/commands/entries.ts
934
- function flattenEntry(entry) {
1143
+ function flattenEntry2(entry) {
935
1144
  const flat = {
936
1145
  id: entry.id?.entry_id || "",
937
1146
  record_id: entry.record_id || entry.parent_record_id || "",
@@ -942,7 +1151,7 @@ function flattenEntry(entry) {
942
1151
  }
943
1152
  return flat;
944
1153
  }
945
- function register8(program2) {
1154
+ function register11(program2) {
946
1155
  const cmd = program2.command("entries").description("Manage list entries");
947
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) => {
948
1157
  const opts = command.optsWithGlobals();
@@ -972,7 +1181,7 @@ function register8(program2) {
972
1181
  outputList(entries, { format, idField: "id" });
973
1182
  return;
974
1183
  }
975
- const flat = entries.map(flattenEntry);
1184
+ const flat = entries.map(flattenEntry2);
976
1185
  outputList(flat, { format, idField: "id" });
977
1186
  });
978
1187
  cmd.command("get <list> <entry-id>").description("Get an entry by ID").action(async (list, entryId, _options, command) => {
@@ -985,7 +1194,7 @@ function register8(program2) {
985
1194
  outputSingle(entry, { format, idField: "id" });
986
1195
  return;
987
1196
  }
988
- const flat = flattenEntry(entry);
1197
+ const flat = flattenEntry2(entry);
989
1198
  outputSingle(flat, { format, idField: "id" });
990
1199
  });
991
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) => {
@@ -1006,7 +1215,28 @@ function register8(program2) {
1006
1215
  outputSingle(entry, { format, idField: "id" });
1007
1216
  return;
1008
1217
  }
1009
- 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);
1010
1240
  outputSingle(flat, { format, idField: "id" });
1011
1241
  });
1012
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) => {
@@ -1025,7 +1255,7 @@ function register8(program2) {
1025
1255
  outputSingle(entry, { format, idField: "id" });
1026
1256
  return;
1027
1257
  }
1028
- const flat = flattenEntry(entry);
1258
+ const flat = flattenEntry2(entry);
1029
1259
  outputSingle(flat, { format, idField: "id" });
1030
1260
  });
1031
1261
  cmd.command("delete <list> <entry-id>").description("Delete an entry").option("-y, --yes", "Skip confirmation").action(async (list, entryId, _options, command) => {
@@ -1044,7 +1274,7 @@ function register8(program2) {
1044
1274
  }
1045
1275
 
1046
1276
  // src/commands/tasks.ts
1047
- function register9(program2) {
1277
+ function register12(program2) {
1048
1278
  const tasks = program2.command("tasks").description("Manage tasks");
1049
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) => {
1050
1280
  const opts = command.optsWithGlobals();
@@ -1155,7 +1385,7 @@ function truncate(str, max) {
1155
1385
  }
1156
1386
 
1157
1387
  // src/commands/notes.ts
1158
- function register10(program2) {
1388
+ function register13(program2) {
1159
1389
  const notes = program2.command("notes").description("Manage notes");
1160
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) => {
1161
1391
  const opts = command.optsWithGlobals();
@@ -1228,7 +1458,7 @@ function register10(program2) {
1228
1458
  }
1229
1459
 
1230
1460
  // src/commands/comments.ts
1231
- function register11(program2) {
1461
+ function register14(program2) {
1232
1462
  const comments = program2.command("comments").description("Manage comments on records");
1233
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) => {
1234
1464
  const opts = command.optsWithGlobals();
@@ -1322,8 +1552,413 @@ function truncate2(str, max) {
1322
1552
  return str.slice(0, max - 3) + "...";
1323
1553
  }
1324
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
+
1325
1960
  // src/commands/members.ts
1326
- function register12(program2) {
1961
+ function register19(program2) {
1327
1962
  const members = program2.command("members").description("Manage workspace members");
1328
1963
  members.command("list").description("List all workspace members").action(async function() {
1329
1964
  const opts = this.optsWithGlobals();
@@ -1358,7 +1993,7 @@ function register12(program2) {
1358
1993
 
1359
1994
  // src/commands/config.ts
1360
1995
  import chalk4 from "chalk";
1361
- function register13(program2) {
1996
+ function register20(program2) {
1362
1997
  const cmd = program2.command("config").description("Manage CLI configuration");
1363
1998
  cmd.command("set <key> <value>").description("Set a config value (e.g., attio config set api-key <key>)").action((key, value) => {
1364
1999
  if (key === "api-key") {
@@ -1482,7 +2117,7 @@ Multiple \`--filter\` flags are ANDed. Use \`--filter-json '{...}'\` for raw Att
1482
2117
  import { execFile } from "child_process";
1483
2118
  import { platform } from "os";
1484
2119
  import chalk5 from "chalk";
1485
- function register14(program2) {
2120
+ function register21(program2) {
1486
2121
  program2.command("open <object> [record-id]").description("Open an object or record in the Attio web app").action(async (object, recordId, options, command) => {
1487
2122
  const opts = command.optsWithGlobals();
1488
2123
  const objectSlug = encodeURIComponent(object);
@@ -1514,7 +2149,7 @@ function register14(program2) {
1514
2149
  // src/commands/init.ts
1515
2150
  import { createInterface as createInterface2 } from "readline";
1516
2151
  import chalk6 from "chalk";
1517
- function register15(program2) {
2152
+ function register22(program2) {
1518
2153
  program2.command("init").description("Interactive setup wizard \u2014 connect to your Attio workspace").action(async function() {
1519
2154
  const opts = this.optsWithGlobals();
1520
2155
  let apiKey = opts.apiKey;
@@ -1607,7 +2242,7 @@ function loadCliVersion() {
1607
2242
  try {
1608
2243
  const here = dirname(fileURLToPath(import.meta.url));
1609
2244
  const packagePath = join2(here, "..", "package.json");
1610
- const packageJson = JSON.parse(readFileSync2(packagePath, "utf-8"));
2245
+ const packageJson = JSON.parse(readFileSync3(packagePath, "utf-8"));
1611
2246
  if (typeof packageJson.version === "string" && packageJson.version.length > 0) {
1612
2247
  return packageJson.version;
1613
2248
  }
@@ -1678,6 +2313,13 @@ register12(program);
1678
2313
  register13(program);
1679
2314
  register14(program);
1680
2315
  register15(program);
2316
+ register16(program);
2317
+ register17(program);
2318
+ register18(program);
2319
+ register19(program);
2320
+ register20(program);
2321
+ register21(program);
2322
+ register22(program);
1681
2323
  program.action(() => {
1682
2324
  if (!isConfigured()) {
1683
2325
  console.error("");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "attio-cli",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "CLI for the Attio CRM API. Built for scripts, agents, and humans who prefer terminals.",
5
5
  "license": "MIT",
6
6
  "type": "module",