@aderis-erp/cli 1.1.4 → 1.1.5

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 (85) hide show
  1. package/dist/commands/access-groups.js +42 -8
  2. package/dist/commands/account-transaction-allocations.js +38 -4
  3. package/dist/commands/account-transactions.d.ts +2 -0
  4. package/dist/commands/account-transactions.js +68 -32
  5. package/dist/commands/accounts.js +46 -12
  6. package/dist/commands/agent-platform.js +37 -3
  7. package/dist/commands/agent-runs.js +49 -15
  8. package/dist/commands/analytics.js +39 -5
  9. package/dist/commands/api-clients.js +47 -13
  10. package/dist/commands/api-keys.js +40 -6
  11. package/dist/commands/async-jobs.js +45 -11
  12. package/dist/commands/audit-log.js +38 -4
  13. package/dist/commands/bank-accounts.js +54 -20
  14. package/dist/commands/bank-integrations.js +56 -22
  15. package/dist/commands/business-partners.js +77 -43
  16. package/dist/commands/chat.js +43 -9
  17. package/dist/commands/credit-card-accounts.js +57 -23
  18. package/dist/commands/currencies.js +37 -3
  19. package/dist/commands/custody-accounts.js +41 -7
  20. package/dist/commands/dashboards.js +52 -18
  21. package/dist/commands/delivery-notes.js +69 -35
  22. package/dist/commands/document-checksums.d.ts +1 -0
  23. package/dist/commands/document-checksums.js +38 -3
  24. package/dist/commands/dunning-configs.js +41 -7
  25. package/dist/commands/dunning-reminders.js +44 -10
  26. package/dist/commands/dunning-runs.js +37 -3
  27. package/dist/commands/email-transactions.js +39 -5
  28. package/dist/commands/employees.js +77 -43
  29. package/dist/commands/entity-configurations.js +39 -5
  30. package/dist/commands/event-mapping-rules.js +37 -3
  31. package/dist/commands/event-processor-mappings.js +41 -7
  32. package/dist/commands/files.js +48 -14
  33. package/dist/commands/financial-instrument-transactions.d.ts +56 -0
  34. package/dist/commands/financial-instrument-transactions.js +1369 -0
  35. package/dist/commands/financial-instrument-types.js +41 -7
  36. package/dist/commands/financial-instruments.js +41 -7
  37. package/dist/commands/health.js +41 -7
  38. package/dist/commands/imports.js +39 -5
  39. package/dist/commands/inbox.js +42 -8
  40. package/dist/commands/integrations.js +39 -5
  41. package/dist/commands/item-categories.js +41 -7
  42. package/dist/commands/items.js +42 -8
  43. package/dist/commands/journal-lines.js +37 -3
  44. package/dist/commands/journals.js +61 -27
  45. package/dist/commands/kpis.js +44 -10
  46. package/dist/commands/ledgers.js +83 -49
  47. package/dist/commands/my-account.js +54 -20
  48. package/dist/commands/notifications.js +37 -3
  49. package/dist/commands/office-locations.js +41 -7
  50. package/dist/commands/payment-methods.js +46 -12
  51. package/dist/commands/payment-runs.js +54 -20
  52. package/dist/commands/payment-terms.js +42 -8
  53. package/dist/commands/payments.js +43 -9
  54. package/dist/commands/payroll-runs.js +71 -37
  55. package/dist/commands/permissions.js +37 -3
  56. package/dist/commands/price-lists.js +48 -14
  57. package/dist/commands/processor-mappings.js +40 -6
  58. package/dist/commands/processors.js +46 -12
  59. package/dist/commands/purchase-order-types.js +41 -7
  60. package/dist/commands/purchase-orders.js +60 -26
  61. package/dist/commands/reports.js +43 -8
  62. package/dist/commands/roles.js +40 -6
  63. package/dist/commands/salary-types.js +41 -7
  64. package/dist/commands/sales-invoices.js +85 -51
  65. package/dist/commands/sales-orders.js +69 -35
  66. package/dist/commands/saved-views.js +39 -5
  67. package/dist/commands/scheduled-processor-mappings.js +41 -7
  68. package/dist/commands/schedules.js +45 -11
  69. package/dist/commands/settings.js +38 -4
  70. package/dist/commands/tags.js +42 -8
  71. package/dist/commands/tax-codes.js +44 -10
  72. package/dist/commands/tax-matrix.js +41 -7
  73. package/dist/commands/templates.js +49 -15
  74. package/dist/commands/tenants.js +44 -10
  75. package/dist/commands/tools.js +38 -4
  76. package/dist/commands/users.js +58 -24
  77. package/dist/commands/vat-declarations.js +61 -27
  78. package/dist/commands/vendor-invoices.js +81 -47
  79. package/dist/commands/webhooks.js +43 -9
  80. package/dist/commands/work-summary.js +37 -3
  81. package/dist/lib/paths.d.ts +8 -0
  82. package/dist/lib/paths.js +16 -0
  83. package/dist/lib/production-guard.js +2 -0
  84. package/oclif.manifest.json +199 -2
  85. package/package.json +9 -3
@@ -1,6 +1,7 @@
1
1
  import { Command, Args, Flags } from '@oclif/core';
2
2
  import { apiRequest, ApiError, isTextualMediaType, normalizeSortBy } from '../lib/api-client.js';
3
3
  import { getCurrentContext, getContext, getBaseUrls, hasCredentials, missingCredentialsMessage } from '../lib/config.js';
4
+ import { resolveUserPath } from '../lib/paths.js';
4
5
  // 'upsert' loops create/update from the CLI, one request per record. It exists
5
6
  // only where the API has no import endpoint of its own; those keep the name
6
7
  // 'import' and are listed in SERVER_IMPORTS.
@@ -11,7 +12,7 @@ const SERVER_IMPORTS = [];
11
12
  const NESTED_UPSERTS = [];
12
13
  const SUBCOMMANDS = [...["usage"], ...(HAS_UPSERT ? ['upsert'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_UPSERTS.map(n => `upsert-${n}`)];
13
14
  const ENDPOINTS = {
14
- 'usage': { method: 'GET', path: '/v1/agent-platform/usage', summary: 'Fetch tenant LLM usage report (agents-platform pass-through).', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
15
+ 'usage': { method: 'GET', path: '/v1/agent-platform/usage', summary: 'Fetch tenant LLM usage report (agents-platform pass-through).', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
15
16
  };
16
17
  export default class AgentPlatform extends Command {
17
18
  static description = 'Manage agent platform';
@@ -26,7 +27,7 @@ export default class AgentPlatform extends Command {
26
27
  table: Flags.boolean({ char: 't', description: 'Output as table (shortcut for --output table)' }),
27
28
  csv: Flags.boolean({ description: 'Output as CSV (shortcut for --output csv)' }),
28
29
  data: Flags.string({ char: 'd', description: 'JSON data for request body' }),
29
- file: Flags.string({ char: 'f', description: 'Read request body from file' }),
30
+ file: Flags.string({ char: 'f', description: 'Read request body from file', parse: async (input) => resolveUserPath(input) }),
30
31
  set: Flags.string({ char: 's', description: 'Set individual field (e.g. --set firstName=John)', multiple: true }),
31
32
  page: Flags.string({ description: 'Page number' }),
32
33
  limit: Flags.string({ description: 'Items per page' }),
@@ -157,6 +158,17 @@ export default class AgentPlatform extends Command {
157
158
  }
158
159
  }
159
160
  }
161
+ // Flags generated from this endpoint's own parameters. Raw-body endpoints
162
+ // have them: their body is not JSON, so --set cannot carry a value there.
163
+ for (const param of endpoint.specParams) {
164
+ const value = flags[param.flag];
165
+ if (value === undefined || value === '') {
166
+ if (param.required)
167
+ this.error(`Missing required flag: --${param.flag}`);
168
+ continue;
169
+ }
170
+ queryParams[param.name] = value;
171
+ }
160
172
  let data;
161
173
  if (endpoint.isMultipart) {
162
174
  if (!flags.file)
@@ -177,6 +189,25 @@ export default class AgentPlatform extends Command {
177
189
  }
178
190
  data = form;
179
191
  }
192
+ else if (endpoint.bodyMediaType) {
193
+ // The body is not JSON (today only application/x-ndjson): it is sent
194
+ // verbatim, under the content type the endpoint asks for.
195
+ if (flags.file) {
196
+ const fsModule = await import('node:fs');
197
+ try {
198
+ data = fsModule.readFileSync(flags.file, 'utf-8');
199
+ }
200
+ catch {
201
+ this.error(`Failed to read file: ${flags.file}`);
202
+ }
203
+ }
204
+ else if (flags.data) {
205
+ data = flags.data;
206
+ }
207
+ else {
208
+ this.error(`This endpoint takes its body as ${endpoint.bodyMediaType}. Pass the file with -f / --file, or the text with --data.`);
209
+ }
210
+ }
180
211
  else if (endpoint.hasBody) {
181
212
  // For binary-response endpoints, --file is the output path, not a JSON
182
213
  // input file — skip the read-as-body branch.
@@ -272,7 +303,7 @@ export default class AgentPlatform extends Command {
272
303
  process.stderr.write(`Body: [multipart/form-data file upload]\n`);
273
304
  }
274
305
  else if (data) {
275
- process.stderr.write(`Body: ${JSON.stringify(data, null, 2)}\n`);
306
+ process.stderr.write(`Body: ${typeof data === 'string' ? data : JSON.stringify(data, null, 2)}\n`);
276
307
  }
277
308
  process.stderr.write('\n');
278
309
  }
@@ -286,6 +317,7 @@ export default class AgentPlatform extends Command {
286
317
  const binResponse = await apiRequest(endpoint.method, apiPath, {
287
318
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
288
319
  data,
320
+ contentType: endpoint.bodyMediaType || undefined,
289
321
  contextName: flags.context,
290
322
  binary: true,
291
323
  });
@@ -311,6 +343,7 @@ export default class AgentPlatform extends Command {
311
343
  const raw = await apiRequest(endpoint.method, apiPath, {
312
344
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
313
345
  data,
346
+ contentType: endpoint.bodyMediaType || undefined,
314
347
  contextName: flags.context,
315
348
  binary: true,
316
349
  });
@@ -331,6 +364,7 @@ export default class AgentPlatform extends Command {
331
364
  response = await apiRequest(endpoint.method, apiPath, {
332
365
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
333
366
  data,
367
+ contentType: endpoint.bodyMediaType || undefined,
334
368
  contextName: flags.context,
335
369
  acceptAny: true,
336
370
  });
@@ -1,6 +1,7 @@
1
1
  import { Command, Args, Flags } from '@oclif/core';
2
2
  import { apiRequest, ApiError, isTextualMediaType, normalizeSortBy } from '../lib/api-client.js';
3
3
  import { getCurrentContext, getContext, getBaseUrls, hasCredentials, missingCredentialsMessage } from '../lib/config.js';
4
+ import { resolveUserPath } from '../lib/paths.js';
4
5
  // 'upsert' loops create/update from the CLI, one request per record. It exists
5
6
  // only where the API has no import endpoint of its own; those keep the name
6
7
  // 'import' and are listed in SERVER_IMPORTS.
@@ -11,19 +12,19 @@ const SERVER_IMPORTS = [];
11
12
  const NESTED_UPSERTS = [];
12
13
  const SUBCOMMANDS = [...["sources", "sources-counts", "sources-email", "get-documents", "documents-process-anyway", "documents-discard", "documents-send-to-inbox", "documents-attach", "get-runs", "get-runs-runId", "runs-answer", "runs-restart", "runs-discard"], ...(HAS_UPSERT ? ['upsert'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_UPSERTS.map(n => `upsert-${n}`)];
13
14
  const ENDPOINTS = {
14
- 'sources': { method: 'GET', path: '/v1/agent-runs/sources', summary: 'List v2 sources: arrivals with their files, documents, results and actions (agents-platform pass-through).', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
15
- 'sources-counts': { method: 'GET', path: '/v1/agent-runs/sources/counts', summary: 'Counts for the v2 status tabs (agents-platform pass-through).', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
16
- 'sources-email': { method: 'GET', path: '/v1/agent-runs/sources/{sourceId}/email', summary: 'The email behind a v2 source, for the View email dialog (agents-platform pass-through).', pathParams: ['sourceId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
17
- 'get-documents': { method: 'GET', path: '/v1/agent-runs/documents/{documentId}', summary: 'One v2 document: header, question, results, facts, timeline, files, actions, technical (agents-platform pass-through).', pathParams: ['documentId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
18
- 'documents-process-anyway': { method: 'POST', path: '/v1/agent-runs/documents/{documentId}/process-anyway', summary: 'Process a duplicate document anyway (agents-platform pass-through).', pathParams: ['documentId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
19
- 'documents-discard': { method: 'POST', path: '/v1/agent-runs/documents/{documentId}/discard', summary: 'Discard a document (agents-platform pass-through).', pathParams: ['documentId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
20
- 'documents-send-to-inbox': { method: 'POST', path: '/v1/agent-runs/documents/{documentId}/send-to-inbox', summary: 'Stop processing a document and file it in the Inbox.', pathParams: ['documentId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
21
- 'documents-attach': { method: 'POST', path: '/v1/agent-runs/documents/{documentId}/attach', summary: 'Attach a document to another record as supporting files.', pathParams: ['documentId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
22
- 'get-runs': { method: 'GET', path: '/v1/agent-runs/runs', summary: 'List v2 runs, the Runs tab worklist (agents-platform pass-through).', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
23
- 'get-runs-runId': { method: 'GET', path: '/v1/agent-runs/runs/{runId}', summary: 'One v2 run detail (agents-platform pass-through).', pathParams: ['runId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
24
- 'runs-answer': { method: 'POST', path: '/v1/agent-runs/runs/{runId}/answer', summary: 'Answer a run stopped for review (agents-platform pass-through).', pathParams: ['runId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
25
- 'runs-restart': { method: 'POST', path: '/v1/agent-runs/runs/{runId}/restart', summary: 'Restart a stopped run (agents-platform pass-through).', pathParams: ['runId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
26
- 'runs-discard': { method: 'POST', path: '/v1/agent-runs/runs/{runId}/discard', summary: 'Discard a stopped run (agents-platform pass-through).', pathParams: ['runId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
15
+ 'sources': { method: 'GET', path: '/v1/agent-runs/sources', summary: 'List v2 sources: arrivals with their files, documents, results and actions (agents-platform pass-through).', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
16
+ 'sources-counts': { method: 'GET', path: '/v1/agent-runs/sources/counts', summary: 'Counts for the v2 status tabs (agents-platform pass-through).', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
17
+ 'sources-email': { method: 'GET', path: '/v1/agent-runs/sources/{sourceId}/email', summary: 'The email behind a v2 source, for the View email dialog (agents-platform pass-through).', pathParams: ['sourceId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
18
+ 'get-documents': { method: 'GET', path: '/v1/agent-runs/documents/{documentId}', summary: 'One v2 document: header, question, results, facts, timeline, files, actions, technical (agents-platform pass-through).', pathParams: ['documentId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
19
+ 'documents-process-anyway': { method: 'POST', path: '/v1/agent-runs/documents/{documentId}/process-anyway', summary: 'Process a duplicate document anyway (agents-platform pass-through).', pathParams: ['documentId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
20
+ 'documents-discard': { method: 'POST', path: '/v1/agent-runs/documents/{documentId}/discard', summary: 'Discard a document (agents-platform pass-through).', pathParams: ['documentId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
21
+ 'documents-send-to-inbox': { method: 'POST', path: '/v1/agent-runs/documents/{documentId}/send-to-inbox', summary: 'Stop processing a document and file it in the Inbox.', pathParams: ['documentId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
22
+ 'documents-attach': { method: 'POST', path: '/v1/agent-runs/documents/{documentId}/attach', summary: 'Attach a document to another record as supporting files.', pathParams: ['documentId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
23
+ 'get-runs': { method: 'GET', path: '/v1/agent-runs/runs', summary: 'List v2 runs, the Runs tab worklist (agents-platform pass-through).', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
24
+ 'get-runs-runId': { method: 'GET', path: '/v1/agent-runs/runs/{runId}', summary: 'One v2 run detail (agents-platform pass-through).', pathParams: ['runId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
25
+ 'runs-answer': { method: 'POST', path: '/v1/agent-runs/runs/{runId}/answer', summary: 'Answer a run stopped for review (agents-platform pass-through).', pathParams: ['runId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
26
+ 'runs-restart': { method: 'POST', path: '/v1/agent-runs/runs/{runId}/restart', summary: 'Restart a stopped run (agents-platform pass-through).', pathParams: ['runId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
27
+ 'runs-discard': { method: 'POST', path: '/v1/agent-runs/runs/{runId}/discard', summary: 'Discard a stopped run (agents-platform pass-through).', pathParams: ['runId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
27
28
  };
28
29
  export default class AgentRuns extends Command {
29
30
  static description = 'Manage agent runs';
@@ -38,7 +39,7 @@ export default class AgentRuns extends Command {
38
39
  table: Flags.boolean({ char: 't', description: 'Output as table (shortcut for --output table)' }),
39
40
  csv: Flags.boolean({ description: 'Output as CSV (shortcut for --output csv)' }),
40
41
  data: Flags.string({ char: 'd', description: 'JSON data for request body' }),
41
- file: Flags.string({ char: 'f', description: 'Read request body from file' }),
42
+ file: Flags.string({ char: 'f', description: 'Read request body from file', parse: async (input) => resolveUserPath(input) }),
42
43
  set: Flags.string({ char: 's', description: 'Set individual field (e.g. --set firstName=John)', multiple: true }),
43
44
  page: Flags.string({ description: 'Page number' }),
44
45
  limit: Flags.string({ description: 'Items per page' }),
@@ -169,6 +170,17 @@ export default class AgentRuns extends Command {
169
170
  }
170
171
  }
171
172
  }
173
+ // Flags generated from this endpoint's own parameters. Raw-body endpoints
174
+ // have them: their body is not JSON, so --set cannot carry a value there.
175
+ for (const param of endpoint.specParams) {
176
+ const value = flags[param.flag];
177
+ if (value === undefined || value === '') {
178
+ if (param.required)
179
+ this.error(`Missing required flag: --${param.flag}`);
180
+ continue;
181
+ }
182
+ queryParams[param.name] = value;
183
+ }
172
184
  let data;
173
185
  if (endpoint.isMultipart) {
174
186
  if (!flags.file)
@@ -189,6 +201,25 @@ export default class AgentRuns extends Command {
189
201
  }
190
202
  data = form;
191
203
  }
204
+ else if (endpoint.bodyMediaType) {
205
+ // The body is not JSON (today only application/x-ndjson): it is sent
206
+ // verbatim, under the content type the endpoint asks for.
207
+ if (flags.file) {
208
+ const fsModule = await import('node:fs');
209
+ try {
210
+ data = fsModule.readFileSync(flags.file, 'utf-8');
211
+ }
212
+ catch {
213
+ this.error(`Failed to read file: ${flags.file}`);
214
+ }
215
+ }
216
+ else if (flags.data) {
217
+ data = flags.data;
218
+ }
219
+ else {
220
+ this.error(`This endpoint takes its body as ${endpoint.bodyMediaType}. Pass the file with -f / --file, or the text with --data.`);
221
+ }
222
+ }
192
223
  else if (endpoint.hasBody) {
193
224
  // For binary-response endpoints, --file is the output path, not a JSON
194
225
  // input file — skip the read-as-body branch.
@@ -284,7 +315,7 @@ export default class AgentRuns extends Command {
284
315
  process.stderr.write(`Body: [multipart/form-data file upload]\n`);
285
316
  }
286
317
  else if (data) {
287
- process.stderr.write(`Body: ${JSON.stringify(data, null, 2)}\n`);
318
+ process.stderr.write(`Body: ${typeof data === 'string' ? data : JSON.stringify(data, null, 2)}\n`);
288
319
  }
289
320
  process.stderr.write('\n');
290
321
  }
@@ -298,6 +329,7 @@ export default class AgentRuns extends Command {
298
329
  const binResponse = await apiRequest(endpoint.method, apiPath, {
299
330
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
300
331
  data,
332
+ contentType: endpoint.bodyMediaType || undefined,
301
333
  contextName: flags.context,
302
334
  binary: true,
303
335
  });
@@ -323,6 +355,7 @@ export default class AgentRuns extends Command {
323
355
  const raw = await apiRequest(endpoint.method, apiPath, {
324
356
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
325
357
  data,
358
+ contentType: endpoint.bodyMediaType || undefined,
326
359
  contextName: flags.context,
327
360
  binary: true,
328
361
  });
@@ -343,6 +376,7 @@ export default class AgentRuns extends Command {
343
376
  response = await apiRequest(endpoint.method, apiPath, {
344
377
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
345
378
  data,
379
+ contentType: endpoint.bodyMediaType || undefined,
346
380
  contextName: flags.context,
347
381
  acceptAny: true,
348
382
  });
@@ -1,6 +1,7 @@
1
1
  import { Command, Args, Flags } from '@oclif/core';
2
2
  import { apiRequest, ApiError, isTextualMediaType, normalizeSortBy } from '../lib/api-client.js';
3
3
  import { getCurrentContext, getContext, getBaseUrls, hasCredentials, missingCredentialsMessage } from '../lib/config.js';
4
+ import { resolveUserPath } from '../lib/paths.js';
4
5
  // 'upsert' loops create/update from the CLI, one request per record. It exists
5
6
  // only where the API has no import endpoint of its own; those keep the name
6
7
  // 'import' and are listed in SERVER_IMPORTS.
@@ -11,9 +12,9 @@ const SERVER_IMPORTS = [];
11
12
  const NESTED_UPSERTS = [];
12
13
  const SUBCOMMANDS = [...["query", "views", "views-name"], ...(HAS_UPSERT ? ['upsert'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_UPSERTS.map(n => `upsert-${n}`)];
13
14
  const ENDPOINTS = {
14
- 'query': { method: 'POST', path: '/v1/analytics/query', summary: 'Run a structured aggregate query over a reporting view', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['measures', 'dimensions', 'filters', 'order'] },
15
- 'views': { method: 'GET', path: '/v1/analytics/views', summary: 'List the analytics views for discovery', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
16
- 'views-name': { method: 'GET', path: '/v1/analytics/views/{name}', summary: 'Describe one analytics view with full metadata', pathParams: ['name'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
15
+ 'query': { method: 'POST', path: '/v1/analytics/query', summary: 'Run a structured aggregate query over a reporting view', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['measures', 'dimensions', 'filters', 'order'], bodyMediaType: '', specParams: [] },
16
+ 'views': { method: 'GET', path: '/v1/analytics/views', summary: 'List the analytics views for discovery', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
17
+ 'views-name': { method: 'GET', path: '/v1/analytics/views/{name}', summary: 'Describe one analytics view with full metadata', pathParams: ['name'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
17
18
  };
18
19
  export default class Analytics extends Command {
19
20
  static description = 'Manage analytics';
@@ -28,7 +29,7 @@ export default class Analytics extends Command {
28
29
  table: Flags.boolean({ char: 't', description: 'Output as table (shortcut for --output table)' }),
29
30
  csv: Flags.boolean({ description: 'Output as CSV (shortcut for --output csv)' }),
30
31
  data: Flags.string({ char: 'd', description: 'JSON data for request body' }),
31
- file: Flags.string({ char: 'f', description: 'Read request body from file' }),
32
+ file: Flags.string({ char: 'f', description: 'Read request body from file', parse: async (input) => resolveUserPath(input) }),
32
33
  set: Flags.string({ char: 's', description: 'Set individual field (e.g. --set firstName=John)', multiple: true }),
33
34
  page: Flags.string({ description: 'Page number' }),
34
35
  limit: Flags.string({ description: 'Items per page' }),
@@ -159,6 +160,17 @@ export default class Analytics extends Command {
159
160
  }
160
161
  }
161
162
  }
163
+ // Flags generated from this endpoint's own parameters. Raw-body endpoints
164
+ // have them: their body is not JSON, so --set cannot carry a value there.
165
+ for (const param of endpoint.specParams) {
166
+ const value = flags[param.flag];
167
+ if (value === undefined || value === '') {
168
+ if (param.required)
169
+ this.error(`Missing required flag: --${param.flag}`);
170
+ continue;
171
+ }
172
+ queryParams[param.name] = value;
173
+ }
162
174
  let data;
163
175
  if (endpoint.isMultipart) {
164
176
  if (!flags.file)
@@ -179,6 +191,25 @@ export default class Analytics extends Command {
179
191
  }
180
192
  data = form;
181
193
  }
194
+ else if (endpoint.bodyMediaType) {
195
+ // The body is not JSON (today only application/x-ndjson): it is sent
196
+ // verbatim, under the content type the endpoint asks for.
197
+ if (flags.file) {
198
+ const fsModule = await import('node:fs');
199
+ try {
200
+ data = fsModule.readFileSync(flags.file, 'utf-8');
201
+ }
202
+ catch {
203
+ this.error(`Failed to read file: ${flags.file}`);
204
+ }
205
+ }
206
+ else if (flags.data) {
207
+ data = flags.data;
208
+ }
209
+ else {
210
+ this.error(`This endpoint takes its body as ${endpoint.bodyMediaType}. Pass the file with -f / --file, or the text with --data.`);
211
+ }
212
+ }
182
213
  else if (endpoint.hasBody) {
183
214
  // For binary-response endpoints, --file is the output path, not a JSON
184
215
  // input file — skip the read-as-body branch.
@@ -274,7 +305,7 @@ export default class Analytics extends Command {
274
305
  process.stderr.write(`Body: [multipart/form-data file upload]\n`);
275
306
  }
276
307
  else if (data) {
277
- process.stderr.write(`Body: ${JSON.stringify(data, null, 2)}\n`);
308
+ process.stderr.write(`Body: ${typeof data === 'string' ? data : JSON.stringify(data, null, 2)}\n`);
278
309
  }
279
310
  process.stderr.write('\n');
280
311
  }
@@ -288,6 +319,7 @@ export default class Analytics extends Command {
288
319
  const binResponse = await apiRequest(endpoint.method, apiPath, {
289
320
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
290
321
  data,
322
+ contentType: endpoint.bodyMediaType || undefined,
291
323
  contextName: flags.context,
292
324
  binary: true,
293
325
  });
@@ -313,6 +345,7 @@ export default class Analytics extends Command {
313
345
  const raw = await apiRequest(endpoint.method, apiPath, {
314
346
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
315
347
  data,
348
+ contentType: endpoint.bodyMediaType || undefined,
316
349
  contextName: flags.context,
317
350
  binary: true,
318
351
  });
@@ -333,6 +366,7 @@ export default class Analytics extends Command {
333
366
  response = await apiRequest(endpoint.method, apiPath, {
334
367
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
335
368
  data,
369
+ contentType: endpoint.bodyMediaType || undefined,
336
370
  contextName: flags.context,
337
371
  acceptAny: true,
338
372
  });
@@ -1,6 +1,7 @@
1
1
  import { Command, Args, Flags } from '@oclif/core';
2
2
  import { apiRequest, ApiError, isTextualMediaType, normalizeSortBy } from '../lib/api-client.js';
3
3
  import { getCurrentContext, getContext, getBaseUrls, hasCredentials, missingCredentialsMessage } from '../lib/config.js';
4
+ import { resolveUserPath } from '../lib/paths.js';
4
5
  // 'upsert' loops create/update from the CLI, one request per record. It exists
5
6
  // only where the API has no import endpoint of its own; those keep the name
6
7
  // 'import' and are listed in SERVER_IMPORTS.
@@ -11,17 +12,17 @@ const SERVER_IMPORTS = [];
11
12
  const NESTED_UPSERTS = [];
12
13
  const SUBCOMMANDS = [...["list", "create", "api-clients-disable-all", "get", "list-api-clients-secrets", "create-api-clients-secrets", "delete-api-clients-secrets", "api-clients-disable", "list-api-clients-api-keys", "create-api-clients-api-keys", "delete-api-clients-api-keys"], ...(HAS_UPSERT ? ['upsert'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_UPSERTS.map(n => `upsert-${n}`)];
13
14
  const ENDPOINTS = {
14
- 'list': { method: 'GET', path: '/v1/auth/api-clients', summary: 'List current tenant API clients, including disabled ones', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
15
- 'create': { method: 'POST', path: '/v1/auth/api-clients', summary: 'Create an API client in the current tenant', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['permissions'] },
16
- 'api-clients-disable-all': { method: 'POST', path: '/v1/auth/api-clients/disable-all', summary: 'Disable current tenant API clients', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
17
- 'get': { method: 'GET', path: '/v1/auth/api-clients/{apiClientId}', summary: 'Read one current tenant API client', pathParams: ['apiClientId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
18
- 'list-api-clients-secrets': { method: 'GET', path: '/v1/auth/api-clients/{apiClientId}/secrets', summary: 'List API client secret metadata', pathParams: ['apiClientId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
19
- 'create-api-clients-secrets': { method: 'POST', path: '/v1/auth/api-clients/{apiClientId}/secrets', summary: 'Issue an API client secret', pathParams: ['apiClientId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
20
- 'delete-api-clients-secrets': { method: 'DELETE', path: '/v1/auth/api-clients/{apiClientId}/secrets/{secretId}', summary: 'Revoke one API client secret', pathParams: ['apiClientId', 'secretId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
21
- 'api-clients-disable': { method: 'POST', path: '/v1/auth/api-clients/{apiClientId}/disable', summary: 'Disable an API client and end its sessions', pathParams: ['apiClientId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
22
- 'list-api-clients-api-keys': { method: 'GET', path: '/v1/auth/api-clients/{apiClientId}/api-keys', summary: 'List integration API key metadata', pathParams: ['apiClientId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
23
- 'create-api-clients-api-keys': { method: 'POST', path: '/v1/auth/api-clients/{apiClientId}/api-keys', summary: 'Issue an integration API key', pathParams: ['apiClientId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
24
- 'delete-api-clients-api-keys': { method: 'DELETE', path: '/v1/auth/api-clients/{apiClientId}/api-keys/{keyId}', summary: 'Revoke one integration API key', pathParams: ['apiClientId', 'keyId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
15
+ 'list': { method: 'GET', path: '/v1/auth/api-clients', summary: 'List current tenant API clients, including disabled ones', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
16
+ 'create': { method: 'POST', path: '/v1/auth/api-clients', summary: 'Create an API client in the current tenant', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['permissions'], bodyMediaType: '', specParams: [] },
17
+ 'api-clients-disable-all': { method: 'POST', path: '/v1/auth/api-clients/disable-all', summary: 'Disable current tenant API clients', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
18
+ 'get': { method: 'GET', path: '/v1/auth/api-clients/{apiClientId}', summary: 'Read one current tenant API client', pathParams: ['apiClientId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
19
+ 'list-api-clients-secrets': { method: 'GET', path: '/v1/auth/api-clients/{apiClientId}/secrets', summary: 'List API client secret metadata', pathParams: ['apiClientId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
20
+ 'create-api-clients-secrets': { method: 'POST', path: '/v1/auth/api-clients/{apiClientId}/secrets', summary: 'Issue an API client secret', pathParams: ['apiClientId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
21
+ 'delete-api-clients-secrets': { method: 'DELETE', path: '/v1/auth/api-clients/{apiClientId}/secrets/{secretId}', summary: 'Revoke one API client secret', pathParams: ['apiClientId', 'secretId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
22
+ 'api-clients-disable': { method: 'POST', path: '/v1/auth/api-clients/{apiClientId}/disable', summary: 'Disable an API client and end its sessions', pathParams: ['apiClientId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
23
+ 'list-api-clients-api-keys': { method: 'GET', path: '/v1/auth/api-clients/{apiClientId}/api-keys', summary: 'List integration API key metadata', pathParams: ['apiClientId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
24
+ 'create-api-clients-api-keys': { method: 'POST', path: '/v1/auth/api-clients/{apiClientId}/api-keys', summary: 'Issue an integration API key', pathParams: ['apiClientId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
25
+ 'delete-api-clients-api-keys': { method: 'DELETE', path: '/v1/auth/api-clients/{apiClientId}/api-keys/{keyId}', summary: 'Revoke one integration API key', pathParams: ['apiClientId', 'keyId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
25
26
  };
26
27
  export default class ApiClients extends Command {
27
28
  static description = 'Manage api clients';
@@ -41,7 +42,7 @@ export default class ApiClients extends Command {
41
42
  table: Flags.boolean({ char: 't', description: 'Output as table (shortcut for --output table)' }),
42
43
  csv: Flags.boolean({ description: 'Output as CSV (shortcut for --output csv)' }),
43
44
  data: Flags.string({ char: 'd', description: 'JSON data for request body' }),
44
- file: Flags.string({ char: 'f', description: 'Read request body from file' }),
45
+ file: Flags.string({ char: 'f', description: 'Read request body from file', parse: async (input) => resolveUserPath(input) }),
45
46
  set: Flags.string({ char: 's', description: 'Set individual field (e.g. --set firstName=John)', multiple: true }),
46
47
  'secret-id': Flags.string({ description: 'Path param: secretId' }),
47
48
  'key-id': Flags.string({ description: 'Path param: keyId' }),
@@ -174,6 +175,17 @@ export default class ApiClients extends Command {
174
175
  }
175
176
  }
176
177
  }
178
+ // Flags generated from this endpoint's own parameters. Raw-body endpoints
179
+ // have them: their body is not JSON, so --set cannot carry a value there.
180
+ for (const param of endpoint.specParams) {
181
+ const value = flags[param.flag];
182
+ if (value === undefined || value === '') {
183
+ if (param.required)
184
+ this.error(`Missing required flag: --${param.flag}`);
185
+ continue;
186
+ }
187
+ queryParams[param.name] = value;
188
+ }
177
189
  let data;
178
190
  if (endpoint.isMultipart) {
179
191
  if (!flags.file)
@@ -194,6 +206,25 @@ export default class ApiClients extends Command {
194
206
  }
195
207
  data = form;
196
208
  }
209
+ else if (endpoint.bodyMediaType) {
210
+ // The body is not JSON (today only application/x-ndjson): it is sent
211
+ // verbatim, under the content type the endpoint asks for.
212
+ if (flags.file) {
213
+ const fsModule = await import('node:fs');
214
+ try {
215
+ data = fsModule.readFileSync(flags.file, 'utf-8');
216
+ }
217
+ catch {
218
+ this.error(`Failed to read file: ${flags.file}`);
219
+ }
220
+ }
221
+ else if (flags.data) {
222
+ data = flags.data;
223
+ }
224
+ else {
225
+ this.error(`This endpoint takes its body as ${endpoint.bodyMediaType}. Pass the file with -f / --file, or the text with --data.`);
226
+ }
227
+ }
197
228
  else if (endpoint.hasBody) {
198
229
  // For binary-response endpoints, --file is the output path, not a JSON
199
230
  // input file — skip the read-as-body branch.
@@ -289,7 +320,7 @@ export default class ApiClients extends Command {
289
320
  process.stderr.write(`Body: [multipart/form-data file upload]\n`);
290
321
  }
291
322
  else if (data) {
292
- process.stderr.write(`Body: ${JSON.stringify(data, null, 2)}\n`);
323
+ process.stderr.write(`Body: ${typeof data === 'string' ? data : JSON.stringify(data, null, 2)}\n`);
293
324
  }
294
325
  process.stderr.write('\n');
295
326
  }
@@ -303,6 +334,7 @@ export default class ApiClients extends Command {
303
334
  const binResponse = await apiRequest(endpoint.method, apiPath, {
304
335
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
305
336
  data,
337
+ contentType: endpoint.bodyMediaType || undefined,
306
338
  contextName: flags.context,
307
339
  binary: true,
308
340
  });
@@ -328,6 +360,7 @@ export default class ApiClients extends Command {
328
360
  const raw = await apiRequest(endpoint.method, apiPath, {
329
361
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
330
362
  data,
363
+ contentType: endpoint.bodyMediaType || undefined,
331
364
  contextName: flags.context,
332
365
  binary: true,
333
366
  });
@@ -348,6 +381,7 @@ export default class ApiClients extends Command {
348
381
  response = await apiRequest(endpoint.method, apiPath, {
349
382
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
350
383
  data,
384
+ contentType: endpoint.bodyMediaType || undefined,
351
385
  contextName: flags.context,
352
386
  acceptAny: true,
353
387
  });
@@ -1,6 +1,7 @@
1
1
  import { Command, Args, Flags } from '@oclif/core';
2
2
  import { apiRequest, ApiError, isTextualMediaType, normalizeSortBy } from '../lib/api-client.js';
3
3
  import { getCurrentContext, getContext, getBaseUrls, hasCredentials, missingCredentialsMessage } from '../lib/config.js';
4
+ import { resolveUserPath } from '../lib/paths.js';
4
5
  // 'upsert' loops create/update from the CLI, one request per record. It exists
5
6
  // only where the API has no import endpoint of its own; those keep the name
6
7
  // 'import' and are listed in SERVER_IMPORTS.
@@ -11,10 +12,10 @@ const SERVER_IMPORTS = [];
11
12
  const NESTED_UPSERTS = [];
12
13
  const SUBCOMMANDS = [...["list", "create", "api-keys-rotate", "delete"], ...(HAS_UPSERT ? ['upsert'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_UPSERTS.map(n => `upsert-${n}`)];
13
14
  const ENDPOINTS = {
14
- 'list': { method: 'GET', path: '/v1/auth/api-keys', summary: 'List the tenant\'s API keys', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
15
- 'create': { method: 'POST', path: '/v1/auth/api-keys', summary: 'Create an API key for the tenant', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['permissions'] },
16
- 'api-keys-rotate': { method: 'POST', path: '/v1/auth/api-keys/{apiKeyId}/rotate', summary: 'Rotate an API key', pathParams: ['apiKeyId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
17
- 'delete': { method: 'DELETE', path: '/v1/auth/api-keys/{apiKeyId}', summary: 'Revoke an API key', pathParams: ['apiKeyId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
15
+ 'list': { method: 'GET', path: '/v1/auth/api-keys', summary: 'List the tenant\'s API keys', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
16
+ 'create': { method: 'POST', path: '/v1/auth/api-keys', summary: 'Create an API key for the tenant', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['permissions'], bodyMediaType: '', specParams: [] },
17
+ 'api-keys-rotate': { method: 'POST', path: '/v1/auth/api-keys/{apiKeyId}/rotate', summary: 'Rotate an API key', pathParams: ['apiKeyId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
18
+ 'delete': { method: 'DELETE', path: '/v1/auth/api-keys/{apiKeyId}', summary: 'Revoke an API key', pathParams: ['apiKeyId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
18
19
  };
19
20
  export default class ApiKeys extends Command {
20
21
  static description = 'Manage api keys';
@@ -33,7 +34,7 @@ export default class ApiKeys extends Command {
33
34
  table: Flags.boolean({ char: 't', description: 'Output as table (shortcut for --output table)' }),
34
35
  csv: Flags.boolean({ description: 'Output as CSV (shortcut for --output csv)' }),
35
36
  data: Flags.string({ char: 'd', description: 'JSON data for request body' }),
36
- file: Flags.string({ char: 'f', description: 'Read request body from file' }),
37
+ file: Flags.string({ char: 'f', description: 'Read request body from file', parse: async (input) => resolveUserPath(input) }),
37
38
  set: Flags.string({ char: 's', description: 'Set individual field (e.g. --set firstName=John)', multiple: true }),
38
39
  page: Flags.string({ description: 'Page number' }),
39
40
  limit: Flags.string({ description: 'Items per page' }),
@@ -164,6 +165,17 @@ export default class ApiKeys extends Command {
164
165
  }
165
166
  }
166
167
  }
168
+ // Flags generated from this endpoint's own parameters. Raw-body endpoints
169
+ // have them: their body is not JSON, so --set cannot carry a value there.
170
+ for (const param of endpoint.specParams) {
171
+ const value = flags[param.flag];
172
+ if (value === undefined || value === '') {
173
+ if (param.required)
174
+ this.error(`Missing required flag: --${param.flag}`);
175
+ continue;
176
+ }
177
+ queryParams[param.name] = value;
178
+ }
167
179
  let data;
168
180
  if (endpoint.isMultipart) {
169
181
  if (!flags.file)
@@ -184,6 +196,25 @@ export default class ApiKeys extends Command {
184
196
  }
185
197
  data = form;
186
198
  }
199
+ else if (endpoint.bodyMediaType) {
200
+ // The body is not JSON (today only application/x-ndjson): it is sent
201
+ // verbatim, under the content type the endpoint asks for.
202
+ if (flags.file) {
203
+ const fsModule = await import('node:fs');
204
+ try {
205
+ data = fsModule.readFileSync(flags.file, 'utf-8');
206
+ }
207
+ catch {
208
+ this.error(`Failed to read file: ${flags.file}`);
209
+ }
210
+ }
211
+ else if (flags.data) {
212
+ data = flags.data;
213
+ }
214
+ else {
215
+ this.error(`This endpoint takes its body as ${endpoint.bodyMediaType}. Pass the file with -f / --file, or the text with --data.`);
216
+ }
217
+ }
187
218
  else if (endpoint.hasBody) {
188
219
  // For binary-response endpoints, --file is the output path, not a JSON
189
220
  // input file — skip the read-as-body branch.
@@ -279,7 +310,7 @@ export default class ApiKeys extends Command {
279
310
  process.stderr.write(`Body: [multipart/form-data file upload]\n`);
280
311
  }
281
312
  else if (data) {
282
- process.stderr.write(`Body: ${JSON.stringify(data, null, 2)}\n`);
313
+ process.stderr.write(`Body: ${typeof data === 'string' ? data : JSON.stringify(data, null, 2)}\n`);
283
314
  }
284
315
  process.stderr.write('\n');
285
316
  }
@@ -293,6 +324,7 @@ export default class ApiKeys extends Command {
293
324
  const binResponse = await apiRequest(endpoint.method, apiPath, {
294
325
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
295
326
  data,
327
+ contentType: endpoint.bodyMediaType || undefined,
296
328
  contextName: flags.context,
297
329
  binary: true,
298
330
  });
@@ -318,6 +350,7 @@ export default class ApiKeys extends Command {
318
350
  const raw = await apiRequest(endpoint.method, apiPath, {
319
351
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
320
352
  data,
353
+ contentType: endpoint.bodyMediaType || undefined,
321
354
  contextName: flags.context,
322
355
  binary: true,
323
356
  });
@@ -338,6 +371,7 @@ export default class ApiKeys extends Command {
338
371
  response = await apiRequest(endpoint.method, apiPath, {
339
372
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
340
373
  data,
374
+ contentType: endpoint.bodyMediaType || undefined,
341
375
  contextName: flags.context,
342
376
  acceptAny: true,
343
377
  });