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.
- package/README.md +13 -1
- package/assets/skills/api/SKILL.md +33 -8
- package/assets/skills/api/references/errors.md +8 -9
- package/assets/skills/api/references/field-map.md +13 -5
- package/assets/skills/conversion/SKILL.md +1 -1
- package/assets/skills/jobs/SKILL.md +1 -1
- package/assets/skills/transaction-recipes/SKILL.md +1 -1
- package/dist/commands/attachments.js +47 -6
- package/dist/commands/bank-rules.js +101 -0
- package/dist/commands/bank.js +63 -19
- package/dist/commands/contact-groups.js +86 -0
- package/dist/commands/custom-fields.js +60 -0
- package/dist/commands/exports.js +1 -1
- package/dist/commands/fixed-assets.js +153 -0
- package/dist/commands/inventory.js +50 -0
- package/dist/commands/schedulers.js +4 -0
- package/dist/commands/search.js +34 -0
- package/dist/commands/subscriptions.js +99 -0
- package/dist/core/api/attachments.js +2 -2
- package/dist/core/api/bank-rules.js +31 -0
- package/dist/core/api/bank.js +6 -0
- package/dist/core/api/contact-groups.js +12 -0
- package/dist/core/api/customer-cn.js +3 -0
- package/dist/core/api/fixed-assets.js +37 -0
- package/dist/core/api/index.js +6 -0
- package/dist/core/api/inventory.js +6 -0
- package/dist/core/api/reports.js +24 -0
- package/dist/core/api/search.js +6 -0
- package/dist/core/api/subscriptions.js +25 -0
- package/dist/core/api/tax-profiles.js +14 -0
- package/dist/core/jobs/bank-recon/tools/match/match.js +86 -63
- package/dist/core/registry/pagination.js +33 -0
- package/dist/core/registry/tools.js +916 -17
- package/dist/index.js +14 -0
- 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
|
|
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.
|
|
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
|
|
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
|
|
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**:
|
|
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
|
-
###
|
|
326
|
-
There is no JSON POST endpoint for creating bank records. Use multipart import:
|
|
325
|
+
### Bank record creation
|
|
327
326
|
|
|
328
|
-
|
|
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
|
-
|
|
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
|
-
| `
|
|
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
|
-
|
|
|
337
|
+
| `source` | `bankStatementEntrySource` | `FILE_IMPORT` or `API` (read-only) |
|
|
338
|
+
| `status` | `status` | `UNRECONCILED`, `RECONCILED`, `ARCHIVED`, `POSSIBLE_DUPLICATE` |
|
|
333
339
|
|
|
334
|
-
**Creating bank records**:
|
|
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 |
|
|
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.
|
|
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.
|
|
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.
|
|
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('--
|
|
42
|
-
.option('--url <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 --
|
|
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
|
+
}
|
package/dist/commands/bank.js
CHANGED
|
@@ -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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
|
86
|
+
filter,
|
|
82
87
|
limit: opts.limit ?? 50,
|
|
83
88
|
offset: 0,
|
|
84
|
-
sort: { sortBy: ['
|
|
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
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
+
}
|