not-manage 0.2.0 → 0.2.2

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.
@@ -1,3 +1,15 @@
1
+ const {
2
+ formatBoolean,
3
+ formatMoney,
4
+ readContactName,
5
+ readFirstMatterLabel,
6
+ readHours,
7
+ readMatterLabel,
8
+ readRoleList,
9
+ readStatus,
10
+ readUserName,
11
+ } = require("./resource-utils");
12
+
1
13
  const ACTIVITY_FIELDS =
2
14
  "id,type,date,quantity,quantity_in_hours,rounded_quantity,rounded_quantity_in_hours,price,total,billed,on_bill,non_billable,no_charge,flat_rate,contingency_fee,note,reference,created_at,updated_at,activity_description{id,name},bill{id,number,state},matter{id,display_number,number,description},user{id,name,first_name,last_name,email}";
3
15
  const BILL_FIELDS =
@@ -15,19 +27,254 @@ const BILLABLE_MATTER_FIELDS =
15
27
  "id,display_number,unbilled_hours,unbilled_amount,amount_in_trust,client{id,name,first_name,last_name}";
16
28
  const BILLABLE_CLIENT_FIELDS =
17
29
  "id,name,unbilled_hours,unbilled_amount,amount_in_trust,billable_matters_count";
30
+ const CALENDAR_ENTRY_FIELDS =
31
+ "id,summary,description,location,start_at,start_date,start_time,end_at,end_date,end_time,all_day,recurrence_rule,court_rule,permission,created_at,updated_at,calendar_owner{id,name,type,source,visible},matter{id,display_number,number,description,client},matter_docket{id,name,status},calendar_entry_event_type{id,name},attendees{id,name,type,email},calendars{id,name,type,source,visible},reminders{id,duration,next_delivery_at,state}";
32
+ const REMINDER_FIELDS =
33
+ "id,duration,next_delivery_at,state,created_at,updated_at,notification_method{id,type,email_address},subject{id,type,identifier,secondary_identifier,tertiary_identifier}";
34
+ const COMMUNICATION_FIELDS =
35
+ "id,subject,body,type,date,received_at,time_entries_count,created_at,updated_at,user{id,name,email},matter{id,display_number,number,description,client},senders{id,name,identifier,secondary_identifier,type},receivers{id,name,identifier,secondary_identifier,type}";
36
+ const CONVERSATION_FIELDS =
37
+ "id,archived,read_only,current_user_is_member,subject,message_count,time_entries_count,read,created_at,updated_at,last_message{id,date,body},first_message{id,date,body},matter{id,display_number,number,description,client}";
38
+ const CONVERSATION_MESSAGE_FIELDS =
39
+ "id,date,body,created_at,updated_at,sender{id,name,identifier,secondary_identifier,type},document{id,name,filename},conversation{id,subject},receivers{id,name,identifier,secondary_identifier,type}";
40
+ const NOTE_FIELDS =
41
+ "id,type,subject,detail,detail_text_type,date,created_at,updated_at,time_entries_count,matter{id,display_number,number,description,client},contact{id,name,first_name,last_name},author{id,name,email}";
42
+ const CUSTOM_FIELD_FIELDS =
43
+ "id,name,parent_type,field_type,displayed,deleted,required,display_order,created_at,updated_at,picklist_options{id,option,deleted_at}";
44
+ const OUTSTANDING_CLIENT_BALANCE_FIELDS =
45
+ "id,associated_matter_ids,last_payment_date,last_shared_date,newest_issued_bill_due_date,pending_payments_total,reminders_enabled,total_outstanding_balance,created_at,updated_at,contact{id,name,first_name,last_name}";
46
+ const MATTER_DOCKET_FIELDS =
47
+ "id,name,start_date,start_time,status,created_at,updated_at,deleted_at,matter{id,display_number,number,description,status,client},jurisdiction{id,description,system_id},trigger{id,description,system_id},service_type{id,description,system_id},calendar_entries{id,summary,start_at,start_date}";
48
+ const MY_EVENT_FIELDS =
49
+ "event{id,message,icon,title,title_url,description,description_url,primary_detail,primary_detail_url,secondary_detail,secondary_detail_url,occurred_at,mobile_icon,subject_type,subject_id}";
50
+
51
+ function readMatterClientName(matter) {
52
+ const single = readContactName(matter?.client);
53
+ if (single !== "-") {
54
+ return single;
55
+ }
56
+
57
+ const list = Array.isArray(matter?.clients) ? matter.clients : [];
58
+ if (list.length > 0) {
59
+ return readContactName(list[0]);
60
+ }
61
+
62
+ return "-";
63
+ }
64
+
65
+ function readTaskComplete(task) {
66
+ if (typeof task?.complete === "boolean") {
67
+ return task.complete;
68
+ }
69
+
70
+ if (typeof task?.status === "string") {
71
+ return task.status.toLowerCase() === "complete";
72
+ }
73
+
74
+ return undefined;
75
+ }
76
+
77
+ function summarizeValues(list, mapValue, maxItems = 3) {
78
+ const values = Array.isArray(list)
79
+ ? list
80
+ .map((item) => mapValue(item))
81
+ .filter((value) => value && value !== "-")
82
+ : [];
83
+
84
+ if (values.length === 0) {
85
+ return "-";
86
+ }
87
+
88
+ if (values.length <= maxItems) {
89
+ return values.join(", ");
90
+ }
91
+
92
+ return `${values.slice(0, maxItems).join(", ")}, +${values.length - maxItems} more`;
93
+ }
94
+
95
+ function readCalendarName(calendar) {
96
+ return String(calendar?.name || calendar?.type || calendar?.id || "-");
97
+ }
98
+
99
+ function readParticipantName(participant) {
100
+ if (!participant || typeof participant !== "object") {
101
+ return "-";
102
+ }
103
+
104
+ return (
105
+ participant.name ||
106
+ participant.identifier ||
107
+ participant.secondary_identifier ||
108
+ String(participant.id || "-")
109
+ );
110
+ }
111
+
112
+ function readPolymorphicObjectLabel(value) {
113
+ if (!value || typeof value !== "object") {
114
+ return "-";
115
+ }
116
+
117
+ return (
118
+ value.identifier ||
119
+ value.secondary_identifier ||
120
+ value.tertiary_identifier ||
121
+ value.type ||
122
+ String(value.id || "-")
123
+ );
124
+ }
125
+
126
+ function readDocumentLabel(document) {
127
+ if (!document || typeof document !== "object") {
128
+ return "-";
129
+ }
130
+
131
+ return document.name || document.filename || String(document.id || "-");
132
+ }
133
+
134
+ function readCalendarEntryTimestamp(entry, prefix) {
135
+ return (
136
+ entry?.[`${prefix}_at`] ||
137
+ [entry?.[`${prefix}_date`], entry?.[`${prefix}_time`]].filter(Boolean).join(" ") ||
138
+ entry?.[`${prefix}_date`] ||
139
+ "-"
140
+ );
141
+ }
142
+
143
+ function readReminderNotificationMethod(reminder) {
144
+ return (
145
+ reminder?.notification_method?.type ||
146
+ reminder?.notification_method?.email_address ||
147
+ "-"
148
+ );
149
+ }
150
+
151
+ function readMatterOrContactLabel(value) {
152
+ const matterLabel = readMatterLabel(value?.matter);
153
+ if (matterLabel !== "-") {
154
+ return matterLabel;
155
+ }
156
+
157
+ return readContactName(value?.contact);
158
+ }
159
+
160
+ function readOutstandingMatterCount(balance) {
161
+ const matterIds = Array.isArray(balance?.associated_matter_ids)
162
+ ? balance.associated_matter_ids
163
+ : [];
164
+ return matterIds.length > 0 ? String(matterIds.length) : "-";
165
+ }
166
+
167
+ function readMatterDocketJurisdiction(docket) {
168
+ return (
169
+ docket?.jurisdiction?.description ||
170
+ docket?.jurisdiction?.system_id ||
171
+ String(docket?.jurisdiction?.id || "-")
172
+ );
173
+ }
174
+
175
+ function readMatterDocketTrigger(docket) {
176
+ return (
177
+ docket?.trigger?.description ||
178
+ docket?.trigger?.system_id ||
179
+ String(docket?.trigger?.id || "-")
180
+ );
181
+ }
182
+
183
+ function readMatterDocketServiceType(docket) {
184
+ return (
185
+ docket?.service_type?.description ||
186
+ docket?.service_type?.system_id ||
187
+ String(docket?.service_type?.id || "-")
188
+ );
189
+ }
190
+
191
+ function readMyEventField(record, fieldName) {
192
+ return record?.event?.[fieldName] || "-";
193
+ }
194
+
195
+ function readMyEventTitle(record) {
196
+ return record?.event?.title || record?.event?.message || record?.event?.description || "-";
197
+ }
198
+
199
+ const VALID_BILL_STATUSES = new Set(["all", "overdue"]);
200
+
201
+ function normalizeBillStatusFilters(options = {}) {
202
+ const state =
203
+ typeof options.state === "string"
204
+ ? options.state.trim() || undefined
205
+ : options.state || undefined;
206
+
207
+ if (options.status === undefined || options.status === null || options.status === "") {
208
+ return { state, status: undefined };
209
+ }
210
+
211
+ if (typeof options.status !== "string") {
212
+ throw new Error(
213
+ "Invalid value for --status on bills/invoices. Use `all`, `overdue`, or `unpaid`."
214
+ );
215
+ }
216
+
217
+ const status = options.status.trim().toLowerCase();
218
+
219
+ if (!status) {
220
+ throw new Error(
221
+ "Invalid value for --status on bills/invoices. Use `all`, `overdue`, or `unpaid`."
222
+ );
223
+ }
224
+
225
+ if (status === "unpaid") {
226
+ if (state && state !== "awaiting_payment") {
227
+ throw new Error(
228
+ "`--status unpaid` conflicts with `--state`. Use `--state awaiting_payment` or remove one of the filters."
229
+ );
230
+ }
231
+
232
+ return {
233
+ state: state || "awaiting_payment",
234
+ status: undefined,
235
+ };
236
+ }
237
+
238
+ if (!VALID_BILL_STATUSES.has(status)) {
239
+ throw new Error(
240
+ "Invalid value for --status on bills/invoices. Use `all`, `overdue`, or `unpaid`."
241
+ );
242
+ }
243
+
244
+ return { state, status };
245
+ }
246
+
247
+ function applyBillStatusFilters(query, options) {
248
+ const filters = normalizeBillStatusFilters(options);
249
+ return {
250
+ ...query,
251
+ state: filters.state,
252
+ status: filters.status,
253
+ };
254
+ }
18
255
 
19
256
  const RESOURCE_ORDER = [
20
257
  "activities",
258
+ "calendar-entries",
259
+ "reminders",
21
260
  "tasks",
22
261
  "contacts",
262
+ "communications",
263
+ "conversations",
264
+ "conversation-messages",
265
+ "notes",
266
+ "custom-fields",
23
267
  "time-entries",
24
268
  "billable-clients",
25
269
  "billable-matters",
26
270
  "bills",
27
271
  "invoices",
272
+ "outstanding-client-balances",
28
273
  "matters",
274
+ "matter-dockets",
29
275
  "users",
30
276
  "practice-areas",
277
+ "my-events",
31
278
  ];
32
279
 
33
280
  const RESOURCE_METADATA = {
@@ -51,7 +298,7 @@ const RESOURCE_METADATA = {
51
298
  list: {
52
299
  activityDescriptionId: { kind: "string", option: "activity-description-id" },
53
300
  all: { kind: "flag", option: "all" },
54
- clientId: { kind: "string", option: "client-id" },
301
+ clientId: { kind: "string", option: "client-id", query: false },
55
302
  createdSince: { kind: "string", option: "created-since" },
56
303
  endDate: { kind: "iso-date", option: "end-date" },
57
304
  fields: { kind: "string", option: "fields" },
@@ -70,11 +317,84 @@ const RESOURCE_METADATA = {
70
317
  userId: { kind: "string", option: "user-id" },
71
318
  },
72
319
  },
320
+ listQuery: {
321
+ limitMax: 200,
322
+ },
73
323
  redaction: {
74
324
  resourceType: "activity",
75
325
  warningLevel: "limited",
76
326
  },
77
327
  riskLevel: "high",
328
+ display: {
329
+ get: {
330
+ fields: [
331
+ { label: "ID", value: (activity) => activity.id },
332
+ { label: "Type", value: (activity) => activity.type },
333
+ { label: "Date", value: (activity) => activity.date },
334
+ { label: "Hours", value: readHours },
335
+ { label: "Price", value: (activity) => formatMoney(activity.price) },
336
+ { label: "Total", value: (activity) => formatMoney(activity.total) },
337
+ { label: "Billed", value: (activity) => formatBoolean(activity.billed) },
338
+ { label: "On Bill", value: (activity) => formatBoolean(activity.on_bill) },
339
+ { label: "Non-Billable", value: (activity) => formatBoolean(activity.non_billable) },
340
+ { label: "No Charge", value: (activity) => formatBoolean(activity.no_charge) },
341
+ { label: "Flat Rate", value: (activity) => formatBoolean(activity.flat_rate) },
342
+ {
343
+ label: "Contingency Fee",
344
+ value: (activity) => formatBoolean(activity.contingency_fee),
345
+ },
346
+ { label: "User", value: (activity) => readUserName(activity.user) },
347
+ { label: "Matter", value: (activity) => readMatterLabel(activity.matter) },
348
+ {
349
+ label: "Activity Description",
350
+ value: (activity) => activity.activity_description?.name,
351
+ },
352
+ { label: "Bill", value: (activity) => activity.bill?.number },
353
+ { label: "Bill State", value: (activity) => activity.bill?.state },
354
+ { label: "Reference", value: (activity) => activity.reference },
355
+ { label: "Note", value: (activity) => activity.note },
356
+ { label: "Created", value: (activity) => activity.created_at },
357
+ { label: "Updated", value: (activity) => activity.updated_at },
358
+ ],
359
+ },
360
+ list: {
361
+ columns: [
362
+ { header: "ID", key: "id", width: 8 },
363
+ { header: "TYPE", key: "type", width: 11 },
364
+ { header: "DATE", key: "date", width: 10 },
365
+ { header: "HOURS", key: "hours", width: 5 },
366
+ { header: "TOTAL", key: "total", width: 10 },
367
+ { header: "BILLED", key: "billed", width: 6 },
368
+ { header: "MATTER", key: "matter", width: 20 },
369
+ { header: "NOTE", key: "note", width: 30, pad: false },
370
+ ],
371
+ emptyMessage: "No activities found for the selected filters.",
372
+ formatRow: (activity) => ({
373
+ billed: formatBoolean(activity.billed),
374
+ date: String(activity.date || "-"),
375
+ hours: readHours(activity),
376
+ id: String(activity.id || "-"),
377
+ matter: readMatterLabel(activity.matter),
378
+ note: String(activity.note || "-"),
379
+ total: formatMoney(activity.total),
380
+ type: String(activity.type || "-"),
381
+ }),
382
+ moreResults: (options) => {
383
+ if (options.pageTokenSupported === false) {
384
+ return [
385
+ "More results are available.",
386
+ "Run again with `--all` to fetch every matching activity.",
387
+ ];
388
+ }
389
+
390
+ return [
391
+ "More results are available.",
392
+ "Run again with `--all` or pass `--page-token` from `--json` output.",
393
+ ];
394
+ },
395
+ noun: "activities",
396
+ },
397
+ },
78
398
  summaryLabels: {
79
399
  plural: "activities",
80
400
  singular: "activity",
@@ -109,11 +429,39 @@ const RESOURCE_METADATA = {
109
429
  startDate: { kind: "iso-date", option: "start-date" },
110
430
  },
111
431
  },
432
+ listQuery: {
433
+ limitMax: 25,
434
+ },
112
435
  redaction: {
113
436
  resourceType: "billable-client",
114
437
  warningLevel: "standard",
115
438
  },
116
439
  riskLevel: "high",
440
+ display: {
441
+ list: {
442
+ columns: [
443
+ { header: "ID", key: "id", width: 8 },
444
+ { header: "NAME", key: "name", width: 28 },
445
+ { header: "HOURS", key: "hours", width: 5 },
446
+ { header: "AMOUNT", key: "amount", width: 10 },
447
+ { header: "TRUST", key: "trust", width: 10 },
448
+ { header: "MATTERS", key: "matters", width: 7, pad: false },
449
+ ],
450
+ emptyMessage: "No billable clients found for the selected filters.",
451
+ formatRow: (record) => ({
452
+ amount: formatMoney(record.unbilled_amount),
453
+ hours:
454
+ record.unbilled_hours === undefined || record.unbilled_hours === null
455
+ ? "-"
456
+ : Number(record.unbilled_hours).toFixed(2),
457
+ id: String(record.id || "-"),
458
+ matters: String(record.billable_matters_count ?? "-"),
459
+ name: String(record.name || "-"),
460
+ trust: formatMoney(record.amount_in_trust),
461
+ }),
462
+ noun: "billable clients",
463
+ },
464
+ },
117
465
  summaryLabels: {
118
466
  plural: "billable clients",
119
467
  singular: "billable client",
@@ -148,11 +496,39 @@ const RESOURCE_METADATA = {
148
496
  startDate: { kind: "iso-date", option: "start-date" },
149
497
  },
150
498
  },
499
+ listQuery: {
500
+ limitMax: 1000,
501
+ },
151
502
  redaction: {
152
503
  resourceType: "billable-matter",
153
504
  warningLevel: "limited",
154
505
  },
155
506
  riskLevel: "high",
507
+ display: {
508
+ list: {
509
+ columns: [
510
+ { header: "ID", key: "id", width: 8 },
511
+ { header: "MATTER", key: "matter", width: 21 },
512
+ { header: "CLIENT", key: "client", width: 20 },
513
+ { header: "HOURS", key: "hours", width: 5 },
514
+ { header: "AMOUNT", key: "amount", width: 10 },
515
+ { header: "TRUST", key: "trust", width: 10, pad: false },
516
+ ],
517
+ emptyMessage: "No billable matters found for the selected filters.",
518
+ formatRow: (record) => ({
519
+ amount: formatMoney(record.unbilled_amount),
520
+ client: readContactName(record.client),
521
+ hours:
522
+ record.unbilled_hours === undefined || record.unbilled_hours === null
523
+ ? "-"
524
+ : Number(record.unbilled_hours).toFixed(2),
525
+ id: String(record.id || "-"),
526
+ matter: String(record.display_number || "-"),
527
+ trust: formatMoney(record.amount_in_trust),
528
+ }),
529
+ noun: "billable matters",
530
+ },
531
+ },
156
532
  summaryLabels: {
157
533
  plural: "billable matters",
158
534
  singular: "billable matter",
@@ -200,11 +576,58 @@ const RESOURCE_METADATA = {
200
576
  updatedSince: { kind: "string", option: "updated-since" },
201
577
  },
202
578
  },
579
+ listQuery: {
580
+ limitMax: 200,
581
+ transform: applyBillStatusFilters,
582
+ },
203
583
  redaction: {
204
584
  resourceType: "bill",
205
585
  warningLevel: "limited",
206
586
  },
207
587
  riskLevel: "high",
588
+ display: {
589
+ get: {
590
+ fields: [
591
+ { label: "ID", value: (bill) => bill.id },
592
+ { label: "Number", value: (bill) => bill.number },
593
+ { label: "State", value: (bill) => bill.state },
594
+ { label: "Type", value: (bill) => bill.type || bill.kind },
595
+ { label: "Client", value: (bill) => readContactName(bill.client) },
596
+ { label: "Matter", value: readFirstMatterLabel },
597
+ { label: "Issued", value: (bill) => bill.issued_at },
598
+ { label: "Due", value: (bill) => bill.due_at },
599
+ { label: "Paid", value: (bill) => formatMoney(bill.paid) },
600
+ { label: "Pending", value: (bill) => formatMoney(bill.pending) },
601
+ { label: "Due Amount", value: (bill) => formatMoney(bill.due) },
602
+ { label: "Total", value: (bill) => formatMoney(bill.total) },
603
+ { label: "Balance", value: (bill) => formatMoney(bill.balance) },
604
+ { label: "Subject", value: (bill) => bill.subject },
605
+ { label: "Memo", value: (bill) => bill.memo },
606
+ { label: "Created", value: (bill) => bill.created_at },
607
+ { label: "Updated", value: (bill) => bill.updated_at },
608
+ ],
609
+ },
610
+ list: {
611
+ columns: [
612
+ { header: "ID", key: "id", width: 8 },
613
+ { header: "NUMBER", key: "number", width: 12 },
614
+ { header: "STATE", key: "state", width: 12 },
615
+ { header: "CLIENT", key: "client", width: 20 },
616
+ { header: "DUE", key: "dueAt", width: 12 },
617
+ { header: "BALANCE", key: "balance", width: 10, pad: false },
618
+ ],
619
+ emptyMessage: "No bills found for the selected filters.",
620
+ formatRow: (bill) => ({
621
+ id: String(bill.id || "-"),
622
+ number: String(bill.number || "-"),
623
+ state: String(bill.state || "-"),
624
+ client: readContactName(bill.client),
625
+ dueAt: String(bill.due_at || "-"),
626
+ balance: formatMoney(bill.balance),
627
+ }),
628
+ noun: "bills",
629
+ },
630
+ },
208
631
  summaryLabels: {
209
632
  plural: "bills",
210
633
  singular: "bill",
@@ -247,11 +670,53 @@ const RESOURCE_METADATA = {
247
670
  updatedSince: { kind: "string", option: "updated-since" },
248
671
  },
249
672
  },
673
+ listQuery: {
674
+ limitMax: 200,
675
+ },
250
676
  redaction: {
251
677
  resourceType: "contact",
252
678
  warningLevel: "standard",
253
679
  },
254
680
  riskLevel: "high",
681
+ display: {
682
+ get: {
683
+ fields: [
684
+ { label: "ID", value: (contact) => contact.id },
685
+ { label: "Name", value: readContactName },
686
+ { label: "Type", value: (contact) => contact.type },
687
+ { label: "Client", value: (contact) => formatBoolean(contact.is_client) },
688
+ { label: "Primary Email", value: (contact) => contact.primary_email_address },
689
+ { label: "Secondary Email", value: (contact) => contact.secondary_email_address },
690
+ { label: "Primary Phone", value: (contact) => contact.primary_phone_number },
691
+ { label: "Secondary Phone", value: (contact) => contact.secondary_phone_number },
692
+ { label: "Clio Connect Email", value: (contact) => contact.clio_connect_email },
693
+ { label: "Title", value: (contact) => contact.title },
694
+ { label: "Prefix", value: (contact) => contact.prefix },
695
+ { label: "Created", value: (contact) => contact.created_at },
696
+ { label: "Updated", value: (contact) => contact.updated_at },
697
+ ],
698
+ },
699
+ list: {
700
+ columns: [
701
+ { header: "ID", key: "id", width: 8 },
702
+ { header: "NAME", key: "name", width: 28 },
703
+ { header: "TYPE", key: "type", width: 12 },
704
+ { header: "CLIENT", key: "client", width: 6 },
705
+ { header: "EMAIL", key: "email", width: 28 },
706
+ { header: "PHONE", key: "phone", width: 18, pad: false },
707
+ ],
708
+ emptyMessage: "No contacts found for the selected filters.",
709
+ formatRow: (contact) => ({
710
+ id: String(contact.id || "-"),
711
+ name: readContactName(contact),
712
+ type: String(contact.type || "-"),
713
+ client: formatBoolean(contact.is_client),
714
+ email: String(contact.primary_email_address || "-"),
715
+ phone: String(contact.primary_phone_number || "-"),
716
+ }),
717
+ noun: "contacts",
718
+ },
719
+ },
255
720
  summaryLabels: {
256
721
  plural: "contacts",
257
722
  singular: "contact",
@@ -299,6 +764,10 @@ const RESOURCE_METADATA = {
299
764
  updatedSince: { kind: "string", option: "updated-since" },
300
765
  },
301
766
  },
767
+ listQuery: {
768
+ limitMax: 200,
769
+ transform: applyBillStatusFilters,
770
+ },
302
771
  redaction: {
303
772
  resourceType: "bill",
304
773
  warningLevel: "limited",
@@ -347,11 +816,62 @@ const RESOURCE_METADATA = {
347
816
  updatedSince: { kind: "string", option: "updated-since" },
348
817
  },
349
818
  },
819
+ listQuery: {
820
+ limitMax: 200,
821
+ },
350
822
  redaction: {
351
823
  resourceType: "matter",
352
824
  warningLevel: "limited",
353
825
  },
354
826
  riskLevel: "high",
827
+ display: {
828
+ get: {
829
+ fields: [
830
+ { label: "ID", value: (matter) => matter.id },
831
+ { label: "Matter", value: (matter) => matter.display_number || matter.number },
832
+ { label: "Description", value: (matter) => matter.description },
833
+ { label: "Status", value: (matter) => readStatus(matter.status) },
834
+ { label: "Client", value: readMatterClientName },
835
+ { label: "Practice Area", value: (matter) => matter.practice_area?.name },
836
+ {
837
+ label: "Responsible Attorney",
838
+ value: (matter) => readUserName(matter.responsible_attorney),
839
+ },
840
+ {
841
+ label: "Responsible Staff",
842
+ value: (matter) => readUserName(matter.responsible_staff),
843
+ },
844
+ {
845
+ label: "Originating Attorney",
846
+ value: (matter) => readUserName(matter.originating_attorney),
847
+ },
848
+ { label: "Billable", value: (matter) => formatBoolean(matter.billable) },
849
+ { label: "Open Date", value: (matter) => matter.open_date },
850
+ { label: "Pending Date", value: (matter) => matter.pending_date },
851
+ { label: "Close Date", value: (matter) => matter.close_date },
852
+ { label: "Created", value: (matter) => matter.created_at },
853
+ { label: "Updated", value: (matter) => matter.updated_at },
854
+ ],
855
+ },
856
+ list: {
857
+ columns: [
858
+ { header: "ID", key: "id", width: 8 },
859
+ { header: "MATTER", key: "displayNumber", width: 21 },
860
+ { header: "STATUS", key: "status", width: 9 },
861
+ { header: "CLIENT", key: "client", width: 20 },
862
+ { header: "DESCRIPTION", key: "description", width: 30, pad: false },
863
+ ],
864
+ emptyMessage: "No matters found for the selected filters.",
865
+ formatRow: (matter) => ({
866
+ id: String(matter.id || "-"),
867
+ displayNumber: String(matter.display_number || matter.number || "-"),
868
+ status: String(readStatus(matter.status)),
869
+ client: String(readMatterClientName(matter)),
870
+ description: String(matter.description || "-"),
871
+ }),
872
+ noun: "matters",
873
+ },
874
+ },
355
875
  summaryLabels: {
356
876
  plural: "matters",
357
877
  singular: "matter",
@@ -384,18 +904,49 @@ const RESOURCE_METADATA = {
384
904
  createdSince: { kind: "string", option: "created-since" },
385
905
  fields: { kind: "string", option: "fields" },
386
906
  limit: { kind: "string", option: "limit" },
387
- matterId: { kind: "string", option: "matter-id" },
907
+ matterId: { kind: "string", option: "matter-id", query: false },
388
908
  name: { kind: "string", option: "name" },
389
909
  order: { kind: "string", option: "order" },
390
910
  pageToken: { kind: "string", option: "page-token" },
391
911
  updatedSince: { kind: "string", option: "updated-since" },
392
912
  },
393
913
  },
914
+ listQuery: {
915
+ limitMax: 200,
916
+ },
394
917
  redaction: {
395
918
  resourceType: "practice-area",
396
919
  warningLevel: "none",
397
920
  },
398
921
  riskLevel: "low",
922
+ display: {
923
+ get: {
924
+ fields: [
925
+ { label: "ID", value: (practiceArea) => practiceArea.id },
926
+ { label: "Code", value: (practiceArea) => practiceArea.code },
927
+ { label: "Name", value: (practiceArea) => practiceArea.name },
928
+ { label: "Category", value: (practiceArea) => practiceArea.category },
929
+ { label: "Created", value: (practiceArea) => practiceArea.created_at },
930
+ { label: "Updated", value: (practiceArea) => practiceArea.updated_at },
931
+ ],
932
+ },
933
+ list: {
934
+ columns: [
935
+ { header: "ID", key: "id", width: 8 },
936
+ { header: "CODE", key: "code", width: 12 },
937
+ { header: "NAME", key: "name", width: 28 },
938
+ { header: "CATEGORY", key: "category", width: 30, pad: false },
939
+ ],
940
+ emptyMessage: "No practice areas found for the selected filters.",
941
+ formatRow: (practiceArea) => ({
942
+ category: String(practiceArea.category || "-"),
943
+ code: String(practiceArea.code || "-"),
944
+ id: String(practiceArea.id || "-"),
945
+ name: String(practiceArea.name || "-"),
946
+ }),
947
+ noun: "practice areas",
948
+ },
949
+ },
399
950
  summaryLabels: {
400
951
  plural: "practice areas",
401
952
  singular: "practice area",
@@ -442,11 +993,53 @@ const RESOURCE_METADATA = {
442
993
  updatedSince: { kind: "string", option: "updated-since" },
443
994
  },
444
995
  },
996
+ listQuery: {
997
+ limitMax: 200,
998
+ },
445
999
  redaction: {
446
1000
  resourceType: "task",
447
1001
  warningLevel: "limited",
448
1002
  },
449
1003
  riskLevel: "high",
1004
+ display: {
1005
+ get: {
1006
+ fields: [
1007
+ { label: "ID", value: (task) => task.id },
1008
+ { label: "Name", value: (task) => task.name },
1009
+ { label: "Description", value: (task) => task.description },
1010
+ { label: "Status", value: (task) => readStatus(task.status) },
1011
+ { label: "Priority", value: (task) => task.priority },
1012
+ { label: "Due", value: (task) => task.due_at },
1013
+ { label: "Complete", value: (task) => formatBoolean(readTaskComplete(task)) },
1014
+ { label: "Matter", value: (task) => readMatterLabel(task.matter) },
1015
+ { label: "Assignee", value: (task) => readUserName(task.assignee) },
1016
+ { label: "Assigner", value: (task) => readUserName(task.assigner) },
1017
+ { label: "Task Type", value: (task) => task.task_type?.name },
1018
+ { label: "Created", value: (task) => task.created_at },
1019
+ { label: "Updated", value: (task) => task.updated_at },
1020
+ ],
1021
+ },
1022
+ list: {
1023
+ columns: [
1024
+ { header: "ID", key: "id", width: 8 },
1025
+ { header: "STATUS", key: "status", width: 12 },
1026
+ { header: "DUE", key: "dueAt", width: 12 },
1027
+ { header: "PRIORITY", key: "priority", width: 8 },
1028
+ { header: "MATTER", key: "matter", width: 20 },
1029
+ { header: "TASK", key: "task", width: 30, pad: false },
1030
+ ],
1031
+ emptyMessage: "No tasks found for the selected filters.",
1032
+ formatRow: (task) => ({
1033
+ id: String(task.id || "-"),
1034
+ status: String(readStatus(task.status)),
1035
+ dueAt: String(task.due_at || "-"),
1036
+ priority: String(task.priority || "-"),
1037
+ matter: readMatterLabel(task.matter),
1038
+ task: String(task.name || "-"),
1039
+ }),
1040
+ noun: "tasks",
1041
+ },
1042
+ },
450
1043
  summaryLabels: {
451
1044
  plural: "tasks",
452
1045
  singular: "task",
@@ -513,6 +1106,975 @@ const RESOURCE_METADATA = {
513
1106
  list: true,
514
1107
  },
515
1108
  },
1109
+ "calendar-entries": {
1110
+ aliases: ["calendar-entry"],
1111
+ apiPath: "calendar_entries",
1112
+ defaultFields: {
1113
+ get: CALENDAR_ENTRY_FIELDS,
1114
+ list: CALENDAR_ENTRY_FIELDS,
1115
+ },
1116
+ handlerKey: "calendar-entries",
1117
+ help: {
1118
+ get: "Fetch a single calendar entry by id",
1119
+ list: "List calendar entries with filters and pagination",
1120
+ },
1121
+ optionSchema: {
1122
+ get: {
1123
+ fields: { kind: "string", option: "fields" },
1124
+ id: { positional: 0 },
1125
+ },
1126
+ list: {
1127
+ all: { kind: "flag", option: "all" },
1128
+ calendarId: { kind: "string", option: "calendar-id" },
1129
+ createdSince: { kind: "string", option: "created-since" },
1130
+ expanded: { kind: "boolean", option: "expanded" },
1131
+ externalPropertyName: { kind: "string", option: "external-property-name" },
1132
+ externalPropertyValue: { kind: "string", option: "external-property-value" },
1133
+ fields: { kind: "string", option: "fields" },
1134
+ from: { kind: "iso-datetime", option: "from" },
1135
+ hasCourtRule: { kind: "boolean", option: "has-court-rule", query: "has_court_rule" },
1136
+ ids: { kind: "string-array", option: "ids", query: "ids[]" },
1137
+ isAllDay: { kind: "boolean", option: "is-all-day", query: "is_all_day" },
1138
+ limit: { kind: "string", option: "limit" },
1139
+ matterId: { kind: "string", option: "matter-id" },
1140
+ ownerEntriesAcrossAllUsers: {
1141
+ kind: "boolean",
1142
+ option: "owner-entries-across-all-users",
1143
+ query: "owner_entries_across_all_users",
1144
+ },
1145
+ pageToken: { kind: "string", option: "page-token" },
1146
+ query: { kind: "string", option: "query" },
1147
+ source: { kind: "string", option: "source" },
1148
+ to: { kind: "iso-datetime", option: "to" },
1149
+ updatedSince: { kind: "string", option: "updated-since" },
1150
+ visible: { kind: "boolean", option: "visible" },
1151
+ },
1152
+ },
1153
+ listQuery: {
1154
+ limitMax: 200,
1155
+ },
1156
+ redaction: {
1157
+ resourceType: "calendar-entry",
1158
+ warningLevel: "limited",
1159
+ },
1160
+ riskLevel: "high",
1161
+ display: {
1162
+ get: {
1163
+ fields: [
1164
+ { label: "ID", value: (entry) => entry.id },
1165
+ { label: "Summary", value: (entry) => entry.summary },
1166
+ { label: "Description", value: (entry) => entry.description },
1167
+ { label: "Location", value: (entry) => entry.location },
1168
+ { label: "Start", value: (entry) => readCalendarEntryTimestamp(entry, "start") },
1169
+ { label: "End", value: (entry) => readCalendarEntryTimestamp(entry, "end") },
1170
+ { label: "All Day", value: (entry) => formatBoolean(entry.all_day) },
1171
+ { label: "Court Rule", value: (entry) => formatBoolean(entry.court_rule) },
1172
+ { label: "Matter", value: (entry) => readMatterLabel(entry.matter) },
1173
+ { label: "Matter Docket", value: (entry) => entry.matter_docket?.name },
1174
+ { label: "Calendar Owner", value: (entry) => readCalendarName(entry.calendar_owner) },
1175
+ { label: "Calendars", value: (entry) => summarizeValues(entry.calendars, readCalendarName) },
1176
+ {
1177
+ label: "Attendees",
1178
+ value: (entry) => summarizeValues(entry.attendees, readParticipantName),
1179
+ },
1180
+ {
1181
+ label: "Event Type",
1182
+ value: (entry) => entry.calendar_entry_event_type?.name,
1183
+ },
1184
+ {
1185
+ label: "Reminders",
1186
+ value: (entry) =>
1187
+ summarizeValues(
1188
+ entry.reminders,
1189
+ (reminder) => `${reminder.duration ?? "-"}:${reminder.state || "-"}`
1190
+ ),
1191
+ },
1192
+ { label: "Created", value: (entry) => entry.created_at },
1193
+ { label: "Updated", value: (entry) => entry.updated_at },
1194
+ ],
1195
+ },
1196
+ list: {
1197
+ columns: [
1198
+ { header: "ID", key: "id", width: 8 },
1199
+ { header: "START", key: "start", width: 20 },
1200
+ { header: "END", key: "end", width: 20 },
1201
+ { header: "ALL DAY", key: "allDay", width: 7 },
1202
+ { header: "MATTER", key: "matter", width: 20 },
1203
+ { header: "SUMMARY", key: "summary", width: 30, pad: false },
1204
+ ],
1205
+ emptyMessage: "No calendar entries found for the selected filters.",
1206
+ formatRow: (entry) => ({
1207
+ allDay: formatBoolean(entry.all_day),
1208
+ end: String(readCalendarEntryTimestamp(entry, "end")),
1209
+ id: String(entry.id || "-"),
1210
+ matter: readMatterLabel(entry.matter),
1211
+ start: String(readCalendarEntryTimestamp(entry, "start")),
1212
+ summary: String(entry.summary || entry.description || "-"),
1213
+ }),
1214
+ noun: "calendar entries",
1215
+ },
1216
+ },
1217
+ summaryLabels: {
1218
+ plural: "calendar entries",
1219
+ singular: "calendar entry",
1220
+ },
1221
+ supports: {
1222
+ get: true,
1223
+ list: true,
1224
+ },
1225
+ },
1226
+ reminders: {
1227
+ aliases: ["reminder"],
1228
+ apiPath: "reminders",
1229
+ defaultFields: {
1230
+ get: REMINDER_FIELDS,
1231
+ list: REMINDER_FIELDS,
1232
+ },
1233
+ handlerKey: "reminders",
1234
+ help: {
1235
+ get: "Fetch a single reminder by id",
1236
+ list: "List reminders with filters and pagination",
1237
+ },
1238
+ optionSchema: {
1239
+ get: {
1240
+ fields: { kind: "string", option: "fields" },
1241
+ id: { positional: 0 },
1242
+ },
1243
+ list: {
1244
+ all: { kind: "flag", option: "all" },
1245
+ createdSince: { kind: "string", option: "created-since" },
1246
+ fields: { kind: "string", option: "fields" },
1247
+ ids: { kind: "string-array", option: "ids", query: "ids[]" },
1248
+ limit: { kind: "string", option: "limit" },
1249
+ notificationMethodId: {
1250
+ kind: "string",
1251
+ option: "notification-method-id",
1252
+ query: "notification_method_id",
1253
+ },
1254
+ order: { kind: "string", option: "order" },
1255
+ pageToken: { kind: "string", option: "page-token" },
1256
+ state: { kind: "string", option: "state" },
1257
+ subjectId: { kind: "string", option: "subject-id", query: "subject_id" },
1258
+ subjectType: { kind: "string", option: "subject-type", query: "subject_type" },
1259
+ updatedSince: { kind: "string", option: "updated-since" },
1260
+ userId: { kind: "string", option: "user-id", query: "user_id" },
1261
+ },
1262
+ },
1263
+ listQuery: {
1264
+ limitMax: 200,
1265
+ },
1266
+ redaction: {
1267
+ resourceType: "reminder",
1268
+ warningLevel: "limited",
1269
+ },
1270
+ riskLevel: "high",
1271
+ display: {
1272
+ get: {
1273
+ fields: [
1274
+ { label: "ID", value: (reminder) => reminder.id },
1275
+ { label: "State", value: (reminder) => reminder.state },
1276
+ { label: "Duration", value: (reminder) => reminder.duration },
1277
+ { label: "Next Delivery", value: (reminder) => reminder.next_delivery_at },
1278
+ {
1279
+ label: "Notification Method",
1280
+ value: (reminder) => readReminderNotificationMethod(reminder),
1281
+ },
1282
+ {
1283
+ label: "Subject",
1284
+ value: (reminder) => readPolymorphicObjectLabel(reminder.subject),
1285
+ },
1286
+ { label: "Created", value: (reminder) => reminder.created_at },
1287
+ { label: "Updated", value: (reminder) => reminder.updated_at },
1288
+ ],
1289
+ },
1290
+ list: {
1291
+ columns: [
1292
+ { header: "ID", key: "id", width: 8 },
1293
+ { header: "STATE", key: "state", width: 12 },
1294
+ { header: "NEXT DELIVERY", key: "nextDeliveryAt", width: 20 },
1295
+ { header: "METHOD", key: "method", width: 12 },
1296
+ { header: "SUBJECT", key: "subject", width: 28, pad: false },
1297
+ ],
1298
+ emptyMessage: "No reminders found for the selected filters.",
1299
+ formatRow: (reminder) => ({
1300
+ id: String(reminder.id || "-"),
1301
+ method: String(readReminderNotificationMethod(reminder)),
1302
+ nextDeliveryAt: String(reminder.next_delivery_at || "-"),
1303
+ state: String(reminder.state || "-"),
1304
+ subject: String(readPolymorphicObjectLabel(reminder.subject)),
1305
+ }),
1306
+ noun: "reminders",
1307
+ },
1308
+ },
1309
+ summaryLabels: {
1310
+ plural: "reminders",
1311
+ singular: "reminder",
1312
+ },
1313
+ supports: {
1314
+ get: true,
1315
+ list: true,
1316
+ },
1317
+ },
1318
+ communications: {
1319
+ aliases: ["communication"],
1320
+ apiPath: "communications",
1321
+ defaultFields: {
1322
+ get: COMMUNICATION_FIELDS,
1323
+ list: COMMUNICATION_FIELDS,
1324
+ },
1325
+ handlerKey: "communications",
1326
+ help: {
1327
+ get: "Fetch a single communication by id",
1328
+ list: "List communications with filters and pagination",
1329
+ },
1330
+ optionSchema: {
1331
+ get: {
1332
+ fields: { kind: "string", option: "fields" },
1333
+ id: { positional: 0 },
1334
+ },
1335
+ list: {
1336
+ all: { kind: "flag", option: "all" },
1337
+ contactId: { kind: "string", option: "contact-id" },
1338
+ createdSince: { kind: "string", option: "created-since" },
1339
+ date: { kind: "iso-date", option: "date" },
1340
+ externalPropertyName: { kind: "string", option: "external-property-name" },
1341
+ externalPropertyValue: { kind: "string", option: "external-property-value" },
1342
+ fields: { kind: "string", option: "fields" },
1343
+ havingTimeEntries: {
1344
+ kind: "boolean",
1345
+ option: "having-time-entries",
1346
+ query: "having_time_entries",
1347
+ },
1348
+ ids: { kind: "string-array", option: "ids", query: "ids[]" },
1349
+ limit: { kind: "string", option: "limit" },
1350
+ matterId: { kind: "string", option: "matter-id" },
1351
+ order: { kind: "string", option: "order" },
1352
+ pageToken: { kind: "string", option: "page-token" },
1353
+ query: { kind: "string", option: "query" },
1354
+ receivedAt: { kind: "iso-datetime", option: "received-at", query: "received_at" },
1355
+ receivedBefore: {
1356
+ kind: "iso-datetime",
1357
+ option: "received-before",
1358
+ query: "received_before",
1359
+ },
1360
+ receivedSince: {
1361
+ kind: "iso-datetime",
1362
+ option: "received-since",
1363
+ query: "received_since",
1364
+ },
1365
+ type: { kind: "string", option: "type" },
1366
+ updatedSince: { kind: "string", option: "updated-since" },
1367
+ userId: { kind: "string", option: "user-id" },
1368
+ },
1369
+ },
1370
+ listQuery: {
1371
+ limitMax: 200,
1372
+ },
1373
+ redaction: {
1374
+ resourceType: "communication",
1375
+ warningLevel: "limited",
1376
+ },
1377
+ riskLevel: "high",
1378
+ display: {
1379
+ get: {
1380
+ fields: [
1381
+ { label: "ID", value: (communication) => communication.id },
1382
+ { label: "Type", value: (communication) => communication.type },
1383
+ { label: "Subject", value: (communication) => communication.subject },
1384
+ { label: "Body", value: (communication) => communication.body },
1385
+ { label: "Date", value: (communication) => communication.date },
1386
+ { label: "Received", value: (communication) => communication.received_at },
1387
+ { label: "Matter", value: (communication) => readMatterLabel(communication.matter) },
1388
+ { label: "User", value: (communication) => readUserName(communication.user) },
1389
+ {
1390
+ label: "Senders",
1391
+ value: (communication) =>
1392
+ summarizeValues(communication.senders, readParticipantName),
1393
+ },
1394
+ {
1395
+ label: "Receivers",
1396
+ value: (communication) =>
1397
+ summarizeValues(communication.receivers, readParticipantName),
1398
+ },
1399
+ { label: "Created", value: (communication) => communication.created_at },
1400
+ { label: "Updated", value: (communication) => communication.updated_at },
1401
+ ],
1402
+ },
1403
+ list: {
1404
+ columns: [
1405
+ { header: "ID", key: "id", width: 8 },
1406
+ { header: "TYPE", key: "type", width: 12 },
1407
+ { header: "RECEIVED", key: "receivedAt", width: 20 },
1408
+ { header: "MATTER", key: "matter", width: 20 },
1409
+ { header: "SUBJECT", key: "subject", width: 30, pad: false },
1410
+ ],
1411
+ emptyMessage: "No communications found for the selected filters.",
1412
+ formatRow: (communication) => ({
1413
+ id: String(communication.id || "-"),
1414
+ matter: readMatterLabel(communication.matter),
1415
+ receivedAt: String(communication.received_at || communication.date || "-"),
1416
+ subject: String(communication.subject || "-"),
1417
+ type: String(communication.type || "-"),
1418
+ }),
1419
+ noun: "communications",
1420
+ },
1421
+ },
1422
+ summaryLabels: {
1423
+ plural: "communications",
1424
+ singular: "communication",
1425
+ },
1426
+ supports: {
1427
+ get: true,
1428
+ list: true,
1429
+ },
1430
+ },
1431
+ conversations: {
1432
+ aliases: ["conversation"],
1433
+ apiPath: "conversations",
1434
+ defaultFields: {
1435
+ get: CONVERSATION_FIELDS,
1436
+ list: CONVERSATION_FIELDS,
1437
+ },
1438
+ handlerKey: "conversations",
1439
+ help: {
1440
+ get: "Fetch a single conversation by id",
1441
+ list: "List conversations with filters and pagination",
1442
+ },
1443
+ optionSchema: {
1444
+ get: {
1445
+ fields: { kind: "string", option: "fields" },
1446
+ id: { positional: 0 },
1447
+ },
1448
+ list: {
1449
+ all: { kind: "flag", option: "all" },
1450
+ archived: { kind: "boolean", option: "archived" },
1451
+ contactId: { kind: "string", option: "contact-id" },
1452
+ createdSince: { kind: "string", option: "created-since" },
1453
+ date: { kind: "iso-date", option: "date" },
1454
+ fields: { kind: "string", option: "fields" },
1455
+ forUser: { kind: "boolean", option: "for-user", query: "for_user" },
1456
+ ids: { kind: "string-array", option: "ids", query: "ids[]" },
1457
+ limit: { kind: "string", option: "limit" },
1458
+ matterId: { kind: "string", option: "matter-id" },
1459
+ order: { kind: "string", option: "order" },
1460
+ pageToken: { kind: "string", option: "page-token" },
1461
+ readStatus: { kind: "boolean", option: "read-status", query: "read_status" },
1462
+ timeEntries: { kind: "boolean", option: "time-entries", query: "time_entries" },
1463
+ updatedSince: { kind: "string", option: "updated-since" },
1464
+ },
1465
+ },
1466
+ listQuery: {
1467
+ limitMax: 200,
1468
+ },
1469
+ redaction: {
1470
+ resourceType: "conversation",
1471
+ warningLevel: "limited",
1472
+ },
1473
+ riskLevel: "high",
1474
+ display: {
1475
+ get: {
1476
+ fields: [
1477
+ { label: "ID", value: (conversation) => conversation.id },
1478
+ { label: "Subject", value: (conversation) => conversation.subject },
1479
+ { label: "Read", value: (conversation) => formatBoolean(conversation.read) },
1480
+ { label: "Archived", value: (conversation) => formatBoolean(conversation.archived) },
1481
+ {
1482
+ label: "Current User Is Member",
1483
+ value: (conversation) => formatBoolean(conversation.current_user_is_member),
1484
+ },
1485
+ { label: "Read Only", value: (conversation) => formatBoolean(conversation.read_only) },
1486
+ { label: "Messages", value: (conversation) => conversation.message_count },
1487
+ { label: "Time Entries", value: (conversation) => conversation.time_entries_count },
1488
+ { label: "Matter", value: (conversation) => readMatterLabel(conversation.matter) },
1489
+ { label: "First Message", value: (conversation) => conversation.first_message?.body },
1490
+ { label: "Last Message", value: (conversation) => conversation.last_message?.body },
1491
+ { label: "Created", value: (conversation) => conversation.created_at },
1492
+ { label: "Updated", value: (conversation) => conversation.updated_at },
1493
+ ],
1494
+ },
1495
+ list: {
1496
+ columns: [
1497
+ { header: "ID", key: "id", width: 8 },
1498
+ { header: "READ", key: "read", width: 4 },
1499
+ { header: "ARCHIVED", key: "archived", width: 8 },
1500
+ { header: "MESSAGES", key: "messages", width: 8 },
1501
+ { header: "MATTER", key: "matter", width: 20 },
1502
+ { header: "SUBJECT", key: "subject", width: 30, pad: false },
1503
+ ],
1504
+ emptyMessage: "No conversations found for the selected filters.",
1505
+ formatRow: (conversation) => ({
1506
+ archived: formatBoolean(conversation.archived),
1507
+ id: String(conversation.id || "-"),
1508
+ matter: readMatterLabel(conversation.matter),
1509
+ messages: String(conversation.message_count ?? "-"),
1510
+ read: formatBoolean(conversation.read),
1511
+ subject: String(conversation.subject || "-"),
1512
+ }),
1513
+ noun: "conversations",
1514
+ },
1515
+ },
1516
+ summaryLabels: {
1517
+ plural: "conversations",
1518
+ singular: "conversation",
1519
+ },
1520
+ supports: {
1521
+ get: true,
1522
+ list: true,
1523
+ },
1524
+ },
1525
+ "conversation-messages": {
1526
+ aliases: ["conversation-message"],
1527
+ apiPath: "conversation_messages",
1528
+ defaultFields: {
1529
+ get: CONVERSATION_MESSAGE_FIELDS,
1530
+ list: CONVERSATION_MESSAGE_FIELDS,
1531
+ },
1532
+ handlerKey: "conversation-messages",
1533
+ help: {
1534
+ get: "Fetch a single conversation message by id",
1535
+ list: "List conversation messages for a conversation",
1536
+ },
1537
+ optionSchema: {
1538
+ get: {
1539
+ fields: { kind: "string", option: "fields" },
1540
+ id: { positional: 0 },
1541
+ },
1542
+ list: {
1543
+ all: { kind: "flag", option: "all" },
1544
+ conversationId: {
1545
+ kind: "string",
1546
+ option: "conversation-id",
1547
+ query: "conversation_id",
1548
+ },
1549
+ createdSince: { kind: "string", option: "created-since" },
1550
+ fields: { kind: "string", option: "fields" },
1551
+ ids: { kind: "string-array", option: "ids", query: "ids[]" },
1552
+ limit: { kind: "string", option: "limit" },
1553
+ order: { kind: "string", option: "order" },
1554
+ pageToken: { kind: "string", option: "page-token" },
1555
+ query: { kind: "string", option: "query" },
1556
+ updatedSince: { kind: "string", option: "updated-since" },
1557
+ },
1558
+ },
1559
+ listQuery: {
1560
+ limitMax: 200,
1561
+ transform(query, options) {
1562
+ if (!options.conversationId) {
1563
+ throw new Error(
1564
+ "`conversation-messages list` requires `--conversation-id`."
1565
+ );
1566
+ }
1567
+
1568
+ return query;
1569
+ },
1570
+ },
1571
+ capabilities: {
1572
+ list: {
1573
+ requiredOptions: ["conversationId"],
1574
+ },
1575
+ },
1576
+ redaction: {
1577
+ resourceType: "conversation-message",
1578
+ warningLevel: "limited",
1579
+ },
1580
+ riskLevel: "high",
1581
+ display: {
1582
+ get: {
1583
+ fields: [
1584
+ { label: "ID", value: (message) => message.id },
1585
+ { label: "Date", value: (message) => message.date },
1586
+ { label: "Body", value: (message) => message.body },
1587
+ { label: "Conversation", value: (message) => message.conversation?.subject },
1588
+ { label: "Sender", value: (message) => readParticipantName(message.sender) },
1589
+ {
1590
+ label: "Receivers",
1591
+ value: (message) => summarizeValues(message.receivers, readParticipantName),
1592
+ },
1593
+ { label: "Document", value: (message) => readDocumentLabel(message.document) },
1594
+ { label: "Created", value: (message) => message.created_at },
1595
+ { label: "Updated", value: (message) => message.updated_at },
1596
+ ],
1597
+ },
1598
+ list: {
1599
+ columns: [
1600
+ { header: "ID", key: "id", width: 8 },
1601
+ { header: "DATE", key: "date", width: 10 },
1602
+ { header: "SENDER", key: "sender", width: 20 },
1603
+ { header: "RECEIVERS", key: "receivers", width: 24 },
1604
+ { header: "BODY", key: "body", width: 30, pad: false },
1605
+ ],
1606
+ emptyMessage: "No conversation messages found for the selected filters.",
1607
+ formatRow: (message) => ({
1608
+ body: String(message.body || "-"),
1609
+ date: String(message.date || "-"),
1610
+ id: String(message.id || "-"),
1611
+ receivers: summarizeValues(message.receivers, readParticipantName, 2),
1612
+ sender: readParticipantName(message.sender),
1613
+ }),
1614
+ noun: "conversation messages",
1615
+ },
1616
+ },
1617
+ summaryLabels: {
1618
+ plural: "conversation messages",
1619
+ singular: "conversation message",
1620
+ },
1621
+ supports: {
1622
+ get: true,
1623
+ list: true,
1624
+ },
1625
+ },
1626
+ notes: {
1627
+ aliases: ["note"],
1628
+ apiPath: "notes",
1629
+ defaultFields: {
1630
+ get: NOTE_FIELDS,
1631
+ list: NOTE_FIELDS,
1632
+ },
1633
+ handlerKey: "notes",
1634
+ help: {
1635
+ get: "Fetch a single note by id",
1636
+ list: "List notes with filters and pagination",
1637
+ },
1638
+ optionSchema: {
1639
+ get: {
1640
+ fields: { kind: "string", option: "fields" },
1641
+ id: { positional: 0 },
1642
+ },
1643
+ list: {
1644
+ all: { kind: "flag", option: "all" },
1645
+ contactId: { kind: "string", option: "contact-id" },
1646
+ createdSince: { kind: "string", option: "created-since" },
1647
+ fields: { kind: "string", option: "fields" },
1648
+ hasTimeEntries: {
1649
+ kind: "boolean",
1650
+ option: "has-time-entries",
1651
+ query: "has_time_entries",
1652
+ },
1653
+ ids: { kind: "string-array", option: "ids", query: "ids[]" },
1654
+ limit: { kind: "string", option: "limit" },
1655
+ matterId: { kind: "string", option: "matter-id" },
1656
+ order: { kind: "string", option: "order" },
1657
+ pageToken: { kind: "string", option: "page-token" },
1658
+ query: { kind: "string", option: "query" },
1659
+ type: { kind: "string", option: "type" },
1660
+ updatedSince: { kind: "string", option: "updated-since" },
1661
+ },
1662
+ },
1663
+ listQuery: {
1664
+ limitMax: 200,
1665
+ transform(query, options) {
1666
+ if (!options.type) {
1667
+ throw new Error("`notes list` requires `--type`.");
1668
+ }
1669
+
1670
+ const normalizedType = String(options.type).trim().toLowerCase();
1671
+ if (normalizedType !== "matter" && normalizedType !== "contact") {
1672
+ throw new Error("`notes list --type` must be `Matter` or `Contact`.");
1673
+ }
1674
+
1675
+ return {
1676
+ ...query,
1677
+ type: normalizedType === "matter" ? "Matter" : "Contact",
1678
+ };
1679
+ },
1680
+ },
1681
+ capabilities: {
1682
+ list: {
1683
+ requiredOptions: ["type"],
1684
+ },
1685
+ },
1686
+ redaction: {
1687
+ resourceType: "note",
1688
+ warningLevel: "limited",
1689
+ },
1690
+ riskLevel: "high",
1691
+ display: {
1692
+ get: {
1693
+ fields: [
1694
+ { label: "ID", value: (note) => note.id },
1695
+ { label: "Type", value: (note) => note.type },
1696
+ { label: "Subject", value: (note) => note.subject },
1697
+ { label: "Detail", value: (note) => note.detail },
1698
+ { label: "Date", value: (note) => note.date },
1699
+ { label: "Matter / Contact", value: readMatterOrContactLabel },
1700
+ { label: "Author", value: (note) => readUserName(note.author) },
1701
+ { label: "Time Entries", value: (note) => note.time_entries_count },
1702
+ { label: "Created", value: (note) => note.created_at },
1703
+ { label: "Updated", value: (note) => note.updated_at },
1704
+ ],
1705
+ },
1706
+ list: {
1707
+ columns: [
1708
+ { header: "ID", key: "id", width: 8 },
1709
+ { header: "TYPE", key: "type", width: 12 },
1710
+ { header: "DATE", key: "date", width: 10 },
1711
+ { header: "MATTER / CONTACT", key: "subjectTarget", width: 20 },
1712
+ { header: "SUBJECT", key: "subject", width: 30, pad: false },
1713
+ ],
1714
+ emptyMessage: "No notes found for the selected filters.",
1715
+ formatRow: (note) => ({
1716
+ date: String(note.date || "-"),
1717
+ id: String(note.id || "-"),
1718
+ subject: String(note.subject || "-"),
1719
+ subjectTarget: readMatterOrContactLabel(note),
1720
+ type: String(note.type || "-"),
1721
+ }),
1722
+ noun: "notes",
1723
+ },
1724
+ },
1725
+ summaryLabels: {
1726
+ plural: "notes",
1727
+ singular: "note",
1728
+ },
1729
+ supports: {
1730
+ get: true,
1731
+ list: true,
1732
+ },
1733
+ },
1734
+ "custom-fields": {
1735
+ aliases: ["custom-field"],
1736
+ apiPath: "custom_fields",
1737
+ defaultFields: {
1738
+ get: CUSTOM_FIELD_FIELDS,
1739
+ list: CUSTOM_FIELD_FIELDS,
1740
+ },
1741
+ handlerKey: "custom-fields",
1742
+ help: {
1743
+ get: "Fetch a single custom field by id",
1744
+ list: "List custom fields with filters and pagination",
1745
+ },
1746
+ optionSchema: {
1747
+ get: {
1748
+ fields: { kind: "string", option: "fields" },
1749
+ id: { positional: 0 },
1750
+ },
1751
+ list: {
1752
+ all: { kind: "flag", option: "all" },
1753
+ createdSince: { kind: "string", option: "created-since" },
1754
+ deleted: { kind: "boolean", option: "deleted" },
1755
+ fieldType: { kind: "string", option: "field-type", query: "field_type" },
1756
+ fields: { kind: "string", option: "fields" },
1757
+ ids: { kind: "string-array", option: "ids", query: "ids[]" },
1758
+ limit: { kind: "string", option: "limit" },
1759
+ order: { kind: "string", option: "order" },
1760
+ pageToken: { kind: "string", option: "page-token" },
1761
+ parentType: { kind: "string", option: "parent-type", query: "parent_type" },
1762
+ query: { kind: "string", option: "query" },
1763
+ updatedSince: { kind: "string", option: "updated-since" },
1764
+ visibleAndRequired: {
1765
+ kind: "boolean",
1766
+ option: "visible-and-required",
1767
+ query: "visible_and_required",
1768
+ },
1769
+ },
1770
+ },
1771
+ listQuery: {
1772
+ limitMax: 200,
1773
+ },
1774
+ redaction: {
1775
+ resourceType: "custom-field",
1776
+ warningLevel: "standard",
1777
+ },
1778
+ riskLevel: "low",
1779
+ display: {
1780
+ get: {
1781
+ fields: [
1782
+ { label: "ID", value: (field) => field.id },
1783
+ { label: "Name", value: (field) => field.name },
1784
+ { label: "Parent Type", value: (field) => field.parent_type },
1785
+ { label: "Field Type", value: (field) => field.field_type },
1786
+ { label: "Displayed", value: (field) => formatBoolean(field.displayed) },
1787
+ { label: "Deleted", value: (field) => formatBoolean(field.deleted) },
1788
+ { label: "Required", value: (field) => formatBoolean(field.required) },
1789
+ { label: "Display Order", value: (field) => field.display_order },
1790
+ {
1791
+ label: "Picklist Options",
1792
+ value: (field) =>
1793
+ summarizeValues(field.picklist_options, (option) => option.option),
1794
+ },
1795
+ { label: "Created", value: (field) => field.created_at },
1796
+ { label: "Updated", value: (field) => field.updated_at },
1797
+ ],
1798
+ },
1799
+ list: {
1800
+ columns: [
1801
+ { header: "ID", key: "id", width: 8 },
1802
+ { header: "NAME", key: "name", width: 28 },
1803
+ { header: "PARENT", key: "parentType", width: 16 },
1804
+ { header: "TYPE", key: "fieldType", width: 16 },
1805
+ { header: "REQUIRED", key: "required", width: 8 },
1806
+ { header: "DISPLAYED", key: "displayed", width: 9, pad: false },
1807
+ ],
1808
+ emptyMessage: "No custom fields found for the selected filters.",
1809
+ formatRow: (field) => ({
1810
+ displayed: formatBoolean(field.displayed),
1811
+ fieldType: String(field.field_type || "-"),
1812
+ id: String(field.id || "-"),
1813
+ name: String(field.name || "-"),
1814
+ parentType: String(field.parent_type || "-"),
1815
+ required: formatBoolean(field.required),
1816
+ }),
1817
+ noun: "custom fields",
1818
+ },
1819
+ },
1820
+ summaryLabels: {
1821
+ plural: "custom fields",
1822
+ singular: "custom field",
1823
+ },
1824
+ supports: {
1825
+ get: true,
1826
+ list: true,
1827
+ },
1828
+ },
1829
+ "outstanding-client-balances": {
1830
+ aliases: ["outstanding-client-balance"],
1831
+ apiPath: "outstanding_client_balances",
1832
+ defaultFields: {
1833
+ list: OUTSTANDING_CLIENT_BALANCE_FIELDS,
1834
+ },
1835
+ handlerKey: "outstanding-client-balances",
1836
+ help: {
1837
+ list: "List outstanding client balances with filters and pagination",
1838
+ },
1839
+ optionSchema: {
1840
+ list: {
1841
+ all: { kind: "flag", option: "all" },
1842
+ contactId: { kind: "string", option: "contact-id" },
1843
+ fields: { kind: "string", option: "fields" },
1844
+ lastPaidEndDate: {
1845
+ kind: "iso-date",
1846
+ option: "last-paid-end-date",
1847
+ query: "last_paid_end_date",
1848
+ },
1849
+ lastPaidStartDate: {
1850
+ kind: "iso-date",
1851
+ option: "last-paid-start-date",
1852
+ query: "last_paid_start_date",
1853
+ },
1854
+ limit: { kind: "string", option: "limit" },
1855
+ newestBillDueEndDate: {
1856
+ kind: "iso-date",
1857
+ option: "newest-bill-due-end-date",
1858
+ query: "newest_bill_due_end_date",
1859
+ },
1860
+ newestBillDueStartDate: {
1861
+ kind: "iso-date",
1862
+ option: "newest-bill-due-start-date",
1863
+ query: "newest_bill_due_start_date",
1864
+ },
1865
+ originatingAttorneyId: {
1866
+ kind: "string",
1867
+ option: "originating-attorney-id",
1868
+ query: "originating_attorney_id",
1869
+ },
1870
+ pageToken: { kind: "string", option: "page-token" },
1871
+ responsibleAttorneyId: {
1872
+ kind: "string",
1873
+ option: "responsible-attorney-id",
1874
+ query: "responsible_attorney_id",
1875
+ },
1876
+ },
1877
+ },
1878
+ listQuery: {
1879
+ limitMax: 200,
1880
+ },
1881
+ redaction: {
1882
+ resourceType: "outstanding-client-balance",
1883
+ warningLevel: "limited",
1884
+ },
1885
+ riskLevel: "high",
1886
+ display: {
1887
+ list: {
1888
+ columns: [
1889
+ { header: "ID", key: "id", width: 8 },
1890
+ { header: "CONTACT", key: "contact", width: 28 },
1891
+ { header: "BALANCE", key: "balance", width: 10 },
1892
+ { header: "NEWEST DUE", key: "newestDue", width: 12 },
1893
+ { header: "LAST PAYMENT", key: "lastPayment", width: 12 },
1894
+ { header: "MATTERS", key: "matters", width: 7, pad: false },
1895
+ ],
1896
+ emptyMessage: "No outstanding client balances found for the selected filters.",
1897
+ formatRow: (balance) => ({
1898
+ balance: formatMoney(balance.total_outstanding_balance),
1899
+ contact: readContactName(balance.contact),
1900
+ id: String(balance.id || "-"),
1901
+ lastPayment: String(balance.last_payment_date || "-"),
1902
+ matters: readOutstandingMatterCount(balance),
1903
+ newestDue: String(balance.newest_issued_bill_due_date || "-"),
1904
+ }),
1905
+ noun: "outstanding client balances",
1906
+ },
1907
+ },
1908
+ summaryLabels: {
1909
+ plural: "outstanding client balances",
1910
+ singular: "outstanding client balance",
1911
+ },
1912
+ supports: {
1913
+ get: false,
1914
+ list: true,
1915
+ },
1916
+ },
1917
+ "matter-dockets": {
1918
+ aliases: ["matter-docket"],
1919
+ apiPath: "court_rules/matter_dockets",
1920
+ defaultFields: {
1921
+ get: MATTER_DOCKET_FIELDS,
1922
+ list: MATTER_DOCKET_FIELDS,
1923
+ },
1924
+ handlerKey: "matter-dockets",
1925
+ help: {
1926
+ get: "Fetch a single matter docket by id",
1927
+ list: "List matter dockets with filters and pagination",
1928
+ },
1929
+ optionSchema: {
1930
+ get: {
1931
+ fields: { kind: "string", option: "fields" },
1932
+ id: { positional: 0 },
1933
+ },
1934
+ list: {
1935
+ all: { kind: "flag", option: "all" },
1936
+ createdSince: { kind: "string", option: "created-since" },
1937
+ fields: { kind: "string", option: "fields" },
1938
+ ids: { kind: "string-array", option: "ids", query: "ids[]" },
1939
+ limit: { kind: "string", option: "limit" },
1940
+ matterId: { kind: "string", option: "matter-id" },
1941
+ matterStatus: { kind: "string", option: "matter-status", query: "matter_status" },
1942
+ order: { kind: "string", option: "order" },
1943
+ pageToken: { kind: "string", option: "page-token" },
1944
+ query: { kind: "string", option: "query" },
1945
+ serviceTypeId: {
1946
+ kind: "string",
1947
+ option: "service-type-id",
1948
+ query: "service_type_id",
1949
+ },
1950
+ status: { kind: "string", option: "status" },
1951
+ updatedSince: { kind: "string", option: "updated-since" },
1952
+ },
1953
+ },
1954
+ listQuery: {
1955
+ limitMax: 200,
1956
+ },
1957
+ redaction: {
1958
+ resourceType: "matter-docket",
1959
+ warningLevel: "limited",
1960
+ },
1961
+ riskLevel: "high",
1962
+ display: {
1963
+ get: {
1964
+ fields: [
1965
+ { label: "ID", value: (docket) => docket.id },
1966
+ { label: "Name", value: (docket) => docket.name },
1967
+ { label: "Status", value: (docket) => docket.status },
1968
+ { label: "Start Date", value: (docket) => docket.start_date },
1969
+ { label: "Start Time", value: (docket) => docket.start_time },
1970
+ { label: "Matter", value: (docket) => readMatterLabel(docket.matter) },
1971
+ {
1972
+ label: "Jurisdiction",
1973
+ value: (docket) => readMatterDocketJurisdiction(docket),
1974
+ },
1975
+ { label: "Trigger", value: (docket) => readMatterDocketTrigger(docket) },
1976
+ {
1977
+ label: "Service Type",
1978
+ value: (docket) => readMatterDocketServiceType(docket),
1979
+ },
1980
+ {
1981
+ label: "Calendar Entries",
1982
+ value: (docket) =>
1983
+ summarizeValues(
1984
+ docket.calendar_entries,
1985
+ (entry) => entry.summary || entry.start_at || entry.start_date
1986
+ ),
1987
+ },
1988
+ { label: "Created", value: (docket) => docket.created_at },
1989
+ { label: "Updated", value: (docket) => docket.updated_at },
1990
+ { label: "Deleted", value: (docket) => docket.deleted_at },
1991
+ ],
1992
+ },
1993
+ list: {
1994
+ columns: [
1995
+ { header: "ID", key: "id", width: 8 },
1996
+ { header: "START DATE", key: "startDate", width: 12 },
1997
+ { header: "STATUS", key: "status", width: 12 },
1998
+ { header: "MATTER", key: "matter", width: 20 },
1999
+ { header: "JURISDICTION", key: "jurisdiction", width: 20 },
2000
+ { header: "NAME", key: "name", width: 24, pad: false },
2001
+ ],
2002
+ emptyMessage: "No matter dockets found for the selected filters.",
2003
+ formatRow: (docket) => ({
2004
+ id: String(docket.id || "-"),
2005
+ jurisdiction: String(readMatterDocketJurisdiction(docket)),
2006
+ matter: readMatterLabel(docket.matter),
2007
+ name: String(docket.name || "-"),
2008
+ startDate: String(docket.start_date || "-"),
2009
+ status: String(docket.status || "-"),
2010
+ }),
2011
+ noun: "matter dockets",
2012
+ },
2013
+ },
2014
+ summaryLabels: {
2015
+ plural: "matter dockets",
2016
+ singular: "matter docket",
2017
+ },
2018
+ supports: {
2019
+ get: true,
2020
+ list: true,
2021
+ },
2022
+ },
2023
+ "my-events": {
2024
+ aliases: ["my-event"],
2025
+ apiPath: "internal_notifications/my_events",
2026
+ defaultFields: {
2027
+ list: MY_EVENT_FIELDS,
2028
+ },
2029
+ handlerKey: "my-events",
2030
+ help: {
2031
+ list: "List internal notification events for the current user",
2032
+ },
2033
+ optionSchema: {
2034
+ list: {
2035
+ all: { kind: "flag", option: "all" },
2036
+ fields: { kind: "string", option: "fields" },
2037
+ limit: { kind: "string", option: "limit" },
2038
+ pageToken: { kind: "string", option: "page-token" },
2039
+ },
2040
+ },
2041
+ listQuery: {
2042
+ limitMax: 200,
2043
+ },
2044
+ redaction: {
2045
+ resourceType: "my-event",
2046
+ warningLevel: "limited",
2047
+ },
2048
+ riskLevel: "high",
2049
+ display: {
2050
+ list: {
2051
+ columns: [
2052
+ { header: "ID", key: "id", width: 8 },
2053
+ { header: "OCCURRED", key: "occurredAt", width: 20 },
2054
+ { header: "SUBJECT", key: "subject", width: 14 },
2055
+ { header: "TITLE", key: "title", width: 28 },
2056
+ { header: "DETAIL", key: "detail", width: 24, pad: false },
2057
+ ],
2058
+ emptyMessage: "No events found for the selected filters.",
2059
+ formatRow: (record) => ({
2060
+ detail: String(readMyEventField(record, "primary_detail")),
2061
+ id: String(readMyEventField(record, "id")),
2062
+ occurredAt: String(readMyEventField(record, "occurred_at")),
2063
+ subject: String(readMyEventField(record, "subject_type")),
2064
+ title: String(readMyEventTitle(record)),
2065
+ }),
2066
+ noun: "events",
2067
+ },
2068
+ },
2069
+ summaryLabels: {
2070
+ plural: "events",
2071
+ singular: "event",
2072
+ },
2073
+ supports: {
2074
+ get: false,
2075
+ list: true,
2076
+ },
2077
+ },
516
2078
  users: {
517
2079
  aliases: ["user"],
518
2080
  apiPath: "users",
@@ -546,11 +2108,55 @@ const RESOURCE_METADATA = {
546
2108
  updatedSince: { kind: "string", option: "updated-since" },
547
2109
  },
548
2110
  },
2111
+ listQuery: {
2112
+ limitMax: 2000,
2113
+ },
549
2114
  redaction: {
550
2115
  resourceType: "user",
551
2116
  warningLevel: "none",
552
2117
  },
553
2118
  riskLevel: "low",
2119
+ display: {
2120
+ get: {
2121
+ fields: [
2122
+ { label: "ID", value: (user) => user.id },
2123
+ { label: "Name", value: readUserName },
2124
+ { label: "Email", value: (user) => user.email },
2125
+ { label: "Enabled", value: (user) => formatBoolean(user.enabled) },
2126
+ { label: "Roles", value: readRoleList },
2127
+ { label: "Subscription", value: (user) => user.subscription_type },
2128
+ { label: "Phone", value: (user) => user.phone_number },
2129
+ { label: "Time Zone", value: (user) => user.time_zone },
2130
+ { label: "Rate", value: (user) => user.rate },
2131
+ { label: "Account Owner", value: (user) => formatBoolean(user.account_owner) },
2132
+ { label: "Clio Connect", value: (user) => formatBoolean(user.clio_connect) },
2133
+ {
2134
+ label: "Court Rules Default Attendee",
2135
+ value: (user) => formatBoolean(user.court_rules_default_attendee),
2136
+ },
2137
+ { label: "Created", value: (user) => user.created_at },
2138
+ { label: "Updated", value: (user) => user.updated_at },
2139
+ ],
2140
+ },
2141
+ list: {
2142
+ columns: [
2143
+ { header: "ID", key: "id", width: 8 },
2144
+ { header: "NAME", key: "name", width: 28 },
2145
+ { header: "EMAIL", key: "email", width: 28 },
2146
+ { header: "ENABLED", key: "enabled", width: 7 },
2147
+ { header: "ROLES", key: "roles", width: 30, pad: false },
2148
+ ],
2149
+ emptyMessage: "No users found for the selected filters.",
2150
+ formatRow: (user) => ({
2151
+ id: String(user.id || "-"),
2152
+ name: readUserName(user),
2153
+ email: String(user.email || "-"),
2154
+ enabled: formatBoolean(user.enabled),
2155
+ roles: readRoleList(user),
2156
+ }),
2157
+ noun: "users",
2158
+ },
2159
+ },
554
2160
  summaryLabels: {
555
2161
  plural: "users",
556
2162
  singular: "user",
@@ -562,6 +2168,23 @@ const RESOURCE_METADATA = {
562
2168
  },
563
2169
  };
564
2170
 
2171
+ function normalizeOperationCapability(resourceMetadata, sub) {
2172
+ const overrides = resourceMetadata.capabilities?.[sub] || {};
2173
+ return {
2174
+ enabled: Boolean(resourceMetadata.supports?.[sub]),
2175
+ requiredOptions: Array.isArray(overrides.requiredOptions)
2176
+ ? overrides.requiredOptions
2177
+ : [],
2178
+ };
2179
+ }
2180
+
2181
+ Object.values(RESOURCE_METADATA).forEach((resourceMetadata) => {
2182
+ resourceMetadata.capabilities = {
2183
+ get: normalizeOperationCapability(resourceMetadata, "get"),
2184
+ list: normalizeOperationCapability(resourceMetadata, "list"),
2185
+ };
2186
+ });
2187
+
565
2188
  const ALIAS_TO_COMMAND = RESOURCE_ORDER.reduce((aliases, command) => {
566
2189
  const metadata = RESOURCE_METADATA[command];
567
2190
  metadata.aliases.forEach((alias) => {
@@ -582,10 +2205,24 @@ function normalizeResourceCommand(command) {
582
2205
  return ALIAS_TO_COMMAND[command] || command;
583
2206
  }
584
2207
 
2208
+ function listRequiredOptionFlags(resourceMetadata, sub) {
2209
+ if (!resourceMetadata) {
2210
+ return [];
2211
+ }
2212
+
2213
+ const requiredOptions = resourceMetadata.capabilities?.[sub]?.requiredOptions || [];
2214
+ return requiredOptions.map((propertyName) => {
2215
+ const optionName = resourceMetadata.optionSchema?.[sub]?.[propertyName]?.option;
2216
+ return optionName ? `--${optionName}` : propertyName;
2217
+ });
2218
+ }
2219
+
585
2220
  module.exports = {
586
2221
  RESOURCE_METADATA,
587
2222
  RESOURCE_ORDER,
588
2223
  getResourceMetadata,
589
2224
  listResourceMetadata,
2225
+ listRequiredOptionFlags,
590
2226
  normalizeResourceCommand,
2227
+ normalizeBillStatusFilters,
591
2228
  };