jaz-clio 4.20.1 → 4.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/README.md +13 -1
  2. package/assets/skills/api/SKILL.md +33 -8
  3. package/assets/skills/api/references/errors.md +8 -9
  4. package/assets/skills/api/references/field-map.md +13 -5
  5. package/assets/skills/conversion/SKILL.md +1 -1
  6. package/assets/skills/jobs/SKILL.md +1 -1
  7. package/assets/skills/transaction-recipes/SKILL.md +1 -1
  8. package/dist/commands/attachments.js +47 -6
  9. package/dist/commands/bank-rules.js +101 -0
  10. package/dist/commands/bank.js +63 -19
  11. package/dist/commands/contact-groups.js +86 -0
  12. package/dist/commands/custom-fields.js +60 -0
  13. package/dist/commands/exports.js +1 -1
  14. package/dist/commands/fixed-assets.js +153 -0
  15. package/dist/commands/inventory.js +50 -0
  16. package/dist/commands/schedulers.js +4 -0
  17. package/dist/commands/search.js +34 -0
  18. package/dist/commands/subscriptions.js +99 -0
  19. package/dist/core/api/attachments.js +2 -2
  20. package/dist/core/api/bank-rules.js +31 -0
  21. package/dist/core/api/bank.js +6 -0
  22. package/dist/core/api/contact-groups.js +12 -0
  23. package/dist/core/api/customer-cn.js +3 -0
  24. package/dist/core/api/fixed-assets.js +37 -0
  25. package/dist/core/api/index.js +6 -0
  26. package/dist/core/api/inventory.js +6 -0
  27. package/dist/core/api/reports.js +24 -0
  28. package/dist/core/api/search.js +6 -0
  29. package/dist/core/api/subscriptions.js +25 -0
  30. package/dist/core/api/tax-profiles.js +14 -0
  31. package/dist/core/jobs/bank-recon/tools/match/match.js +86 -63
  32. package/dist/core/registry/pagination.js +33 -0
  33. package/dist/core/registry/tools.js +916 -17
  34. package/dist/index.js +14 -0
  35. package/package.json +3 -1
package/README.md CHANGED
@@ -128,7 +128,7 @@ Every command supports `--json` for structured output — ideal for piping to ot
128
128
 
129
129
  ## MCP Server
130
130
 
131
- Expose all 145 CLI tools to AI coding assistants via the [Model Context Protocol](https://modelcontextprotocol.io). The server runs locally on your machine — no cloud, no ports. API calls go directly from your machine to the Jaz API.
131
+ Expose all 200 CLI tools to AI coding assistants via the [Model Context Protocol](https://modelcontextprotocol.io). The server runs locally on your machine — no cloud, no ports. API calls go directly from your machine to the Jaz API.
132
132
 
133
133
  **Claude Code:**
134
134
 
@@ -167,6 +167,18 @@ clio init --platform claude # Force a specific platform
167
167
 
168
168
  See the [full README](https://github.com/teamtinvio/jaz-ai) for skill details, reference file catalogs, and plugin installation.
169
169
 
170
+ ## Privacy & Security
171
+
172
+ Jaz AI runs entirely on your machine. API calls go directly from your machine to the Jaz API over HTTPS. This tool does not include telemetry or collect data. Your API key is stored locally in `~/.config/jaz-clio/credentials.json`.
173
+
174
+ See our full privacy policy: [jaz.ai/legal](https://jaz.ai/legal)
175
+
176
+ ## Support
177
+
178
+ - Help center: [help.jaz.ai](https://help.jaz.ai)
179
+ - GitHub Issues: [github.com/teamtinvio/jaz-ai/issues](https://github.com/teamtinvio/jaz-ai/issues)
180
+ - Email: api-support@jaz.ai
181
+
170
182
  ## License
171
183
 
172
184
  [MIT](https://github.com/teamtinvio/jaz-ai/blob/main/LICENSE) - Copyright (c) 2026 Tinvio / Jaz
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: jaz-api
3
- version: 4.20.1
3
+ version: 4.22.0
4
4
  description: Complete reference for the Jaz REST API — the accounting platform backend. Use this skill whenever building, modifying, debugging, or extending any code that calls the API — including API clients, integrations, data seeding, test data, or new endpoint work. Contains every field name, response shape, error, gotcha, and edge case discovered through live production testing.
5
5
  license: MIT
6
6
  compatibility: Requires Jaz API key (x-jk-api-key header). Works with Claude Code, Google Antigravity, OpenAI Codex, GitHub Copilot, Cursor, and any agent that reads markdown.
@@ -77,8 +77,8 @@ You are working with the **Jaz REST API** — the accounting platform backend. A
77
77
  31. **Cash transfers use `cashOut`/`cashIn` sub-objects** — NOT flat `fromAccountResourceId`/`toAccountResourceId`. Each: `{ accountResourceId, amount }`.
78
78
 
79
79
  ### Schedulers
80
- 32. **Scheduled invoices/bills wrap in `{ invoice: {...} }` or `{ bill: {...} }`** — not flat. Recurrence field is `repeat` (NOT `frequency`/`interval`). `saveAsDraft: false` required.
81
- 33. **Scheduled journals use FLAT structure** with `schedulerEntries` — not nested in `journal` wrapper.
80
+ 32. **Scheduled invoices/bills wrap in `{ invoice: {...} }` or `{ bill: {...} }`** — not flat. Recurrence field is `repeat` (NOT `frequency`/`interval`). `saveAsDraft: false` required. **`reference` is required** inside the `invoice`/`bill` wrapper — omitting it causes 422.
81
+ 33. **Scheduled journals use FLAT structure** with `schedulerEntries` — not nested in `journal` wrapper. **`valueDate` is required** at the top level (alongside `startDate`, `repeat`, etc.).
82
82
 
83
83
  ### Bookmarks
84
84
  34. **Bookmarks use `items` array wrapper** with `name`, `value`, `categoryCode`, `datatypeCode`.
@@ -113,11 +113,11 @@ You are working with the **Jaz REST API** — the accounting platform backend. A
113
113
  41. **Invoice GET uses `organizationAccountResourceId`** for line item accounts — POST uses `accountResourceId`. Request-side aliases resolve `issueDate` → `valueDate`, `bankAccountResourceId` → `accountResourceId`, etc.
114
114
  42. **Scheduler GET returns `interval`** — POST uses `repeat`. (Response-side asymmetry remains.)
115
115
  43. **Search sort is an object** — `{ sort: { sortBy: ["valueDate"], order: "DESC" } }`. Required when `offset` is present (even `offset: 0`).
116
- 44. **Bank records: two import methods** Multipart CSV/OFX via `POST /magic/importBankStatementFromAttachment` (camelCase fields: `sourceFile`, `accountResourceId`, `businessTransactionType: "BANK_STATEMENT"`, `sourceType: "FILE"` same camelCase rule as rule 59). JSON via `POST /bank-records/:accountResourceId` with `{ records: [{description, netAmount, valueDate, ...}] }`.
116
+ 44. **Bank records** **Create**: Multipart CSV/OFX via `POST /magic/importBankStatementFromAttachment` or JSON via `POST /bank-records/:accountResourceId` with `{ records: [{amount, transactionDate, description?, payerOrPayee?, reference?}] }` (positive = cash-in, negative = cash-out, response: `{data: {errors: []}}`). **Search**: `POST /bank-records/:accountResourceId/search` filter fields: `valueDate` (DateExpression), `status` (StringExpression: UNRECONCILED, RECONCILED, ARCHIVED, POSSIBLE_DUPLICATE), `description`, `extContactName` (payer/payee), `extReference`, `netAmount` (BigDecimalExpression), `extAccountNumber`. Sort by `valueDate` DESC default.
117
117
  45. **Withholding tax** on bills/supplier CNs only. Retry pattern: if `WITHHOLDING_CODE_NOT_FOUND`, strip field and retry.
118
- 46. **Known API bugs (500s)**: Contact groups PUT, custom fields PUT, capsules POST, catalogs POST, inventory balances GET — all return 500.
118
+ 46. **Known API bugs (500s/404s)**: Contact groups PUT, custom fields PUT, capsules POST, catalogs POST, inventory balances by status GET (`/inventory-balances/:status`) — all return 500. Auto-reconciliation view (`POST /view-auto-reconciliation`) returns 404 (endpoint may not be deployed).
119
119
  47. **Non-existent endpoints**: `POST /deposits`, `POST /inventory/adjustments`, `GET /payments` (list), and `POST /payments/search` return 404 — these endpoints are not implemented. To list/search payments, use `POST /cashflow-transactions/search` (the unified transaction ledger — see Rule 63).
120
- 48. **Attachments require PDF/PNG**: `POST /:type/:id/attachments` uses multipart `file` field but rejects `text/plain`. Use `application/pdf` or `image/png`.
120
+ 48. **Attachments require multipart `file` field**: `POST /:type/:id/attachments` expects a binary file in the `file` form-data field NOT a `sourceUrl` text field. Rejects `text/plain`. Use `application/pdf` or `image/png`. CLI: `clio attachments add --file <path>` (local file) or `--url <url>` (downloads first, then uploads as `file`).
121
121
  49. **Currency rate direction: `rate` = functionalToSource (1 base = X foreign)** — POST `rate: 0.74` for a SGD org means 1 SGD = 0.74 USD. **If your data stores rates as "1 USD = 1.35 SGD" (sourceToFunctional), you MUST invert: `rate = 1 / 1.35 = 0.74`.** GET confirms both: `rateFunctionalToSource` (what you POSTed) and `rateSourceToFunctional` (the inverse).
122
122
 
123
123
  ### Search & Filter
@@ -222,6 +222,30 @@ Step 3: For each draft where ready = true:
222
222
 
223
223
  Bills, invoices, and credit notes share identical mandatory field specs. Adding `clio invoices draft` or `clio customer-credit-notes draft` later reuses all validation, formatting, and CLI flag logic from `draft-helpers.ts` — only the API calls differ.
224
224
 
225
+ ### Bank Rules
226
+ 89. **Bank rules GET by ID has double-nested response** — `GET /bank-rules/:id` returns `{ data: { data: [...], totalElements, totalPages } }` (double `data` wrapper). Unlike standard `GET /:type/:id` which returns `{ data: {...} }`. The inner `data` is an array containing the single rule. Unwrap with `response.data.data[0]`.
227
+ 90. **Bank rules search uses `/bank-rules/search`** — Standard search pattern with filter/sort/limit/offset. Sort fields: standard (name, createdAt, etc.).
228
+
229
+ ### Fixed Assets
230
+ 91. **Fixed asset search does NOT support `createdAt` sort** — Valid sort fields: `resourceId`, `name`, `purchaseDate`, `typeName`, `purchaseAmount`, `bookValueNetBookValueAmount`, `depreciationMethod`, `status`. Using `createdAt` returns 422. Default to `purchaseDate` DESC.
231
+ 92. **Fixed asset disposal/sale/transfer use different endpoint patterns** — Discard: `POST /discard-fixed-assets/:id` (body includes `resourceId` + dates). Mark sold: `POST /mark-as-sold/fixed-assets` (body-only, no path param). Transfer: `POST /transfer-fixed-assets` (body-only). Undo: `POST /undo-disposal/fixed-assets/:id`.
232
+
233
+ ### Subscriptions & Scheduled Transactions
234
+ 93. **Subscription endpoints are under `/scheduled/subscriptions`** — List, GET, POST, PUT, DELETE all at `/api/v1/scheduled/subscriptions[/:id]`. Cancel is at `/api/v1/scheduled/cancel-subscriptions/:id` (different path pattern).
235
+ 94. **Scheduled transaction search does NOT support `createdAt` sort** — `POST /scheduled-transaction/search` sort fields: `startDate`, `nextScheduleDate`, etc. Default to `startDate` DESC. This is a cross-entity search across all scheduled types (invoices, bills, journals, subscriptions).
236
+
237
+ ### Universal Search
238
+ 95. **Universal search uses `query` param (NOT `q`)** — `GET /search?query=<term>` returns categorized results across contacts, invoices, bills, credit notes, journals. Response is a flat object with category keys, each containing an array of matches. No pagination — returns top matches per category.
239
+
240
+ ### Contact Groups
241
+ 96. **Contact groups have `associatedContacts` array** — Each group contains `{ name, resourceId, associatedContacts: [{ name, resourceId }] }`. Search via `POST /contact-groups/search`. Known bug: PUT returns 500 (Rule 46).
242
+
243
+ ### Inventory
244
+ 97. **Inventory balance uses `GET /inventory-item-balance/:itemResourceId`** — Returns `{ itemResourceId, latestAverageCostAmount, baseQty, baseUnit }`. Note: this is the ITEM resourceId, not an inventory-specific ID. The `/inventory-balances/:status` endpoint returns 500 (Rule 46).
245
+
246
+ ### Withholding Tax
247
+ 98. **Withholding tax codes via `GET /withholding-tax-codes`** — Returns a flat array of 1,360+ entries (PH PSIC codes). Each entry: `{ code, description, taxRate, ... }`. No pagination — full list in one call. Use for PH/SG tax compliance.
248
+
225
249
  ## Supporting Files
226
250
 
227
251
  For detailed reference, read these files in this skill directory:
@@ -269,6 +293,7 @@ The backend DX overhaul is live. Key improvements now available:
269
293
  - **Field names**: All request bodies use camelCase
270
294
  - **Date serialization**: Python `date` type → `YYYY-MM-DD` strings
271
295
  - **Bill payments**: Embed in bill creation body (safest). Standalone `POST /bills/{id}/payments` also works.
272
- - **Bank records**: Use multipart `POST /magic/importBankStatementFromAttachment`
273
- - **Scheduled bills**: Wrap as `{ status, startDate, endDate, repeat, bill: {...} }`
296
+ - **Bank records**: Create via JSON `POST /bank-records/:id` or multipart `POST /magic/importBankStatementFromAttachment`. Search via `POST /bank-records/:id/search` with filters (valueDate, status, description, extContactName, netAmount, extReference).
297
+ - **Scheduled invoices/bills**: Wrap as `{ status, startDate, endDate, repeat, invoice/bill: { reference, valueDate, dueDate, contactResourceId, lineItems, saveAsDraft: false } }`. `reference` is required.
298
+ - **Scheduled journals**: Flat: `{ status, startDate, endDate, repeat, valueDate, schedulerEntries, reference }`. `valueDate` is required.
274
299
  - **FX currency (invoices, bills, credit notes, AND journals)**: `currency: { sourceCurrency: "USD" }` (auto-fetches platform rate) or `currency: { sourceCurrency: "USD", exchangeRate: 1.35 }` (custom rate). Same object form on all transaction types. **Never use `currencyCode` string** — silently ignored.
@@ -322,10 +322,14 @@ if (acct.code) ctx.coaIds[acct.code] = acct.resourceId;
322
322
 
323
323
  ## Bank Record Errors
324
324
 
325
- ### No JSON POST endpoint exists
326
- There is no JSON POST endpoint for creating bank records. Use multipart import:
325
+ ### Bank record creation
327
326
 
328
- **Workaround**: Use multipart import instead:
327
+ Two methods for creating bank records:
328
+
329
+ **Method 1: JSON POST** (programmatic)
330
+ `POST /api/v1/bank-records/:accountResourceId` with `{ records: [{amount, transactionDate, description?, payerOrPayee?, reference?}] }`. Returns `{ data: { errors: [] } }` on success. 1-100 records per call.
331
+
332
+ **Method 2: Multipart import** (file upload)
329
333
  ```
330
334
  POST /api/v1/magic/importBankStatementFromAttachment
331
335
  Content-Type: multipart/form-data
@@ -337,12 +341,7 @@ Fields:
337
341
  - sourceType: "FILE" (valid values: URL, FILE)
338
342
  ```
339
343
 
340
- Production clients use this multipart endpoint exclusively. Multipart import is the more reliable method.
341
-
342
- **Legacy guidance** (still valid for general bank record handling):
343
- 1. Always use `Math.abs()` on amounts — amounts must be positive
344
- 2. Use `type: "CREDIT"` or `type: "DEBIT"` to indicate direction
345
- 3. Check that `bankAccountResourceId` is a valid CoA entry with `accountType: "Bank Accounts"`
344
+ **Amount rules**: For JSON POST, positive = cash-in, negative = cash-out. Dates in YYYY-MM-DD format. `accountResourceId` must be a bank-type CoA account.
346
345
 
347
346
  ---
348
347
 
@@ -326,12 +326,20 @@ DELETE → expects "A" (parentEntityResourceId, via /cashflow-journals/:id)
326
326
 
327
327
  | What You'd Guess | Actual API Field | Notes |
328
328
  |------------------|-------------------|-------|
329
- | `date` | `transactionDate` | ISO date string |
330
- | `direction` | `type` | `"CREDIT"` or `"DEBIT"` (uppercase) |
329
+ | `date` (create) | `transactionDate` | ISO date string (YYYY-MM-DD) |
330
+ | `date` (search response) | `valueDate` | Returned as ISO 8601 UTC |
331
+ | `amount` (create) | `amount` | Positive = cash-in, negative = cash-out |
332
+ | `amount` (search response) | `netAmount` | Same sign convention |
333
+ | `payer` / `payee` (create) | `payerOrPayee` | Free text |
334
+ | `payer` / `payee` (search) | `extContactName` | Search filter field |
335
+ | `reference` (search) | `extReference` | Search filter field |
331
336
  | `memo` | `description` | Free text |
332
- | (negative amount) | `amount` (positive) + `type` | Always positive, direction via type |
337
+ | `source` | `bankStatementEntrySource` | `FILE_IMPORT` or `API` (read-only) |
338
+ | `status` | `status` | `UNRECONCILED`, `RECONCILED`, `ARCHIVED`, `POSSIBLE_DUPLICATE` |
333
339
 
334
- **Creating bank records**: Use multipart `POST /api/v1/magic/importBankStatementFromAttachment` the only endpoint for creating bank records. No JSON POST endpoint exists.
340
+ **Creating bank records**: Two methods. JSON POST via `POST /bank-records/:accountResourceId` with `{ records: [{amount, transactionDate, ...}] }` (1-100 records). Multipart via `POST /magic/importBankStatementFromAttachment` for CSV/OFX file uploads.
341
+
342
+ **Searching bank records**: `POST /bank-records/:accountResourceId/search` with filter fields: `valueDate` (DateExpression), `status` (StringExpression), `description` (StringExpression), `extContactName` (StringExpression), `extReference` (StringExpression), `netAmount` (BigDecimalExpression), `extAccountNumber` (StringExpression). Sort by `valueDate` DESC default.
335
343
 
336
344
  ---
337
345
 
@@ -510,7 +518,7 @@ Battle-tested patterns from production Jaz API clients:
510
518
  | Alias generator | `alias_generator=to_camel` (snake_case to camelCase) |
511
519
  | Date type | Python `date` type serializes to `YYYY-MM-DD` strings |
512
520
  | Bill payments | Always embedded in bill creation body, never standalone |
513
- | Bank records | Multipart import via `importBankStatementFromAttachment` the only endpoint |
521
+ | Bank records | JSON POST `/bank-records/:id` or multipart `/magic/importBankStatementFromAttachment`. Search via `/bank-records/:id/search`. |
514
522
  | Scheduled bills | Wrapped as `{ repeat, startDate, endDate, bill: {...} }`. Field is `repeat` (NOT `frequency`/`interval`) |
515
523
  | FX currency | MUST use `currency` OBJECT on ALL transaction types (invoices, bills, credit notes, journals): `{ sourceCurrency: "USD" }` (auto platform rate) or `{ sourceCurrency: "USD", exchangeRate: 1.35 }` (custom). String `currencyCode` silently ignored. |
516
524
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: jaz-conversion
3
- version: 4.20.1
3
+ version: 4.22.0
4
4
  description: Accounting data conversion skill — migrates customer data from Xero, QuickBooks, Sage, MYOB, and Excel exports to Jaz. Covers config, quick, and full conversion workflows, Excel parsing, CoA/contact/tax/items mapping, clearing accounts, TTB, and TB verification.
5
5
  ---
6
6
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: jaz-jobs
3
- version: 4.20.1
3
+ version: 4.22.0
4
4
  description: 12 accounting jobs for SMB bookkeepers and accountants — month-end, quarter-end, and year-end close playbooks plus 9 ad-hoc operational jobs (bank recon, document collection, GST/VAT filing, payment runs, credit control, supplier recon, audit prep, fixed asset review, statutory filing). Jobs can have paired tools as nested subcommands (e.g., `clio jobs bank-recon match`, `clio jobs document-collection ingest`, `clio jobs statutory-filing sg-cs`). Paired with an interactive CLI blueprint generator (clio jobs).
5
5
  license: MIT
6
6
  compatibility: Works with Claude Code, Claude Cowork, Claude.ai, and any agent that reads markdown. For API payloads, load the jaz-api skill. For individual transaction patterns, load the jaz-recipes skill.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: jaz-recipes
3
- version: 4.20.1
3
+ version: 4.22.0
4
4
  description: 16 IFRS-compliant recipes for complex multi-step accounting in Jaz — prepaid amortization, deferred revenue, loan schedules, IFRS 16 leases, hire purchase, fixed deposits, asset disposal, FX revaluation, ECL provisioning, IAS 37 provisions, dividends, intercompany, and capital WIP. Each recipe includes journal entries, capsule structure, and verification steps. Paired with 13 financial calculators that produce execution-ready blueprints with workings.
5
5
  license: MIT
6
6
  compatibility: Works with Claude Code, Claude Cowork, Claude.ai, and any agent that reads markdown. For API payloads, load the jaz-api skill alongside this one.
@@ -1,7 +1,21 @@
1
1
  import chalk from 'chalk';
2
+ import { readFileSync } from 'node:fs';
3
+ import { basename, extname, resolve } from 'node:path';
2
4
  import { listAttachments, addAttachment, fetchAttachmentTable } from '../core/api/attachments.js';
3
5
  import { apiAction } from './api-action.js';
4
6
  const BT_TYPES = ['invoices', 'bills', 'journals', 'scheduled_journals', 'customer-credit-notes', 'supplier-credit-notes'];
7
+ const ATTACHMENT_MIME_MAP = {
8
+ '.pdf': 'application/pdf',
9
+ '.png': 'image/png',
10
+ '.jpg': 'image/jpeg',
11
+ '.jpeg': 'image/jpeg',
12
+ '.gif': 'image/gif',
13
+ '.webp': 'image/webp',
14
+ '.svg': 'image/svg+xml',
15
+ '.csv': 'text/csv',
16
+ '.xls': 'application/vnd.ms-excel',
17
+ '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
18
+ };
5
19
  export function registerAttachmentsCommand(program) {
6
20
  const attachments = program
7
21
  .command('attachments')
@@ -35,11 +49,12 @@ export function registerAttachmentsCommand(program) {
35
49
  // ── clio attachments add ───────────────────────────────────────
36
50
  attachments
37
51
  .command('add')
38
- .description('Add an attachment to a transaction')
52
+ .description('Add an attachment to a transaction (file upload or URL download)')
39
53
  .requiredOption('--type <type>', `Transaction type (${BT_TYPES.join(', ')})`)
40
54
  .requiredOption('--id <resourceId>', 'Transaction resourceId')
41
- .option('--attachment-id <id>', 'Attachment ID')
42
- .option('--url <url>', 'Source file URL')
55
+ .option('--file <path>', 'Local file path (PDF, PNG, JPG, etc.)')
56
+ .option('--url <url>', 'Download file from URL and attach')
57
+ .option('--attachment-id <id>', 'Link existing attachment by ID')
43
58
  .option('--api-key <key>', 'API key')
44
59
  .option('--json', 'Output as JSON')
45
60
  .action(apiAction(async (client, opts) => {
@@ -48,15 +63,41 @@ export function registerAttachmentsCommand(program) {
48
63
  console.error(chalk.red(`Invalid type. Use one of: ${BT_TYPES.join(', ')}`));
49
64
  process.exit(1);
50
65
  }
51
- if (!opts.attachmentId && !opts.url) {
52
- console.error(chalk.red('Provide --attachment-id or --url'));
66
+ if (!opts.attachmentId && !opts.url && !opts.file) {
67
+ console.error(chalk.red('Provide --file, --url, or --attachment-id'));
53
68
  process.exit(1);
54
69
  }
70
+ let file;
71
+ let fileName;
72
+ if (opts.file) {
73
+ // Local file upload
74
+ const filePath = resolve(opts.file);
75
+ const ext = extname(filePath).toLowerCase();
76
+ const mime = ATTACHMENT_MIME_MAP[ext] ?? 'application/octet-stream';
77
+ const buffer = readFileSync(filePath);
78
+ file = new Blob([buffer], { type: mime });
79
+ fileName = basename(filePath);
80
+ }
81
+ else if (opts.url) {
82
+ // Download from URL, then upload as file
83
+ const res = await fetch(opts.url);
84
+ if (!res.ok) {
85
+ console.error(chalk.red(`Failed to download: ${res.status} ${res.statusText}`));
86
+ process.exit(1);
87
+ }
88
+ const buffer = await res.arrayBuffer();
89
+ const contentType = res.headers.get('content-type') ?? 'application/octet-stream';
90
+ file = new Blob([buffer], { type: contentType });
91
+ // Extract filename from URL path
92
+ const urlPath = new URL(opts.url).pathname;
93
+ fileName = basename(urlPath) || 'attachment';
94
+ }
55
95
  const result = await addAttachment(client, {
56
96
  businessTransactionType: btType,
57
97
  businessTransactionResourceId: opts.id,
98
+ file,
99
+ fileName,
58
100
  attachmentId: opts.attachmentId,
59
- sourceUrl: opts.url,
60
101
  });
61
102
  if (opts.json) {
62
103
  console.log(JSON.stringify(result, null, 2));
@@ -0,0 +1,101 @@
1
+ import chalk from 'chalk';
2
+ import { listBankRules, getBankRule, searchBankRules, createBankRule, deleteBankRule, } from '../core/api/bank-rules.js';
3
+ import { apiAction } from './api-action.js';
4
+ import { parsePositiveInt, parseNonNegativeInt, readBodyInput } from './parsers.js';
5
+ import { paginatedFetch, paginatedJson, displaySlice } from './pagination.js';
6
+ export function registerBankRulesCommand(program) {
7
+ const cmd = program
8
+ .command('bank-rules')
9
+ .description('Manage bank reconciliation rules');
10
+ cmd
11
+ .command('list')
12
+ .description('List bank rules')
13
+ .option('--limit <n>', 'Max results', parsePositiveInt)
14
+ .option('--offset <n>', 'Offset', parseNonNegativeInt)
15
+ .option('--all', 'Fetch all pages')
16
+ .option('--api-key <key>', 'API key')
17
+ .option('--json', 'JSON output')
18
+ .action(apiAction(async (client, opts) => {
19
+ const result = await paginatedFetch(opts, (p) => listBankRules(client, p), { label: 'Fetching bank rules' });
20
+ if (opts.json) {
21
+ console.log(paginatedJson(result, opts));
22
+ return;
23
+ }
24
+ console.log(chalk.bold(`Bank Rules (${result.data.length} of ${result.totalElements}):\n`));
25
+ const { items, overflow } = displaySlice(result.data);
26
+ for (const r of items)
27
+ console.log(` ${chalk.cyan(r.resourceId)} ${r.name} ${chalk.dim(r.actionType)}`);
28
+ if (overflow > 0)
29
+ console.log(chalk.dim(` ... and ${overflow} more`));
30
+ }));
31
+ cmd
32
+ .command('get <resourceId>')
33
+ .description('Get a bank rule')
34
+ .option('--api-key <key>', 'API key')
35
+ .option('--json', 'JSON output')
36
+ .action((resourceId, opts) => apiAction(async (client) => {
37
+ const res = await getBankRule(client, resourceId);
38
+ if (opts.json) {
39
+ console.log(JSON.stringify(res.data, null, 2));
40
+ return;
41
+ }
42
+ console.log(chalk.bold('Name:'), res.data.name);
43
+ console.log(chalk.bold('Action:'), res.data.actionType);
44
+ console.log(chalk.bold('ID:'), res.data.resourceId);
45
+ })(opts));
46
+ cmd
47
+ .command('search <query>')
48
+ .description('Search bank rules')
49
+ .option('--limit <n>', 'Max results', parsePositiveInt)
50
+ .option('--offset <n>', 'Offset', parseNonNegativeInt)
51
+ .option('--api-key <key>', 'API key')
52
+ .option('--json', 'JSON output')
53
+ .action((query, opts) => apiAction(async (client) => {
54
+ const result = await paginatedFetch(opts, ({ limit, offset }) => searchBankRules(client, { filter: { name: { contains: query } }, limit, offset }), { label: 'Searching bank rules', defaultLimit: 20 });
55
+ if (opts.json) {
56
+ console.log(paginatedJson(result, opts));
57
+ return;
58
+ }
59
+ if (result.data.length === 0) {
60
+ console.log('No bank rules found.');
61
+ return;
62
+ }
63
+ console.log(chalk.bold(`Found ${result.data.length} rule(s):\n`));
64
+ for (const r of result.data)
65
+ console.log(` ${chalk.cyan(r.resourceId)} ${r.name}`);
66
+ })(opts));
67
+ cmd
68
+ .command('create')
69
+ .description('Create a bank rule')
70
+ .option('--input <file>', 'Read request body from JSON file')
71
+ .option('--api-key <key>', 'API key')
72
+ .option('--json', 'JSON output')
73
+ .action(apiAction(async (client, opts) => {
74
+ const body = readBodyInput(opts);
75
+ if (!body) {
76
+ console.error(chalk.red('Use --input <file> to provide rule configuration.'));
77
+ process.exit(1);
78
+ }
79
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
80
+ const res = await createBankRule(client, body);
81
+ if (opts.json) {
82
+ console.log(JSON.stringify(res.data, null, 2));
83
+ return;
84
+ }
85
+ console.log(chalk.green('Bank rule created.'));
86
+ console.log(chalk.bold('ID:'), res.data.resourceId);
87
+ }));
88
+ cmd
89
+ .command('delete <resourceId>')
90
+ .description('Delete a bank rule')
91
+ .option('--api-key <key>', 'API key')
92
+ .option('--json', 'JSON output')
93
+ .action((resourceId, opts) => apiAction(async (client) => {
94
+ await deleteBankRule(client, resourceId);
95
+ if (opts.json) {
96
+ console.log(JSON.stringify({ deleted: true, resourceId }));
97
+ return;
98
+ }
99
+ console.log(chalk.green(`Bank rule ${resourceId} deleted.`));
100
+ })(opts));
101
+ }
@@ -1,7 +1,8 @@
1
1
  import chalk from 'chalk';
2
2
  import { readFileSync } from 'node:fs';
3
3
  import { basename, extname, resolve } from 'node:path';
4
- import { listBankAccounts, getBankAccount, searchBankRecords, importBankStatement, } from '../core/api/bank.js';
4
+ import { listBankAccounts, getBankAccount, searchBankRecords, addBankRecords, importBankStatement, } from '../core/api/bank.js';
5
+ import { buildBankRecordFilter } from '../core/registry/pagination.js';
5
6
  import { apiAction } from './api-action.js';
6
7
  import { parsePositiveInt } from './parsers.js';
7
8
  const BANK_MIME_MAP = {
@@ -61,27 +62,31 @@ export function registerBankCommand(program) {
61
62
  .description('Search bank records for a bank account')
62
63
  .option('--from <YYYY-MM-DD>', 'Filter from date (inclusive)')
63
64
  .option('--to <YYYY-MM-DD>', 'Filter to date (inclusive)')
64
- .option('--status <status>', 'Filter by status (UNRECONCILED, RECONCILED)')
65
+ .option('--status <status>', 'Filter by status (UNRECONCILED, RECONCILED, ARCHIVED, POSSIBLE_DUPLICATE)')
66
+ .option('--description <text>', 'Filter by description (contains)')
67
+ .option('--payer <name>', 'Filter by payer/payee name (contains)')
68
+ .option('--reference <ref>', 'Filter by reference (contains)')
69
+ .option('--amount-min <n>', 'Filter by minimum amount', parseFloat)
70
+ .option('--amount-max <n>', 'Filter by maximum amount', parseFloat)
65
71
  .option('--limit <n>', 'Max results (default 50)', parsePositiveInt)
66
72
  .option('--api-key <key>', 'API key (overrides stored/env)')
67
73
  .option('--json', 'Output as JSON')
68
74
  .action((accountResourceId, opts) => apiAction(async (client) => {
69
- const filter = {};
70
- if (opts.status)
71
- filter.status = { eq: opts.status };
72
- if (opts.from || opts.to) {
73
- const dateFilter = {};
74
- if (opts.from)
75
- dateFilter.gte = opts.from;
76
- if (opts.to)
77
- dateFilter.lte = opts.to;
78
- filter.date = dateFilter;
79
- }
75
+ const filter = buildBankRecordFilter({
76
+ status: opts.status,
77
+ from: opts.from,
78
+ to: opts.to,
79
+ description: opts.description,
80
+ payer: opts.payer,
81
+ reference: opts.reference,
82
+ amountMin: opts.amountMin,
83
+ amountMax: opts.amountMax,
84
+ });
80
85
  const res = await searchBankRecords(client, accountResourceId, {
81
- filter: Object.keys(filter).length > 0 ? filter : undefined,
86
+ filter,
82
87
  limit: opts.limit ?? 50,
83
88
  offset: 0,
84
- sort: { sortBy: ['date'], order: 'DESC' },
89
+ sort: { sortBy: ['valueDate'], order: 'DESC' },
85
90
  });
86
91
  if (opts.json) {
87
92
  console.log(JSON.stringify(res, null, 2));
@@ -93,10 +98,49 @@ export function registerBankCommand(program) {
93
98
  }
94
99
  console.log(chalk.bold(`Bank Records (${res.data.length} of ${res.totalElements}):\n`));
95
100
  for (const r of res.data) {
96
- const amount = r.amount >= 0
97
- ? chalk.green(`+${r.amount.toFixed(2)}`)
98
- : chalk.red(r.amount.toFixed(2));
99
- console.log(` ${chalk.cyan(r.resourceId)} ${r.date} ${amount} ${r.description} ${chalk.dim(r.status)}`);
101
+ const net = typeof r.netAmount === 'number' && Number.isFinite(r.netAmount) ? r.netAmount : 0;
102
+ const amount = net >= 0
103
+ ? chalk.green(`+${net.toFixed(2)}`)
104
+ : chalk.red(net.toFixed(2));
105
+ const payer = r.extContactName ? ` ${chalk.yellow(r.extContactName)}` : '';
106
+ console.log(` ${chalk.cyan(r.resourceId)} ${r.valueDate} ${amount} ${r.description ?? ''}${payer} ${chalk.dim(r.status)}`);
107
+ }
108
+ }
109
+ })(opts));
110
+ // ── clio bank add-records ──────────────────────────────────────
111
+ bank
112
+ .command('add-records <accountResourceId>')
113
+ .description('Add bank records via JSON (1-100 records per call)')
114
+ .requiredOption('--records <json>', 'JSON array: [{amount, transactionDate, description?, payerOrPayee?, reference?}]')
115
+ .option('--api-key <key>', 'API key (overrides stored/env)')
116
+ .option('--json', 'Output as JSON')
117
+ .action((accountResourceId, opts) => apiAction(async (client) => {
118
+ const parsed = JSON.parse(opts.records);
119
+ if (!Array.isArray(parsed))
120
+ throw new Error('--records must be a JSON array');
121
+ if (parsed.length < 1 || parsed.length > 100)
122
+ throw new Error('--records must contain 1-100 entries');
123
+ for (const [i, r] of parsed.entries()) {
124
+ if (typeof r !== 'object' || r === null)
125
+ throw new Error(`records[${i}] must be an object`);
126
+ if (typeof r.amount !== 'number' || !Number.isFinite(r.amount))
127
+ throw new Error(`records[${i}].amount must be a finite number`);
128
+ if (typeof r.transactionDate !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(r.transactionDate))
129
+ throw new Error(`records[${i}].transactionDate must be YYYY-MM-DD`);
130
+ }
131
+ const res = await addBankRecords(client, accountResourceId, parsed);
132
+ const errors = Array.isArray(res?.data?.errors) ? res.data.errors : [];
133
+ if (opts.json) {
134
+ console.log(JSON.stringify(res.data, null, 2));
135
+ }
136
+ else {
137
+ if (errors.length > 0) {
138
+ console.log(chalk.red(`${errors.length} error(s):`));
139
+ for (const e of errors)
140
+ console.log(` ${chalk.red(String(e))}`);
141
+ }
142
+ else {
143
+ console.log(chalk.green(`Created ${parsed.length} bank record(s).`));
100
144
  }
101
145
  }
102
146
  })(opts));
@@ -0,0 +1,86 @@
1
+ import chalk from 'chalk';
2
+ import { listContactGroups, getContactGroup, searchContactGroups, createContactGroup, } from '../core/api/contact-groups.js';
3
+ import { apiAction } from './api-action.js';
4
+ import { parsePositiveInt, parseNonNegativeInt, requireFields } from './parsers.js';
5
+ import { paginatedFetch, paginatedJson, displaySlice } from './pagination.js';
6
+ export function registerContactGroupsCommand(program) {
7
+ const cmd = program
8
+ .command('contact-groups')
9
+ .description('Manage contact groups');
10
+ cmd
11
+ .command('list')
12
+ .description('List contact groups')
13
+ .option('--limit <n>', 'Max results', parsePositiveInt)
14
+ .option('--offset <n>', 'Offset', parseNonNegativeInt)
15
+ .option('--all', 'Fetch all pages')
16
+ .option('--api-key <key>', 'API key')
17
+ .option('--json', 'JSON output')
18
+ .action(apiAction(async (client, opts) => {
19
+ const result = await paginatedFetch(opts, (p) => listContactGroups(client, p), { label: 'Fetching contact groups' });
20
+ if (opts.json) {
21
+ console.log(paginatedJson(result, opts));
22
+ return;
23
+ }
24
+ console.log(chalk.bold(`Contact Groups (${result.data.length} of ${result.totalElements}):\n`));
25
+ const { items, overflow } = displaySlice(result.data);
26
+ for (const g of items)
27
+ console.log(` ${chalk.cyan(g.resourceId)} ${g.name} ${chalk.dim(`${g.associatedContacts.length} contacts`)}`);
28
+ if (overflow > 0)
29
+ console.log(chalk.dim(` ... and ${overflow} more`));
30
+ }));
31
+ cmd
32
+ .command('get <resourceId>')
33
+ .description('Get contact group details')
34
+ .option('--api-key <key>', 'API key')
35
+ .option('--json', 'JSON output')
36
+ .action((resourceId, opts) => apiAction(async (client) => {
37
+ const res = await getContactGroup(client, resourceId);
38
+ if (opts.json) {
39
+ console.log(JSON.stringify(res.data, null, 2));
40
+ return;
41
+ }
42
+ const g = res.data;
43
+ console.log(chalk.bold('Name:'), g.name);
44
+ console.log(chalk.bold('Contacts:'), g.associatedContacts.length);
45
+ for (const c of g.associatedContacts)
46
+ console.log(` ${chalk.cyan(c.resourceId)} ${c.name}`);
47
+ console.log(chalk.bold('ID:'), g.resourceId);
48
+ })(opts));
49
+ cmd
50
+ .command('search <query>')
51
+ .description('Search contact groups by name')
52
+ .option('--limit <n>', 'Max results', parsePositiveInt)
53
+ .option('--offset <n>', 'Offset', parseNonNegativeInt)
54
+ .option('--api-key <key>', 'API key')
55
+ .option('--json', 'JSON output')
56
+ .action((query, opts) => apiAction(async (client) => {
57
+ const result = await paginatedFetch(opts, ({ limit, offset }) => searchContactGroups(client, { filter: { name: { contains: query } }, limit, offset }), { label: 'Searching contact groups', defaultLimit: 20 });
58
+ if (opts.json) {
59
+ console.log(paginatedJson(result, opts));
60
+ return;
61
+ }
62
+ if (result.data.length === 0) {
63
+ console.log('No contact groups found.');
64
+ return;
65
+ }
66
+ console.log(chalk.bold(`Found ${result.data.length} group(s):\n`));
67
+ for (const g of result.data)
68
+ console.log(` ${chalk.cyan(g.resourceId)} ${g.name}`);
69
+ })(opts));
70
+ cmd
71
+ .command('create')
72
+ .description('Create a contact group')
73
+ .option('--name <name>', 'Group name')
74
+ .option('--api-key <key>', 'API key')
75
+ .option('--json', 'JSON output')
76
+ .action(apiAction(async (client, opts) => {
77
+ requireFields(opts, [{ flag: '--name', key: 'name' }]);
78
+ const res = await createContactGroup(client, { name: opts.name });
79
+ if (opts.json) {
80
+ console.log(JSON.stringify(res.data, null, 2));
81
+ return;
82
+ }
83
+ console.log(chalk.green(`Contact group "${opts.name}" created.`));
84
+ console.log(chalk.bold('ID:'), res.data.resourceId);
85
+ }));
86
+ }