@aderis-erp/cli 1.0.17 → 1.1.1

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 (105) hide show
  1. package/dist/commands/access-groups.js +49 -17
  2. package/dist/commands/account-transaction-allocations.js +46 -14
  3. package/dist/commands/account-transactions.js +75 -35
  4. package/dist/commands/accounts.js +54 -22
  5. package/dist/commands/agent-platform.js +53 -21
  6. package/dist/commands/{nango-integrations.d.ts → agent-runs.d.ts} +1 -1
  7. package/dist/commands/agent-runs.js +1155 -0
  8. package/dist/commands/analytics.js +47 -15
  9. package/dist/commands/api-keys.js +49 -16
  10. package/dist/commands/{text-templates.d.ts → async-jobs.d.ts} +1 -1
  11. package/dist/commands/async-jobs.js +1158 -0
  12. package/dist/commands/audit-log.js +46 -14
  13. package/dist/commands/bank-accounts.js +63 -30
  14. package/dist/commands/bank-integrations.d.ts +1 -0
  15. package/dist/commands/bank-integrations.js +66 -31
  16. package/dist/commands/business-partners.js +77 -44
  17. package/dist/commands/chat.js +52 -19
  18. package/dist/commands/credit-card-accounts.d.ts +1 -0
  19. package/dist/commands/credit-card-accounts.js +67 -29
  20. package/dist/commands/delivery-notes.js +78 -44
  21. package/dist/commands/dunning-configs.js +49 -17
  22. package/dist/commands/dunning-reminders.js +52 -20
  23. package/dist/commands/dunning-runs.js +45 -13
  24. package/dist/commands/email-transactions.js +47 -15
  25. package/dist/commands/employees.js +76 -44
  26. package/dist/commands/entity-configurations.js +47 -15
  27. package/dist/commands/event-processor-mappings.js +49 -17
  28. package/dist/commands/files.js +56 -24
  29. package/dist/commands/health.js +49 -17
  30. package/dist/commands/{tag-configurations.d.ts → imports.d.ts} +1 -1
  31. package/dist/commands/{tag-configurations.js → imports.js} +59 -31
  32. package/dist/commands/inbox.js +52 -19
  33. package/dist/commands/integration-identities.d.ts +40 -0
  34. package/dist/commands/integration-identities.js +1151 -0
  35. package/dist/commands/integrations.js +47 -15
  36. package/dist/commands/item-categories.js +49 -17
  37. package/dist/commands/items.js +49 -17
  38. package/dist/commands/journal-lines.js +45 -13
  39. package/dist/commands/journals.js +68 -36
  40. package/dist/commands/ledgers.d.ts +3 -1
  41. package/dist/commands/ledgers.js +96 -34
  42. package/dist/commands/my-account.d.ts +39 -0
  43. package/dist/commands/my-account.js +1160 -0
  44. package/dist/commands/office-locations.js +49 -17
  45. package/dist/commands/payment-methods.js +54 -22
  46. package/dist/commands/payment-runs.js +61 -29
  47. package/dist/commands/payment-terms.js +50 -18
  48. package/dist/commands/payments.js +51 -19
  49. package/dist/commands/payroll-runs.d.ts +43 -0
  50. package/dist/commands/payroll-runs.js +1188 -0
  51. package/dist/commands/permissions.d.ts +39 -0
  52. package/dist/commands/{text-templates.js → permissions.js} +54 -30
  53. package/dist/commands/price-lists.js +54 -22
  54. package/dist/commands/processor-mappings.js +48 -16
  55. package/dist/commands/processors.js +55 -24
  56. package/dist/commands/purchase-order-types.js +49 -17
  57. package/dist/commands/purchase-orders.js +69 -36
  58. package/dist/commands/reports.js +49 -17
  59. package/dist/commands/roles.d.ts +39 -0
  60. package/dist/commands/{nango-integrations.js → roles.js} +63 -26
  61. package/dist/commands/salary-types.js +49 -17
  62. package/dist/commands/sales-invoices.js +84 -51
  63. package/dist/commands/sales-orders.js +78 -44
  64. package/dist/commands/saved-views.js +47 -15
  65. package/dist/commands/scheduled-processor-mappings.js +49 -17
  66. package/dist/commands/schedules.js +53 -21
  67. package/dist/commands/support/end.d.ts +9 -0
  68. package/dist/commands/support/end.js +29 -0
  69. package/dist/commands/support/start.d.ts +14 -0
  70. package/dist/commands/support/start.js +50 -0
  71. package/dist/commands/support/status.d.ts +10 -0
  72. package/dist/commands/support/status.js +45 -0
  73. package/dist/commands/support/tenants.d.ts +10 -0
  74. package/dist/commands/support/tenants.js +39 -0
  75. package/dist/commands/tags.js +51 -18
  76. package/dist/commands/tax-codes.js +52 -20
  77. package/dist/commands/tax-matrix.js +49 -17
  78. package/dist/commands/templates.d.ts +39 -0
  79. package/dist/commands/templates.js +1157 -0
  80. package/dist/commands/tenants.js +52 -20
  81. package/dist/commands/tools.js +46 -14
  82. package/dist/commands/users.d.ts +42 -0
  83. package/dist/commands/users.js +1174 -0
  84. package/dist/commands/vat-declarations.d.ts +2 -0
  85. package/dist/commands/vat-declarations.js +72 -36
  86. package/dist/commands/vendor-invoices.js +81 -49
  87. package/dist/commands/webhooks.js +51 -19
  88. package/dist/commands/workflows.js +47 -15
  89. package/dist/lib/api-client.d.ts +6 -0
  90. package/dist/lib/api-client.js +13 -0
  91. package/dist/lib/auth/constants.d.ts +12 -0
  92. package/dist/lib/auth/constants.js +12 -0
  93. package/dist/lib/auth/index.d.ts +11 -1
  94. package/dist/lib/auth/index.js +105 -6
  95. package/dist/lib/auth/jwks.d.ts +19 -0
  96. package/dist/lib/auth/jwks.js +112 -0
  97. package/dist/lib/auth/keycloak-provider.d.ts +45 -7
  98. package/dist/lib/auth/keycloak-provider.js +161 -27
  99. package/dist/lib/auth/tokens.d.ts +8 -3
  100. package/dist/lib/auth/tokens.js +8 -7
  101. package/dist/lib/auth/types.d.ts +5 -1
  102. package/dist/lib/config.d.ts +18 -0
  103. package/dist/lib/config.js +10 -0
  104. package/oclif.manifest.json +2182 -744
  105. package/package.json +7 -4
@@ -1,44 +1,45 @@
1
1
  import { Command, Args, Flags } from '@oclif/core';
2
- import { apiRequest, ApiError, normalizeSortBy } from '../lib/api-client.js';
2
+ import { apiRequest, ApiError, isTextualMediaType, normalizeSortBy } from '../lib/api-client.js';
3
3
  import { getCurrentContext, getContext, getBaseUrls, hasCredentials, missingCredentialsMessage } from '../lib/config.js';
4
4
  // Add 'import' action if both create and update exist
5
5
  const HAS_IMPORT = true;
6
6
  const HAS_EXPORT = true;
7
7
  const HAS_CAMT053 = false;
8
8
  const NESTED_IMPORTS = ["notes", "files", "contacts", "contacts-notes"];
9
- const SUBCOMMANDS = [...["list", "create", "get", "update", "delete", "merge", "activate", "get-favicon", "update-favicon", "trigger-favicon-backfill", "list-notes", "create-notes", "get-notes", "update-notes", "delete-notes", "list-files", "create-files", "get-files", "update-files", "delete-files", "files-download", "list-contacts", "create-contacts", "get-contacts", "update-contacts", "delete-contacts", "list-contacts-notes", "create-contacts-notes", "get-contacts-notes", "update-contacts-notes", "delete-contacts-notes"], ...(HAS_IMPORT ? ['import'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_IMPORTS.map(n => `import-${n}`)];
9
+ const SUBCOMMANDS = [...["list", "create", "match-candidates", "get", "update", "delete", "merge", "activate", "get-favicon", "update-favicon", "trigger-favicon-backfill", "list-notes", "create-notes", "get-notes", "update-notes", "delete-notes", "list-files", "create-files", "get-files", "update-files", "delete-files", "files-download", "list-contacts", "create-contacts", "get-contacts", "update-contacts", "delete-contacts", "list-contacts-notes", "create-contacts-notes", "get-contacts-notes", "update-contacts-notes", "delete-contacts-notes"], ...(HAS_IMPORT ? ['import'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_IMPORTS.map(n => `import-${n}`)];
10
10
  const ENDPOINTS = {
11
- 'list': { method: 'GET', path: '/v1/business-partners', summary: 'Get all business partners with pagination', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
12
- 'create': { method: 'POST', path: '/v1/business-partners', summary: 'Create a new business partner', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['salesOrderRecipientCc', 'salesOrderRecipientBcc', 'salesInvoiceRecipientCc', 'salesInvoiceRecipientBcc', 'deliveryNoteRecipientCc', 'deliveryNoteRecipientBcc', 'contacts', 'tagIds', 'tagCodes', 'salesEarlyPaymentDiscounts', 'purchaseEarlyPaymentDiscounts', 'notes'] },
13
- 'get': { method: 'GET', path: '/v1/business-partners/{id}', summary: 'Get a business partner by ID', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
14
- 'update': { method: 'PATCH', path: '/v1/business-partners/{id}', summary: 'Update a business partner', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['salesOrderRecipientCc', 'salesOrderRecipientBcc', 'salesInvoiceRecipientCc', 'salesInvoiceRecipientBcc', 'deliveryNoteRecipientCc', 'deliveryNoteRecipientBcc', 'contacts', 'tagIds', 'tagCodes', 'salesEarlyPaymentDiscounts', 'purchaseEarlyPaymentDiscounts', 'notes'] },
15
- 'delete': { method: 'DELETE', path: '/v1/business-partners/{id}', summary: 'Delete a business partner', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
16
- 'merge': { method: 'POST', path: '/v1/business-partners/{id}/merge', summary: 'Merge a business partner into another', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
17
- 'activate': { method: 'POST', path: '/v1/business-partners/{id}/activate', summary: 'Activate a business partner', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
18
- 'get-favicon': { method: 'GET', path: '/v1/business-partners/{id}/favicon', summary: 'Download a business partner\'s stored favicon', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
19
- 'update-favicon': { method: 'PUT', path: '/v1/business-partners/{id}/favicon', summary: 'Manually upload a business partner\'s favicon', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
20
- 'trigger-favicon-backfill': { method: 'POST', path: '/v1/business-partners/favicon-backfill', summary: 'Fetch favicons for every business partner in this tenant that has none yet', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
21
- 'list-notes': { method: 'GET', path: '/v1/business-partners/{businessPartnerId}/notes', summary: 'Get all notes for a business partner', pathParams: ['businessPartnerId'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
22
- 'create-notes': { method: 'POST', path: '/v1/business-partners/{businessPartnerId}/notes', summary: 'Create a note for a business partner', pathParams: ['businessPartnerId'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
23
- 'get-notes': { method: 'GET', path: '/v1/business-partners/{businessPartnerId}/notes/{noteId}', summary: 'Get a specific note for a business partner', pathParams: ['businessPartnerId', 'noteId'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
24
- 'update-notes': { method: 'PATCH', path: '/v1/business-partners/{businessPartnerId}/notes/{noteId}', summary: 'Update a note for a business partner', pathParams: ['businessPartnerId', 'noteId'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
25
- 'delete-notes': { method: 'DELETE', path: '/v1/business-partners/{businessPartnerId}/notes/{noteId}', summary: 'Delete a note from a business partner', pathParams: ['businessPartnerId', 'noteId'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
26
- 'list-files': { method: 'GET', path: '/v1/business-partners/{parentId}/files', summary: 'List file attachments for a business partner', pathParams: ['parentId'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
27
- 'create-files': { method: 'POST', path: '/v1/business-partners/{parentId}/files', summary: 'Upload a file attachment to a business partner', pathParams: ['parentId'], hasBody: false, isMultipart: true, isBinaryResponse: false, bodyArrayFields: [] },
28
- 'get-files': { method: 'GET', path: '/v1/business-partners/{parentId}/files/{fileId}', summary: 'Get a file attachment for a business partner', pathParams: ['parentId', 'fileId'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
29
- 'update-files': { method: 'PATCH', path: '/v1/business-partners/{parentId}/files/{fileId}', summary: 'Update file attachment metadata for a business partner', pathParams: ['parentId', 'fileId'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
30
- 'delete-files': { method: 'DELETE', path: '/v1/business-partners/{parentId}/files/{fileId}', summary: 'Delete a file attachment from a business partner', pathParams: ['parentId', 'fileId'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
31
- 'files-download': { method: 'GET', path: '/v1/business-partners/{parentId}/files/{fileId}/download', summary: 'Download a file attachment from a business partner', pathParams: ['parentId', 'fileId'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
32
- 'list-contacts': { method: 'GET', path: '/v1/business-partners/{businessPartnerId}/contacts', summary: 'Get all contacts for a business partner with pagination', pathParams: ['businessPartnerId'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
33
- 'create-contacts': { method: 'POST', path: '/v1/business-partners/{businessPartnerId}/contacts', summary: 'Create a new contact for a business partner', pathParams: ['businessPartnerId'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['tagIds', 'tagCodes', 'notes'] },
34
- 'get-contacts': { method: 'GET', path: '/v1/business-partners/{businessPartnerId}/contacts/{id}', summary: 'Get a contact by ID', pathParams: ['id', 'businessPartnerId'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
35
- 'update-contacts': { method: 'PATCH', path: '/v1/business-partners/{businessPartnerId}/contacts/{id}', summary: 'Update a contact', pathParams: ['id', 'businessPartnerId'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['tagIds', 'tagCodes', 'notes'] },
36
- 'delete-contacts': { method: 'DELETE', path: '/v1/business-partners/{businessPartnerId}/contacts/{id}', summary: 'Delete a contact', pathParams: ['id', 'businessPartnerId'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
37
- 'list-contacts-notes': { method: 'GET', path: '/v1/business-partners/contacts/{contactId}/notes', summary: 'Get all notes for a contact', pathParams: ['contactId'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
38
- 'create-contacts-notes': { method: 'POST', path: '/v1/business-partners/contacts/{contactId}/notes', summary: 'Create a note for a contact', pathParams: ['contactId'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
39
- 'get-contacts-notes': { method: 'GET', path: '/v1/business-partners/contacts/{contactId}/notes/{noteId}', summary: 'Get a specific note for a contact', pathParams: ['contactId', 'noteId'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
40
- 'update-contacts-notes': { method: 'PATCH', path: '/v1/business-partners/contacts/{contactId}/notes/{noteId}', summary: 'Update a note for a contact', pathParams: ['contactId', 'noteId'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
41
- 'delete-contacts-notes': { method: 'DELETE', path: '/v1/business-partners/contacts/{contactId}/notes/{noteId}', summary: 'Delete a note from a contact', pathParams: ['contactId', 'noteId'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
11
+ 'list': { method: 'GET', path: '/v1/business-partners', summary: 'Get all business partners with pagination', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
12
+ 'create': { method: 'POST', path: '/v1/business-partners', summary: 'Create a new business partner', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['salesOrderRecipientCc', 'salesOrderRecipientBcc', 'salesInvoiceRecipientCc', 'salesInvoiceRecipientBcc', 'deliveryNoteRecipientCc', 'deliveryNoteRecipientBcc', 'dunningRecipientCc', 'dunningRecipientBcc', 'contacts', 'tagIds', 'tagCodes', 'salesEarlyPaymentDiscounts', 'purchaseEarlyPaymentDiscounts', 'notes'] },
13
+ 'match-candidates': { method: 'GET', path: '/v1/business-partners/match-candidates', summary: 'Rank the business partners whose names resemble a bank counterparty text', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
14
+ 'get': { method: 'GET', path: '/v1/business-partners/{id}', summary: 'Get a business partner by ID', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
15
+ 'update': { method: 'PATCH', path: '/v1/business-partners/{id}', summary: 'Update a business partner', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['salesOrderRecipientCc', 'salesOrderRecipientBcc', 'salesInvoiceRecipientCc', 'salesInvoiceRecipientBcc', 'deliveryNoteRecipientCc', 'deliveryNoteRecipientBcc', 'dunningRecipientCc', 'dunningRecipientBcc', 'contacts', 'tagIds', 'tagCodes', 'salesEarlyPaymentDiscounts', 'purchaseEarlyPaymentDiscounts', 'notes'] },
16
+ 'delete': { method: 'DELETE', path: '/v1/business-partners/{id}', summary: 'Delete a business partner', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
17
+ 'merge': { method: 'POST', path: '/v1/business-partners/{id}/merge', summary: 'Merge a business partner into another', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
18
+ 'activate': { method: 'POST', path: '/v1/business-partners/{id}/activate', summary: 'Activate a business partner', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
19
+ 'get-favicon': { method: 'GET', path: '/v1/business-partners/{id}/favicon', summary: 'Download a business partner\'s stored favicon', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: true, hasBinaryVariant: false, bodyArrayFields: [] },
20
+ 'update-favicon': { method: 'PUT', path: '/v1/business-partners/{id}/favicon', summary: 'Manually upload a business partner\'s favicon', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
21
+ 'trigger-favicon-backfill': { method: 'POST', path: '/v1/business-partners/favicon-backfill', summary: 'Fetch favicons for every business partner in this tenant that has none yet', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
22
+ 'list-notes': { method: 'GET', path: '/v1/business-partners/{businessPartnerId}/notes', summary: 'Get all notes for a business partner', pathParams: ['businessPartnerId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
23
+ 'create-notes': { method: 'POST', path: '/v1/business-partners/{businessPartnerId}/notes', summary: 'Create a note for a business partner', pathParams: ['businessPartnerId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
24
+ 'get-notes': { method: 'GET', path: '/v1/business-partners/{businessPartnerId}/notes/{noteId}', summary: 'Get a specific note for a business partner', pathParams: ['businessPartnerId', 'noteId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
25
+ 'update-notes': { method: 'PATCH', path: '/v1/business-partners/{businessPartnerId}/notes/{noteId}', summary: 'Update a note for a business partner', pathParams: ['businessPartnerId', 'noteId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
26
+ 'delete-notes': { method: 'DELETE', path: '/v1/business-partners/{businessPartnerId}/notes/{noteId}', summary: 'Delete a note from a business partner', pathParams: ['businessPartnerId', 'noteId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
27
+ 'list-files': { method: 'GET', path: '/v1/business-partners/{parentId}/files', summary: 'List file attachments for a business partner', pathParams: ['parentId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
28
+ 'create-files': { method: 'POST', path: '/v1/business-partners/{parentId}/files', summary: 'Upload a file attachment to a business partner', pathParams: ['parentId'], hasBody: false, isMultipart: true, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
29
+ 'get-files': { method: 'GET', path: '/v1/business-partners/{parentId}/files/{fileId}', summary: 'Get a file attachment for a business partner', pathParams: ['parentId', 'fileId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
30
+ 'update-files': { method: 'PATCH', path: '/v1/business-partners/{parentId}/files/{fileId}', summary: 'Update file attachment metadata for a business partner', pathParams: ['parentId', 'fileId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
31
+ 'delete-files': { method: 'DELETE', path: '/v1/business-partners/{parentId}/files/{fileId}', summary: 'Delete a file attachment from a business partner', pathParams: ['parentId', 'fileId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
32
+ 'files-download': { method: 'GET', path: '/v1/business-partners/{parentId}/files/{fileId}/download', summary: 'Download a file attachment from a business partner', pathParams: ['parentId', 'fileId'], hasBody: false, isMultipart: false, isBinaryResponse: true, hasBinaryVariant: false, bodyArrayFields: [] },
33
+ 'list-contacts': { method: 'GET', path: '/v1/business-partners/{businessPartnerId}/contacts', summary: 'Get all contacts for a business partner with pagination', pathParams: ['businessPartnerId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
34
+ 'create-contacts': { method: 'POST', path: '/v1/business-partners/{businessPartnerId}/contacts', summary: 'Create a new contact for a business partner', pathParams: ['businessPartnerId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['tagIds', 'tagCodes', 'notes'] },
35
+ 'get-contacts': { method: 'GET', path: '/v1/business-partners/{businessPartnerId}/contacts/{id}', summary: 'Get a contact by ID', pathParams: ['id', 'businessPartnerId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
36
+ 'update-contacts': { method: 'PATCH', path: '/v1/business-partners/{businessPartnerId}/contacts/{id}', summary: 'Update a contact', pathParams: ['id', 'businessPartnerId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['tagIds', 'tagCodes', 'notes'] },
37
+ 'delete-contacts': { method: 'DELETE', path: '/v1/business-partners/{businessPartnerId}/contacts/{id}', summary: 'Delete a contact', pathParams: ['id', 'businessPartnerId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
38
+ 'list-contacts-notes': { method: 'GET', path: '/v1/business-partners/contacts/{contactId}/notes', summary: 'Get all notes for a contact', pathParams: ['contactId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
39
+ 'create-contacts-notes': { method: 'POST', path: '/v1/business-partners/contacts/{contactId}/notes', summary: 'Create a note for a contact', pathParams: ['contactId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
40
+ 'get-contacts-notes': { method: 'GET', path: '/v1/business-partners/contacts/{contactId}/notes/{noteId}', summary: 'Get a specific note for a contact', pathParams: ['contactId', 'noteId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
41
+ 'update-contacts-notes': { method: 'PATCH', path: '/v1/business-partners/contacts/{contactId}/notes/{noteId}', summary: 'Update a note for a contact', pathParams: ['contactId', 'noteId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
42
+ 'delete-contacts-notes': { method: 'DELETE', path: '/v1/business-partners/contacts/{contactId}/notes/{noteId}', summary: 'Delete a note from a contact', pathParams: ['contactId', 'noteId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
42
43
  };
43
44
  export default class BusinessPartners extends Command {
44
45
  static description = 'Manage business partners';
@@ -293,10 +294,11 @@ export default class BusinessPartners extends Command {
293
294
  process.stderr.write('\n');
294
295
  }
295
296
  try {
296
- // Binary response: download to file
297
+ // Binary response: to -f / --file, or to stdout when it is redirected.
297
298
  if (endpoint.isBinaryResponse) {
298
- if (!flags.file) {
299
- this.error(`Endpoint returns a binary file. Specify the output path with -f / --file (e.g. -f report.xlsx).`);
299
+ const toStdout = !flags.file;
300
+ if (toStdout && process.stdout.isTTY) {
301
+ this.error(`Endpoint returns a binary file. Specify the output path with -f / --file (e.g. -f report.xlsx), or redirect stdout to a file.`);
300
302
  }
301
303
  const binResponse = await apiRequest(endpoint.method, apiPath, {
302
304
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
@@ -304,24 +306,55 @@ export default class BusinessPartners extends Command {
304
306
  contextName: flags.context,
305
307
  binary: true,
306
308
  });
307
- const fsModule = await import('node:fs');
308
309
  const binData = binResponse.data;
309
310
  const buffer = Buffer.isBuffer(binData)
310
311
  ? binData
311
312
  : Buffer.from(binData);
313
+ if (toStdout) {
314
+ // Nothing else may reach stdout here — it is the file.
315
+ process.stdout.write(buffer);
316
+ return;
317
+ }
318
+ const fsModule = await import('node:fs');
312
319
  fsModule.writeFileSync(flags.file, buffer);
313
320
  this.log(`Saved ${buffer.length} bytes to ${flags.file}`);
314
321
  return;
315
322
  }
316
- const response = await apiRequest(endpoint.method, apiPath, {
317
- params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
318
- data,
319
- contextName: flags.context,
320
- acceptAny: true,
321
- });
323
+ let response;
324
+ if (endpoint.hasBinaryVariant) {
325
+ // This route answers JSON or a file depending on the request body, so the
326
+ // bytes are read first and the response's own content-type decides. Asking
327
+ // for text here would corrupt an xlsx/pdf before anything could inspect it.
328
+ const raw = await apiRequest(endpoint.method, apiPath, {
329
+ params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
330
+ data,
331
+ contextName: flags.context,
332
+ binary: true,
333
+ });
334
+ const rawType = String(raw.headers['content-type'] || '').toLowerCase();
335
+ const rawData = raw.data;
336
+ const buffer = Buffer.isBuffer(rawData) ? rawData : Buffer.from(rawData);
337
+ if (!isTextualMediaType(rawType)) {
338
+ if (process.stdout.isTTY) {
339
+ this.error(`The API returned ${rawType || 'binary data'}. Redirect stdout to a file, e.g. ... > report.xlsx`);
340
+ }
341
+ // Nothing else may reach stdout here — it is the file.
342
+ process.stdout.write(buffer);
343
+ return;
344
+ }
345
+ response = { data: buffer.toString('utf8'), headers: raw.headers, status: raw.status };
346
+ }
347
+ else {
348
+ response = await apiRequest(endpoint.method, apiPath, {
349
+ params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
350
+ data,
351
+ contextName: flags.context,
352
+ acceptAny: true,
353
+ });
354
+ }
322
355
  // Check content-type for non-JSON responses (XML, text, etc.)
323
356
  const contentType = (response.headers['content-type'] || '').toLowerCase();
324
- if (contentType.includes('xml') || contentType.includes('text/')) {
357
+ if (isTextualMediaType(contentType) && !contentType.startsWith('application/json')) {
325
358
  // Output raw content for XML/text responses (CSV, plain, HTML, etc.)
326
359
  this.log(String(response.data));
327
360
  return;
@@ -1,19 +1,20 @@
1
1
  import { Command, Args, Flags } from '@oclif/core';
2
- import { apiRequest, ApiError, normalizeSortBy } from '../lib/api-client.js';
2
+ import { apiRequest, ApiError, isTextualMediaType, normalizeSortBy } from '../lib/api-client.js';
3
3
  import { getCurrentContext, getContext, getBaseUrls, hasCredentials, missingCredentialsMessage } from '../lib/config.js';
4
4
  // Add 'import' action if both create and update exist
5
5
  const HAS_IMPORT = true;
6
6
  const HAS_EXPORT = false;
7
7
  const HAS_CAMT053 = false;
8
8
  const NESTED_IMPORTS = [];
9
- const SUBCOMMANDS = [...["create", "get", "get-all", "update", "delete", "stream"], ...(HAS_IMPORT ? ['import'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_IMPORTS.map(n => `import-${n}`)];
9
+ const SUBCOMMANDS = [...["create", "get", "get-all", "update", "delete", "stream", "token-refresh"], ...(HAS_IMPORT ? ['import'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_IMPORTS.map(n => `import-${n}`)];
10
10
  const ENDPOINTS = {
11
- 'create': { method: 'POST', path: '/v1/chat', summary: 'Send a chat message and stream the assistant reply (SSE).', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['attachments'] },
12
- 'get': { method: 'GET', path: '/v1/conversations', summary: 'List the caller\'s chat conversations, most recently updated first.', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
13
- 'get-all': { method: 'GET', path: '/v1/conversations/{id}', summary: 'Fetch one conversation with its messages.', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
14
- 'update': { method: 'PATCH', path: '/v1/conversations/{id}', summary: 'Store client-owned UI state on a conversation.', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
15
- 'delete': { method: 'DELETE', path: '/v1/conversations/{id}', summary: 'Delete one conversation.', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
16
- 'stream': { method: 'GET', path: '/v1/conversations/{id}/stream', summary: 'Reattach to a conversation\'s running turn (SSE replay + follow).', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
11
+ 'create': { method: 'POST', path: '/v1/chat', summary: 'Send a chat message and stream the assistant reply (SSE).', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['attachments'] },
12
+ 'get': { method: 'GET', path: '/v1/conversations', summary: 'List the caller\'s chat conversations, most recently updated first.', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
13
+ 'get-all': { method: 'GET', path: '/v1/conversations/{id}', summary: 'Fetch one conversation with its messages.', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
14
+ 'update': { method: 'PATCH', path: '/v1/conversations/{id}', summary: 'Store client-owned UI state on a conversation.', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
15
+ 'delete': { method: 'DELETE', path: '/v1/conversations/{id}', summary: 'Delete one conversation.', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
16
+ 'stream': { method: 'GET', path: '/v1/conversations/{id}/stream', summary: 'Reattach to a conversation\'s running turn (SSE replay + follow).', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
17
+ 'token-refresh': { method: 'POST', path: '/v1/chat/token/refresh', summary: 'Refresh the delegated token a chat turn runs on', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
17
18
  };
18
19
  export default class Chat extends Command {
19
20
  static description = 'Manage chat';
@@ -263,10 +264,11 @@ export default class Chat extends Command {
263
264
  process.stderr.write('\n');
264
265
  }
265
266
  try {
266
- // Binary response: download to file
267
+ // Binary response: to -f / --file, or to stdout when it is redirected.
267
268
  if (endpoint.isBinaryResponse) {
268
- if (!flags.file) {
269
- this.error(`Endpoint returns a binary file. Specify the output path with -f / --file (e.g. -f report.xlsx).`);
269
+ const toStdout = !flags.file;
270
+ if (toStdout && process.stdout.isTTY) {
271
+ this.error(`Endpoint returns a binary file. Specify the output path with -f / --file (e.g. -f report.xlsx), or redirect stdout to a file.`);
270
272
  }
271
273
  const binResponse = await apiRequest(endpoint.method, apiPath, {
272
274
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
@@ -274,24 +276,55 @@ export default class Chat extends Command {
274
276
  contextName: flags.context,
275
277
  binary: true,
276
278
  });
277
- const fsModule = await import('node:fs');
278
279
  const binData = binResponse.data;
279
280
  const buffer = Buffer.isBuffer(binData)
280
281
  ? binData
281
282
  : Buffer.from(binData);
283
+ if (toStdout) {
284
+ // Nothing else may reach stdout here — it is the file.
285
+ process.stdout.write(buffer);
286
+ return;
287
+ }
288
+ const fsModule = await import('node:fs');
282
289
  fsModule.writeFileSync(flags.file, buffer);
283
290
  this.log(`Saved ${buffer.length} bytes to ${flags.file}`);
284
291
  return;
285
292
  }
286
- const response = await apiRequest(endpoint.method, apiPath, {
287
- params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
288
- data,
289
- contextName: flags.context,
290
- acceptAny: true,
291
- });
293
+ let response;
294
+ if (endpoint.hasBinaryVariant) {
295
+ // This route answers JSON or a file depending on the request body, so the
296
+ // bytes are read first and the response's own content-type decides. Asking
297
+ // for text here would corrupt an xlsx/pdf before anything could inspect it.
298
+ const raw = await apiRequest(endpoint.method, apiPath, {
299
+ params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
300
+ data,
301
+ contextName: flags.context,
302
+ binary: true,
303
+ });
304
+ const rawType = String(raw.headers['content-type'] || '').toLowerCase();
305
+ const rawData = raw.data;
306
+ const buffer = Buffer.isBuffer(rawData) ? rawData : Buffer.from(rawData);
307
+ if (!isTextualMediaType(rawType)) {
308
+ if (process.stdout.isTTY) {
309
+ this.error(`The API returned ${rawType || 'binary data'}. Redirect stdout to a file, e.g. ... > report.xlsx`);
310
+ }
311
+ // Nothing else may reach stdout here — it is the file.
312
+ process.stdout.write(buffer);
313
+ return;
314
+ }
315
+ response = { data: buffer.toString('utf8'), headers: raw.headers, status: raw.status };
316
+ }
317
+ else {
318
+ response = await apiRequest(endpoint.method, apiPath, {
319
+ params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
320
+ data,
321
+ contextName: flags.context,
322
+ acceptAny: true,
323
+ });
324
+ }
292
325
  // Check content-type for non-JSON responses (XML, text, etc.)
293
326
  const contentType = (response.headers['content-type'] || '').toLowerCase();
294
- if (contentType.includes('xml') || contentType.includes('text/')) {
327
+ if (isTextualMediaType(contentType) && !contentType.startsWith('application/json')) {
295
328
  // Output raw content for XML/text responses (CSV, plain, HTML, etc.)
296
329
  this.log(String(response.data));
297
330
  return;
@@ -16,6 +16,7 @@ export default class CreditCardAccounts extends Command {
16
16
  set: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
17
17
  'mapping-id': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
18
18
  'note-id': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
19
+ 'card-id': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
19
20
  page: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
20
21
  limit: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
21
22
  include: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
@@ -1,28 +1,33 @@
1
1
  import { Command, Args, Flags } from '@oclif/core';
2
- import { apiRequest, ApiError, normalizeSortBy } from '../lib/api-client.js';
2
+ import { apiRequest, ApiError, isTextualMediaType, normalizeSortBy } from '../lib/api-client.js';
3
3
  import { getCurrentContext, getContext, getBaseUrls, hasCredentials, missingCredentialsMessage } from '../lib/config.js';
4
4
  // Add 'import' action if both create and update exist
5
5
  const HAS_IMPORT = true;
6
6
  const HAS_EXPORT = true;
7
7
  const HAS_CAMT053 = false;
8
- const NESTED_IMPORTS = ["ledger-mappings", "notes"];
9
- const SUBCOMMANDS = [...["list", "create", "get", "update", "delete", "list-ledger-mappings", "create-ledger-mappings", "get-ledger-mappings", "update-ledger-mappings", "delete-ledger-mappings", "list-notes", "create-notes", "get-notes", "update-notes", "delete-notes"], ...(HAS_IMPORT ? ['import'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_IMPORTS.map(n => `import-${n}`)];
8
+ const NESTED_IMPORTS = ["ledger-mappings", "notes", "cards"];
9
+ const SUBCOMMANDS = [...["list", "create", "get", "update", "delete", "list-ledger-mappings", "create-ledger-mappings", "get-ledger-mappings", "update-ledger-mappings", "delete-ledger-mappings", "list-notes", "create-notes", "get-notes", "update-notes", "delete-notes", "list-cards", "create-cards", "get-cards", "update-cards", "delete-cards"], ...(HAS_IMPORT ? ['import'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_IMPORTS.map(n => `import-${n}`)];
10
10
  const ENDPOINTS = {
11
- 'list': { method: 'GET', path: '/v1/credit-card-accounts', summary: 'Get all credit card accounts with pagination', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
12
- 'create': { method: 'POST', path: '/v1/credit-card-accounts', summary: 'Create a new credit card account (with optional notes)', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['last4', 'notes'] },
13
- 'get': { method: 'GET', path: '/v1/credit-card-accounts/{id}', summary: 'Get a credit card account by ID', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
14
- 'update': { method: 'PATCH', path: '/v1/credit-card-accounts/{id}', summary: 'Update a credit card account', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['last4', 'notes'] },
15
- 'delete': { method: 'DELETE', path: '/v1/credit-card-accounts/{id}', summary: 'Delete a credit card account', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
16
- 'list-ledger-mappings': { method: 'GET', path: '/v1/credit-card-accounts/{creditCardAccountId}/ledger-mappings', summary: 'Get all ledger mappings for a credit card account', pathParams: ['creditCardAccountId'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
17
- 'create-ledger-mappings': { method: 'POST', path: '/v1/credit-card-accounts/{creditCardAccountId}/ledger-mappings', summary: 'Create a ledger mapping for a credit card account', pathParams: ['creditCardAccountId'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
18
- 'get-ledger-mappings': { method: 'GET', path: '/v1/credit-card-accounts/{creditCardAccountId}/ledger-mappings/{mappingId}', summary: 'Get a ledger mapping by ID', pathParams: ['creditCardAccountId', 'mappingId'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
19
- 'update-ledger-mappings': { method: 'PATCH', path: '/v1/credit-card-accounts/{creditCardAccountId}/ledger-mappings/{mappingId}', summary: 'Update a ledger mapping', pathParams: ['creditCardAccountId', 'mappingId'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
20
- 'delete-ledger-mappings': { method: 'DELETE', path: '/v1/credit-card-accounts/{creditCardAccountId}/ledger-mappings/{mappingId}', summary: 'Delete a ledger mapping', pathParams: ['creditCardAccountId', 'mappingId'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
21
- 'list-notes': { method: 'GET', path: '/v1/credit-card-accounts/{creditCardAccountId}/notes', summary: 'Get all notes for a credit card account', pathParams: ['creditCardAccountId'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
22
- 'create-notes': { method: 'POST', path: '/v1/credit-card-accounts/{creditCardAccountId}/notes', summary: 'Create a note for a credit card account', pathParams: ['creditCardAccountId'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
23
- 'get-notes': { method: 'GET', path: '/v1/credit-card-accounts/{creditCardAccountId}/notes/{noteId}', summary: 'Get a specific note for a credit card account', pathParams: ['creditCardAccountId', 'noteId'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
24
- 'update-notes': { method: 'PATCH', path: '/v1/credit-card-accounts/{creditCardAccountId}/notes/{noteId}', summary: 'Update a note for a credit card account', pathParams: ['creditCardAccountId', 'noteId'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
25
- 'delete-notes': { method: 'DELETE', path: '/v1/credit-card-accounts/{creditCardAccountId}/notes/{noteId}', summary: 'Delete a note from a credit card account', pathParams: ['creditCardAccountId', 'noteId'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
11
+ 'list': { method: 'GET', path: '/v1/credit-card-accounts', summary: 'Get all credit card accounts with pagination', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
12
+ 'create': { method: 'POST', path: '/v1/credit-card-accounts', summary: 'Create a new credit card account (with optional notes)', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['notes'] },
13
+ 'get': { method: 'GET', path: '/v1/credit-card-accounts/{id}', summary: 'Get a credit card account by ID', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
14
+ 'update': { method: 'PATCH', path: '/v1/credit-card-accounts/{id}', summary: 'Update a credit card account', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['notes'] },
15
+ 'delete': { method: 'DELETE', path: '/v1/credit-card-accounts/{id}', summary: 'Delete a credit card account', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
16
+ 'list-ledger-mappings': { method: 'GET', path: '/v1/credit-card-accounts/{creditCardAccountId}/ledger-mappings', summary: 'Get all ledger mappings for a credit card account', pathParams: ['creditCardAccountId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
17
+ 'create-ledger-mappings': { method: 'POST', path: '/v1/credit-card-accounts/{creditCardAccountId}/ledger-mappings', summary: 'Create a ledger mapping for a credit card account', pathParams: ['creditCardAccountId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
18
+ 'get-ledger-mappings': { method: 'GET', path: '/v1/credit-card-accounts/{creditCardAccountId}/ledger-mappings/{mappingId}', summary: 'Get a ledger mapping by ID', pathParams: ['creditCardAccountId', 'mappingId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
19
+ 'update-ledger-mappings': { method: 'PATCH', path: '/v1/credit-card-accounts/{creditCardAccountId}/ledger-mappings/{mappingId}', summary: 'Update a ledger mapping', pathParams: ['creditCardAccountId', 'mappingId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
20
+ 'delete-ledger-mappings': { method: 'DELETE', path: '/v1/credit-card-accounts/{creditCardAccountId}/ledger-mappings/{mappingId}', summary: 'Delete a ledger mapping', pathParams: ['creditCardAccountId', 'mappingId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
21
+ 'list-notes': { method: 'GET', path: '/v1/credit-card-accounts/{creditCardAccountId}/notes', summary: 'Get all notes for a credit card account', pathParams: ['creditCardAccountId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
22
+ 'create-notes': { method: 'POST', path: '/v1/credit-card-accounts/{creditCardAccountId}/notes', summary: 'Create a note for a credit card account', pathParams: ['creditCardAccountId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
23
+ 'get-notes': { method: 'GET', path: '/v1/credit-card-accounts/{creditCardAccountId}/notes/{noteId}', summary: 'Get a specific note for a credit card account', pathParams: ['creditCardAccountId', 'noteId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
24
+ 'update-notes': { method: 'PATCH', path: '/v1/credit-card-accounts/{creditCardAccountId}/notes/{noteId}', summary: 'Update a note for a credit card account', pathParams: ['creditCardAccountId', 'noteId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
25
+ 'delete-notes': { method: 'DELETE', path: '/v1/credit-card-accounts/{creditCardAccountId}/notes/{noteId}', summary: 'Delete a note from a credit card account', pathParams: ['creditCardAccountId', 'noteId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
26
+ 'list-cards': { method: 'GET', path: '/v1/credit-card-accounts/{creditCardAccountId}/cards', summary: 'Get all cards on a credit card account', pathParams: ['creditCardAccountId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
27
+ 'create-cards': { method: 'POST', path: '/v1/credit-card-accounts/{creditCardAccountId}/cards', summary: 'Create a card on a credit card account', pathParams: ['creditCardAccountId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
28
+ 'get-cards': { method: 'GET', path: '/v1/credit-card-accounts/{creditCardAccountId}/cards/{cardId}', summary: 'Get a card by ID', pathParams: ['creditCardAccountId', 'cardId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
29
+ 'update-cards': { method: 'PATCH', path: '/v1/credit-card-accounts/{creditCardAccountId}/cards/{cardId}', summary: 'Update a card', pathParams: ['creditCardAccountId', 'cardId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
30
+ 'delete-cards': { method: 'DELETE', path: '/v1/credit-card-accounts/{creditCardAccountId}/cards/{cardId}', summary: 'Delete a manual card', pathParams: ['creditCardAccountId', 'cardId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
26
31
  };
27
32
  export default class CreditCardAccounts extends Command {
28
33
  static description = 'Manage credit card accounts';
@@ -48,6 +53,7 @@ export default class CreditCardAccounts extends Command {
48
53
  set: Flags.string({ char: 's', description: 'Set individual field (e.g. --set firstName=John)', multiple: true }),
49
54
  'mapping-id': Flags.string({ description: 'Path param: mappingId' }),
50
55
  'note-id': Flags.string({ description: 'Path param: noteId' }),
56
+ 'card-id': Flags.string({ description: 'Path param: cardId' }),
51
57
  page: Flags.string({ description: 'Page number' }),
52
58
  limit: Flags.string({ description: 'Items per page' }),
53
59
  include: Flags.string({ description: 'Relations to include' }),
@@ -276,10 +282,11 @@ export default class CreditCardAccounts extends Command {
276
282
  process.stderr.write('\n');
277
283
  }
278
284
  try {
279
- // Binary response: download to file
285
+ // Binary response: to -f / --file, or to stdout when it is redirected.
280
286
  if (endpoint.isBinaryResponse) {
281
- if (!flags.file) {
282
- this.error(`Endpoint returns a binary file. Specify the output path with -f / --file (e.g. -f report.xlsx).`);
287
+ const toStdout = !flags.file;
288
+ if (toStdout && process.stdout.isTTY) {
289
+ this.error(`Endpoint returns a binary file. Specify the output path with -f / --file (e.g. -f report.xlsx), or redirect stdout to a file.`);
283
290
  }
284
291
  const binResponse = await apiRequest(endpoint.method, apiPath, {
285
292
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
@@ -287,24 +294,55 @@ export default class CreditCardAccounts extends Command {
287
294
  contextName: flags.context,
288
295
  binary: true,
289
296
  });
290
- const fsModule = await import('node:fs');
291
297
  const binData = binResponse.data;
292
298
  const buffer = Buffer.isBuffer(binData)
293
299
  ? binData
294
300
  : Buffer.from(binData);
301
+ if (toStdout) {
302
+ // Nothing else may reach stdout here — it is the file.
303
+ process.stdout.write(buffer);
304
+ return;
305
+ }
306
+ const fsModule = await import('node:fs');
295
307
  fsModule.writeFileSync(flags.file, buffer);
296
308
  this.log(`Saved ${buffer.length} bytes to ${flags.file}`);
297
309
  return;
298
310
  }
299
- const response = await apiRequest(endpoint.method, apiPath, {
300
- params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
301
- data,
302
- contextName: flags.context,
303
- acceptAny: true,
304
- });
311
+ let response;
312
+ if (endpoint.hasBinaryVariant) {
313
+ // This route answers JSON or a file depending on the request body, so the
314
+ // bytes are read first and the response's own content-type decides. Asking
315
+ // for text here would corrupt an xlsx/pdf before anything could inspect it.
316
+ const raw = await apiRequest(endpoint.method, apiPath, {
317
+ params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
318
+ data,
319
+ contextName: flags.context,
320
+ binary: true,
321
+ });
322
+ const rawType = String(raw.headers['content-type'] || '').toLowerCase();
323
+ const rawData = raw.data;
324
+ const buffer = Buffer.isBuffer(rawData) ? rawData : Buffer.from(rawData);
325
+ if (!isTextualMediaType(rawType)) {
326
+ if (process.stdout.isTTY) {
327
+ this.error(`The API returned ${rawType || 'binary data'}. Redirect stdout to a file, e.g. ... > report.xlsx`);
328
+ }
329
+ // Nothing else may reach stdout here — it is the file.
330
+ process.stdout.write(buffer);
331
+ return;
332
+ }
333
+ response = { data: buffer.toString('utf8'), headers: raw.headers, status: raw.status };
334
+ }
335
+ else {
336
+ response = await apiRequest(endpoint.method, apiPath, {
337
+ params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
338
+ data,
339
+ contextName: flags.context,
340
+ acceptAny: true,
341
+ });
342
+ }
305
343
  // Check content-type for non-JSON responses (XML, text, etc.)
306
344
  const contentType = (response.headers['content-type'] || '').toLowerCase();
307
- if (contentType.includes('xml') || contentType.includes('text/')) {
345
+ if (isTextualMediaType(contentType) && !contentType.startsWith('application/json')) {
308
346
  // Output raw content for XML/text responses (CSV, plain, HTML, etc.)
309
347
  this.log(String(response.data));
310
348
  return;