jaz-clio 4.30.2 → 4.30.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: jaz-api
3
- version: 4.30.2
3
+ version: 4.30.6
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.
@@ -238,6 +238,9 @@ Bills, invoices, and credit notes share identical mandatory field specs. Adding
238
238
  ### Fixed Assets
239
239
  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.
240
240
  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`.
241
+ 92a. **Two ways to create fixed assets** — (1) **Register** (`POST /fixed-assets`): links to an existing purchase bill or journal. ACTIVE assets require `purchaseBusinessTransactionType` (`PURCHASE` or `JOURNAL_MANUAL`) and `purchaseBusinessTransactionResourceId`. Draft assets skip this validation. (2) **Transfer** (`POST /transfer-fixed-assets`): standalone asset entry, no linked transaction needed.
242
+ 92b. **`saveAsDraft` defaults to `true`** — To create an ACTIVE fixed asset, pass `saveAsDraft: false` with ALL required fields: `name`, `category`, `typeCode`, `purchaseAmount`, `purchaseDate`, `purchaseAssetAccountResourceId`, `depreciationMethod`, `effectiveLife`, and for `STRAIGHT_LINE`: `depreciationStartDate`, `accumulatedDepreciationAccountResourceId`, `depreciationExpenseAccountResourceId`. Omitting any returns 422.
243
+ 92c. **Valid enums** — `depreciationMethod`: `STRAIGHT_LINE`, `NO_DEPRECIATION`. `category`: `TANGIBLE`, `INTANGIBLE`. Optional string fields (`purchaseBusinessTransactionResourceId`, `accumulatedDepreciationAccountResourceId`, `capsuleResourceId`) can be safely omitted — the API ignores empty values.
241
244
 
242
245
  ### Subscriptions & Scheduled Transactions
243
246
  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).
@@ -1571,22 +1571,27 @@ POST /api/v1/fixed-assets
1571
1571
  ```json
1572
1572
  {
1573
1573
  "name": "Office Laptop - MacBook Pro",
1574
+ "purchaseAmount": 3500.00,
1574
1575
  "purchaseDate": "2026-01-15",
1575
- "purchaseCost": 3500.00,
1576
- "depreciationMethod": "STRAIGHT_LINE",
1577
- "usefulLifeMonths": 36,
1578
1576
  "depreciationStartDate": "2026-01-15",
1579
- "assetAccountResourceId": "fixed-asset-coa-uuid",
1580
- "depreciationAccountResourceId": "depreciation-coa-uuid",
1581
- "expenseAccountResourceId": "expense-coa-uuid",
1582
- "currencyCode": "SGD"
1577
+ "purchaseAssetAccountResourceId": "fixed-asset-coa-uuid",
1578
+ "depreciationMethod": "STRAIGHT_LINE",
1579
+ "effectiveLife": 36,
1580
+ "depreciableValueResidualAmount": 0,
1581
+ "depreciationExpenseAccountResourceId": "depreciation-expense-coa-uuid",
1582
+ "accumulatedDepreciationAccountResourceId": "accumulated-depreciation-coa-uuid",
1583
+ "saveAsDraft": true
1583
1584
  }
1584
1585
  ```
1585
1586
 
1586
- - `depreciationMethod`: `"STRAIGHT_LINE"` (uppercase)
1587
- - `usefulLifeMonths`: Integer
1588
- - `depreciationStartDate`: YYYY-MM-DD format (required — omitting causes "Invalid Depreciation date format" error). Typically same as `purchaseDate`.
1589
- - All three account fields must reference valid CoA entries
1587
+ - `purchaseDate` and `depreciationStartDate`: YYYY-MM-DD (both required — omitting returns 422)
1588
+ - `purchaseAmount`: Purchase cost (required)
1589
+ - `purchaseAssetAccountResourceId`: Asset account (required)
1590
+ - `depreciationMethod`: `"STRAIGHT_LINE"` or `"NO_DEPRECIATION"`
1591
+ - `effectiveLife`: Integer (months)
1592
+ - `category`: `"TANGIBLE"` or `"INTANGIBLE"`
1593
+ - `saveAsDraft`: Defaults to `true`. Set `false` to activate — requires `purchaseBusinessTransactionType` (`PURCHASE`/`JOURNAL_MANUAL`) + `purchaseBusinessTransactionResourceId`
1594
+ - Optional string fields (`purchaseBusinessTransactionResourceId`, `capsuleResourceId`) can be safely omitted for drafts
1590
1595
 
1591
1596
  ### Response
1592
1597
  ```json
@@ -426,7 +426,7 @@
426
426
  ### Fixed Assets
427
427
  | Method | Path | Description |
428
428
  |--------|------|-------------|
429
- | POST | `/fixed-assets` | Create (**known 500 bug**) |
429
+ | POST | `/fixed-assets` | Create (requires `purchaseDate` + `depreciationStartDate`) |
430
430
  | GET | `/fixed-assets` | List |
431
431
  | GET | `/fixed-assets/:resourceId` | Get by ID |
432
432
  | POST | `/fixed-assets/search` | Search |
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: jaz-conversion
3
- version: 4.30.2
3
+ version: 4.30.6
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.30.2
3
+ version: 4.30.6
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.30.2
3
+ version: 4.30.6
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.
@@ -63,16 +63,37 @@ export function registerBankRulesCommand(program) {
63
63
  })(opts));
64
64
  cmd
65
65
  .command('create')
66
- .description('Create a bank rule')
67
- .option('--input <file>', 'Read request body from JSON file')
66
+ .description('Create a bank reconciliation rule')
67
+ .option('--name <name>', 'Rule name (required)')
68
+ .option('--action-type <type>', 'Action type, e.g. RECONCILE_WITH_DIRECT_CASH_ENTRY (required)')
69
+ .option('--account <resourceId>', 'Bank account resourceId this rule applies to (required)')
70
+ .option('--config <json>', 'Rule configuration as JSON (allocation type, accounts, percentages, tax)')
71
+ .option('--input <file>', 'Read full request body from JSON file (overrides flags)')
68
72
  .option('--api-key <key>', 'API key')
69
73
  .option('--format <type>', 'Output format: table, json, csv, yaml')
70
74
  .option('--json', 'JSON output')
71
75
  .action(apiAction(async (client, opts) => {
72
- const body = readBodyInput(opts);
76
+ let body = readBodyInput(opts);
73
77
  if (!body) {
74
- console.error(chalk.red('Use --input <file> to provide rule configuration.'));
75
- process.exit(1);
78
+ if (!opts.name || !opts.actionType || !opts.account) {
79
+ console.error(chalk.red('Required: --name, --action-type, --account'));
80
+ console.error(chalk.dim('Or use --input <file> to provide full JSON body.'));
81
+ process.exit(1);
82
+ }
83
+ body = {
84
+ name: opts.name,
85
+ actionType: opts.actionType,
86
+ appliesToReconciliationAccountResourceId: opts.account,
87
+ };
88
+ if (opts.config) {
89
+ try {
90
+ body.configuration = JSON.parse(opts.config);
91
+ }
92
+ catch {
93
+ console.error(chalk.red('Invalid --config JSON'));
94
+ process.exit(1);
95
+ }
96
+ }
76
97
  }
77
98
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
78
99
  const res = await createBankRule(client, body);
@@ -87,7 +108,8 @@ export function registerBankRulesCommand(program) {
87
108
  .command('update <resourceId>')
88
109
  .description('Update a bank rule')
89
110
  .option('--name <name>', 'New name')
90
- .option('--input <file>', 'Read full update body from JSON file')
111
+ .option('--config <json>', 'Updated configuration as JSON')
112
+ .option('--input <file>', 'Read full update body from JSON file (overrides flags)')
91
113
  .option('--api-key <key>', 'API key')
92
114
  .option('--format <type>', 'Output format: table, json, csv, yaml')
93
115
  .option('--json', 'JSON output')
@@ -101,6 +123,15 @@ export function registerBankRulesCommand(program) {
101
123
  data = {};
102
124
  if (opts.name !== undefined)
103
125
  data.name = opts.name;
126
+ if (opts.config) {
127
+ try {
128
+ data.configuration = JSON.parse(opts.config);
129
+ }
130
+ catch {
131
+ console.error(chalk.red('Invalid --config JSON'));
132
+ process.exit(1);
133
+ }
134
+ }
104
135
  }
105
136
  const res = await updateBankRule(client, resourceId, data);
106
137
  if (opts.json) {
@@ -374,7 +374,7 @@ export function registerBillsCommand(program) {
374
374
  let attachmentCount = 0;
375
375
  try {
376
376
  const attRes = await listAttachments(client, 'bills', b.resourceId);
377
- attachmentCount = Array.isArray(attRes.data) ? attRes.data.length : 0;
377
+ attachmentCount = attRes.data.length;
378
378
  }
379
379
  catch {
380
380
  // Attachment listing may fail for some bills — don't block the report
@@ -558,7 +558,7 @@ export function registerBillsCommand(program) {
558
558
  const billRes = await getBill(client, resourceId);
559
559
  const bill = billRes.data;
560
560
  const attRes = await listAttachments(client, 'bills', resourceId);
561
- const attachments = Array.isArray(attRes.data) ? attRes.data : [];
561
+ const attachments = attRes.data;
562
562
  if (opts.json) {
563
563
  console.log(JSON.stringify({
564
564
  billResourceId: resourceId,
@@ -345,7 +345,7 @@ export function registerCustomerCreditNotesCommand(program) {
345
345
  let attachmentCount = 0;
346
346
  try {
347
347
  const attRes = await listAttachments(client, 'customer-credit-notes', cn.resourceId);
348
- attachmentCount = Array.isArray(attRes.data) ? attRes.data.length : 0;
348
+ attachmentCount = attRes.data.length;
349
349
  }
350
350
  catch { /* Attachment listing may fail — don't block the report */ }
351
351
  return buildDraftReport(cn, CREDIT_NOTE_REQUIRED_FIELDS, attachmentCount);
@@ -496,7 +496,7 @@ export function registerCustomerCreditNotesCommand(program) {
496
496
  const cnRes = await getCustomerCreditNote(client, resourceId);
497
497
  const cn = cnRes.data;
498
498
  const attRes = await listAttachments(client, 'customer-credit-notes', resourceId);
499
- const attachments = Array.isArray(attRes.data) ? attRes.data : [];
499
+ const attachments = attRes.data;
500
500
  if (opts.json) {
501
501
  console.log(JSON.stringify({ creditNoteResourceId: resourceId, creditNoteReference: cn.reference || null, attachments }, null, 2));
502
502
  }
@@ -1,7 +1,7 @@
1
1
  import chalk from 'chalk';
2
2
  import { listFixedAssets, getFixedAsset, searchFixedAssets, createFixedAsset, updateFixedAsset, deleteFixedAsset, discardFixedAsset, markFixedAssetSold, transferFixedAsset, undoFixedAssetDisposal, } from '../core/api/fixed-assets.js';
3
3
  import { apiAction } from './api-action.js';
4
- import { parsePositiveInt, parseNonNegativeInt, readBodyInput, parseCustomFields } from './parsers.js';
4
+ import { parsePositiveInt, parseNonNegativeInt, parseMoney, readBodyInput, parseCustomFields } from './parsers.js';
5
5
  import { paginatedFetch } from './pagination.js';
6
6
  import { outputList } from './output.js';
7
7
  import { formatId, formatCurrency } from './format-helpers.js';
@@ -84,16 +84,69 @@ export function registerFixedAssetsCommand(program) {
84
84
  cmd
85
85
  .command('create')
86
86
  .description('Register a new fixed asset')
87
- .option('--input <file>', 'Read request body from JSON file')
88
- .option('--custom-fields <json>', 'Custom field values as JSON array: [{\"customFieldName\":\"PO Number\",\"actualValue\":\"PO-123\"}]')
87
+ .option('--name <name>', 'Asset name (required)')
88
+ .option('--type <typeName>', 'Asset type (e.g., "Buildings", "Vehicles", "Furniture")')
89
+ .option('--category <cat>', 'TANGIBLE or INTANGIBLE')
90
+ .option('--amount <n>', 'Purchase cost (required)', parseMoney)
91
+ .option('--date <YYYY-MM-DD>', 'Purchase date (required)')
92
+ .option('--depreciation-start <YYYY-MM-DD>', 'Depreciation start date (required)')
93
+ .option('--asset-account <id>', 'Asset account resourceId (required)')
94
+ .option('--depreciation-method <method>', 'STRAIGHT_LINE or NO_DEPRECIATION')
95
+ .option('--effective-life <months>', 'Effective life in months', parsePositiveInt)
96
+ .option('--residual <n>', 'Residual/salvage value', parseMoney)
97
+ .option('--depreciation-expense-account <id>', 'Depreciation expense account resourceId')
98
+ .option('--accumulated-depreciation-account <id>', 'Accumulated depreciation account resourceId')
99
+ .option('--purchase-bt-type <type>', 'Linked purchase BT type: PURCHASE (bill) or JOURNAL_MANUAL')
100
+ .option('--purchase-bt-id <id>', 'Linked purchase BT resourceId (required for ACTIVE assets)')
101
+ .option('--notes <text>', 'Internal notes')
102
+ .option('--draft', 'Save as draft (default: true)')
103
+ .option('--finalize', 'Activate immediately (saveAsDraft: false)')
104
+ .option('--tag <name>', 'Tag name')
105
+ .option('--custom-fields <json>', 'Custom field values as JSON array')
106
+ .option('--input <file>', 'Read full request body from JSON file (overrides flags)')
89
107
  .option('--format <type>', 'Output format: table, json, csv, yaml')
90
108
  .option('--api-key <key>', 'API key')
91
109
  .option('--json', 'JSON output')
92
110
  .action(apiAction(async (client, opts) => {
93
- const body = readBodyInput(opts);
111
+ let body = readBodyInput(opts);
94
112
  if (!body) {
95
- console.error(chalk.red('Use --input <file> to provide asset data.'));
96
- process.exit(1);
113
+ // Build body from flags
114
+ if (!opts.name || opts.amount === undefined || !opts.date || !opts.assetAccount || !opts.depreciationStart) {
115
+ console.error(chalk.red('Required: --name, --amount, --date, --depreciation-start, --asset-account'));
116
+ console.error(chalk.dim('Or use --input <file> to provide full JSON body.'));
117
+ process.exit(1);
118
+ }
119
+ body = {
120
+ name: opts.name,
121
+ purchaseAmount: opts.amount,
122
+ purchaseDate: opts.date,
123
+ depreciationStartDate: opts.depreciationStart,
124
+ purchaseAssetAccountResourceId: opts.assetAccount,
125
+ };
126
+ if (opts.type)
127
+ body.typeName = opts.type;
128
+ if (opts.category)
129
+ body.category = opts.category;
130
+ if (opts.depreciationMethod)
131
+ body.depreciationMethod = opts.depreciationMethod;
132
+ if (opts.effectiveLife)
133
+ body.effectiveLife = opts.effectiveLife;
134
+ if (opts.residual !== undefined)
135
+ body.depreciableValueResidualAmount = opts.residual;
136
+ if (opts.depreciationExpenseAccount)
137
+ body.depreciationExpenseAccountResourceId = opts.depreciationExpenseAccount;
138
+ if (opts.accumulatedDepreciationAccount)
139
+ body.accumulatedDepreciationAccountResourceId = opts.accumulatedDepreciationAccount;
140
+ if (opts.purchaseBtType)
141
+ body.purchaseBusinessTransactionType = opts.purchaseBtType;
142
+ if (opts.purchaseBtId)
143
+ body.purchaseBusinessTransactionResourceId = opts.purchaseBtId;
144
+ if (opts.notes)
145
+ body.internalNotes = opts.notes;
146
+ if (opts.tag)
147
+ body.tags = [opts.tag];
148
+ if (opts.finalize)
149
+ body.saveAsDraft = false;
97
150
  }
98
151
  if (opts.customFields)
99
152
  body.customFields = parseCustomFields(opts.customFields);
@@ -124,8 +177,12 @@ export function registerFixedAssetsCommand(program) {
124
177
  .command('update <resourceId>')
125
178
  .description('Update a fixed asset')
126
179
  .option('--name <name>', 'New name')
180
+ .option('--notes <text>', 'Internal notes')
181
+ .option('--depreciation-method <method>', 'STRAIGHT_LINE or NO_DEPRECIATION')
182
+ .option('--effective-life <months>', 'Effective life in months', parsePositiveInt)
183
+ .option('--tag <name>', 'Tag name')
127
184
  .option('--custom-fields <json>', 'Custom field values as JSON array')
128
- .option('--input <file>', 'Read full update body from JSON file')
185
+ .option('--input <file>', 'Read full update body from JSON file (overrides flags)')
129
186
  .option('--format <type>', 'Output format: table, json, csv, yaml')
130
187
  .option('--api-key <key>', 'API key')
131
188
  .option('--json', 'JSON output')
@@ -139,9 +196,17 @@ export function registerFixedAssetsCommand(program) {
139
196
  data = {};
140
197
  if (opts.name !== undefined)
141
198
  data.name = opts.name;
142
- if (opts.customFields)
143
- data.customFields = parseCustomFields(opts.customFields);
199
+ if (opts.notes !== undefined)
200
+ data.internalNotes = opts.notes;
201
+ if (opts.depreciationMethod)
202
+ data.depreciationMethod = opts.depreciationMethod;
203
+ if (opts.effectiveLife)
204
+ data.effectiveLife = opts.effectiveLife;
205
+ if (opts.tag)
206
+ data.tags = [opts.tag];
144
207
  }
208
+ if (opts.customFields)
209
+ data.customFields = parseCustomFields(opts.customFields);
145
210
  const res = await updateFixedAsset(client, resourceId, data);
146
211
  if (opts.json) {
147
212
  console.log(JSON.stringify(res.data, null, 2));
@@ -191,16 +256,30 @@ export function registerFixedAssetsCommand(program) {
191
256
  }));
192
257
  cmd
193
258
  .command('transfer')
194
- .description('Transfer a fixed asset')
195
- .option('--input <file>', 'Read transfer body from JSON file')
259
+ .description('Transfer a fixed asset to a different type/account')
260
+ .option('--id <resourceId>', 'Source fixed asset resourceId (required)')
261
+ .option('--name <name>', 'New asset name')
262
+ .option('--type <typeName>', 'New asset type')
263
+ .option('--asset-account <id>', 'New asset account resourceId')
264
+ .option('--input <file>', 'Read full transfer body from JSON file (overrides flags)')
196
265
  .option('--format <type>', 'Output format: table, json, csv, yaml')
197
266
  .option('--api-key <key>', 'API key')
198
267
  .option('--json', 'JSON output')
199
268
  .action(apiAction(async (client, opts) => {
200
- const body = readBodyInput(opts);
269
+ let body = readBodyInput(opts);
201
270
  if (!body) {
202
- console.error(chalk.red('Use --input <file> to provide transfer data.'));
203
- process.exit(1);
271
+ if (!opts.id) {
272
+ console.error(chalk.red('Required: --id <resourceId>'));
273
+ console.error(chalk.dim('Or use --input <file> to provide full JSON body.'));
274
+ process.exit(1);
275
+ }
276
+ body = { resourceId: opts.id };
277
+ if (opts.name)
278
+ body.name = opts.name;
279
+ if (opts.type)
280
+ body.typeName = opts.type;
281
+ if (opts.assetAccount)
282
+ body.purchaseAssetAccountResourceId = opts.assetAccount;
204
283
  }
205
284
  const res = await transferFixedAsset(client, body);
206
285
  if (opts.json) {
@@ -387,7 +387,7 @@ export function registerInvoicesCommand(program) {
387
387
  let attachmentCount = 0;
388
388
  try {
389
389
  const attRes = await listAttachments(client, 'invoices', inv.resourceId);
390
- attachmentCount = Array.isArray(attRes.data) ? attRes.data.length : 0;
390
+ attachmentCount = attRes.data.length;
391
391
  }
392
392
  catch { /* Attachment listing may fail — don't block the report */ }
393
393
  return buildDraftReport(inv, INVOICE_REQUIRED_FIELDS, attachmentCount);
@@ -539,7 +539,7 @@ export function registerInvoicesCommand(program) {
539
539
  const invRes = await getInvoice(client, resourceId);
540
540
  const inv = invRes.data;
541
541
  const attRes = await listAttachments(client, 'invoices', resourceId);
542
- const attachments = Array.isArray(attRes.data) ? attRes.data : [];
542
+ const attachments = attRes.data;
543
543
  if (opts.json) {
544
544
  console.log(JSON.stringify({ invoiceResourceId: resourceId, invoiceReference: inv.reference || null, attachments }, null, 2));
545
545
  }
@@ -242,7 +242,7 @@ export function registerJournalsCommand(program) {
242
242
  let attachmentCount = 0;
243
243
  try {
244
244
  const attRes = await listAttachments(client, 'journals', j.resourceId);
245
- attachmentCount = Array.isArray(attRes.data) ? attRes.data.length : 0;
245
+ attachmentCount = attRes.data.length;
246
246
  }
247
247
  catch { /* Attachment listing may fail — don't block the report */ }
248
248
  return buildDraftReport(j, JOURNAL_REQUIRED_FIELDS, attachmentCount, 'journalEntries');
@@ -377,7 +377,7 @@ export function registerJournalsCommand(program) {
377
377
  const journalRes = await getJournal(client, resourceId);
378
378
  const journal = journalRes.data;
379
379
  const attRes = await listAttachments(client, 'journals', resourceId);
380
- const attachments = Array.isArray(attRes.data) ? attRes.data : [];
380
+ const attachments = attRes.data;
381
381
  if (opts.json) {
382
382
  console.log(JSON.stringify({ journalResourceId: resourceId, journalReference: journal.reference || null, attachments }, null, 2));
383
383
  }
@@ -1,7 +1,7 @@
1
1
  import chalk from 'chalk';
2
2
  import { listSubscriptions, getSubscription, createSubscription, updateSubscription, deleteSubscription, cancelSubscription, searchScheduledTransactions, } from '../core/api/subscriptions.js';
3
3
  import { apiAction } from './api-action.js';
4
- import { parsePositiveInt, parseNonNegativeInt, readBodyInput } from './parsers.js';
4
+ import { parsePositiveInt, parseNonNegativeInt, parseMoney, readBodyInput } from './parsers.js';
5
5
  import { paginatedFetch } from './pagination.js';
6
6
  import { outputList } from './output.js';
7
7
  import { formatId } from './format-helpers.js';
@@ -56,16 +56,59 @@ export function registerSubscriptionsCommand(program) {
56
56
  })(opts));
57
57
  cmd
58
58
  .command('create')
59
- .description('Create a subscription')
60
- .option('--input <file>', 'Read request body from JSON file')
59
+ .description('Create a recurring subscription')
60
+ .option('--type <type>', 'SALE (invoice) or PURCHASE (bill) (required)')
61
+ .option('--interval <interval>', 'WEEKLY, MONTHLY, or YEARLY (required)')
62
+ .option('--start-date <YYYY-MM-DD>', 'Start date (required)')
63
+ .option('--end-date <YYYY-MM-DD>', 'End date (optional, omit for ongoing)')
64
+ .option('--contact <resourceId>', 'Contact resourceId (required)')
65
+ .option('--ref <reference>', 'Reference / invoice number')
66
+ .option('--date <YYYY-MM-DD>', 'Transaction value date')
67
+ .option('--due <YYYY-MM-DD>', 'Due date')
68
+ .option('--lines <json>', 'Line items as JSON array')
69
+ .option('--amount <n>', 'Single line item amount (shorthand)', parseMoney)
70
+ .option('--account <resourceId>', 'Line item account resourceId (used with --amount)')
71
+ .option('--line-name <name>', 'Line item name (used with --amount)')
72
+ .option('--input <file>', 'Read full request body from JSON file (overrides flags)')
61
73
  .option('--format <type>', 'Output format: table, json, csv, yaml')
62
74
  .option('--api-key <key>', 'API key')
63
75
  .option('--json', 'JSON output')
64
76
  .action(apiAction(async (client, opts) => {
65
- const body = readBodyInput(opts);
77
+ let body = readBodyInput(opts);
66
78
  if (!body) {
67
- console.error(chalk.red('Use --input <file> to provide subscription data.'));
68
- process.exit(1);
79
+ if (!opts.type || !opts.interval || !opts.startDate) {
80
+ console.error(chalk.red('Required: --type, --interval, --start-date'));
81
+ console.error(chalk.dim('Or use --input <file> to provide full JSON body.'));
82
+ process.exit(1);
83
+ }
84
+ // Build line items from --lines or --amount shorthand
85
+ let lineItems;
86
+ if (opts.lines) {
87
+ try {
88
+ lineItems = JSON.parse(opts.lines);
89
+ }
90
+ catch {
91
+ console.error(chalk.red('Invalid --lines JSON'));
92
+ process.exit(1);
93
+ }
94
+ }
95
+ else if (opts.amount !== undefined) {
96
+ lineItems = [{ name: opts.lineName ?? 'Subscription', unitPrice: opts.amount, quantity: 1, accountResourceId: opts.account }];
97
+ }
98
+ body = {
99
+ businessTransactionType: opts.type,
100
+ interval: opts.interval,
101
+ startDate: opts.startDate,
102
+ status: 'ACTIVE',
103
+ contactResourceId: opts.contact,
104
+ reference: opts.ref,
105
+ valueDate: opts.date,
106
+ dueDate: opts.due,
107
+ lineItems,
108
+ saveAsDraft: false,
109
+ };
110
+ if (opts.endDate)
111
+ body.endDate = opts.endDate;
69
112
  }
70
113
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
71
114
  const res = await createSubscription(client, body);
@@ -329,7 +329,7 @@ export function registerSupplierCreditNotesCommand(program) {
329
329
  let attachmentCount = 0;
330
330
  try {
331
331
  const attRes = await listAttachments(client, 'supplier-credit-notes', cn.resourceId);
332
- attachmentCount = Array.isArray(attRes.data) ? attRes.data.length : 0;
332
+ attachmentCount = attRes.data.length;
333
333
  }
334
334
  catch { /* Attachment listing may fail — don't block the report */ }
335
335
  return buildDraftReport(cn, CREDIT_NOTE_REQUIRED_FIELDS, attachmentCount);
@@ -480,7 +480,7 @@ export function registerSupplierCreditNotesCommand(program) {
480
480
  const cnRes = await getSupplierCreditNote(client, resourceId);
481
481
  const cn = cnRes.data;
482
482
  const attRes = await listAttachments(client, 'supplier-credit-notes', resourceId);
483
- const attachments = Array.isArray(attRes.data) ? attRes.data : [];
483
+ const attachments = attRes.data;
484
484
  if (opts.json) {
485
485
  console.log(JSON.stringify({ creditNoteResourceId: resourceId, creditNoteReference: cn.reference || null, attachments }, null, 2));
486
486
  }
@@ -3,7 +3,10 @@
3
3
  * Works for bills, invoices, journals, and credit notes.
4
4
  */
5
5
  export async function listAttachments(client, btType, btResourceId) {
6
- return client.get(`/api/v1/${btType}/${btResourceId}/attachments`);
6
+ const result = await client.get(`/api/v1/${btType}/${btResourceId}/attachments`);
7
+ // Guard all consumers (CLI, MCP, daemon): client.get() returns undefined on 204,
8
+ // and some endpoints return {} or { data: null } — always ensure .data is an array.
9
+ return { data: Array.isArray(result?.data) ? result.data : [] };
7
10
  }
8
11
  export async function addAttachment(client, data) {
9
12
  const { businessTransactionType: btType, businessTransactionResourceId: btId, ...rest } = data;
@@ -53,7 +53,7 @@ import { generateFaReviewBlueprint } from '../jobs/fa-review/blueprint.js';
53
53
  import { generateDocumentCollectionBlueprint } from '../jobs/document-collection/blueprint.js';
54
54
  import { generateStatutoryFilingBlueprint } from '../jobs/statutory-filing/blueprint.js';
55
55
  // Draft validation (pure logic from core/drafts/)
56
- import { buildDraftReport, INVOICE_REQUIRED_FIELDS, BILL_REQUIRED_FIELDS, CREDIT_NOTE_REQUIRED_FIELDS, JOURNAL_REQUIRED_FIELDS, } from '../drafts/index.js';
56
+ import { validateDraft, buildDraftReport, INVOICE_REQUIRED_FIELDS, BILL_REQUIRED_FIELDS, CREDIT_NOTE_REQUIRED_FIELDS, JOURNAL_REQUIRED_FIELDS, } from '../drafts/index.js';
57
57
  // ── Shared param snippets (DRY) ─────────────────────────────────
58
58
  const PAGINATION_PARAMS = {
59
59
  limit: { type: 'number', description: 'Max results per page (default 20, max 1000).' },
@@ -103,6 +103,20 @@ const CLASSIFIER_CONFIG_PARAM = {
103
103
  },
104
104
  description: 'Nano classifier config for line items. Each entry links a capsule type with selected classes.',
105
105
  };
106
+ const JOURNAL_ENTRY_PARAM = {
107
+ type: 'array',
108
+ items: {
109
+ type: 'object',
110
+ properties: {
111
+ accountResourceId: { type: 'string', description: 'Account resourceId' },
112
+ type: { type: 'string', enum: ['DEBIT', 'CREDIT'], description: 'Debit or credit' },
113
+ amount: { type: 'number', description: 'Amount' },
114
+ description: { type: 'string', description: 'Line description' },
115
+ },
116
+ required: ['accountResourceId', 'type', 'amount'],
117
+ },
118
+ description: 'Journal entries (debit/credit lines with accountResourceId, type, amount)',
119
+ };
106
120
  const LINE_ITEM_PARAM = {
107
121
  type: 'array',
108
122
  items: {
@@ -279,27 +293,26 @@ async function fetchAndMerge(client, type, resourceId, overrides) {
279
293
  if (v !== undefined)
280
294
  base[k] = v;
281
295
  }
282
- // Auto-resolve missing accountResourceId for finalize (exact name match, auditable)
283
- const items = base.lineItems;
284
- if (items?.length) {
285
- const missing = items.filter(li => !li.accountResourceId);
286
- if (missing.length > 0) {
287
- const defaultName = type === 'invoice' || type === 'customer_credit_note'
288
- ? 'Operating Revenue' : 'Operating Expense';
289
- const acctResult = await searchAccounts(client, { filter: { name: { eq: defaultName } }, limit: 1 });
290
- const defaultAcct = acctResult.data?.[0];
291
- if (defaultAcct?.resourceId) {
292
- for (const li of missing)
293
- li.accountResourceId = defaultAcct.resourceId;
294
- }
295
- const stillMissing = items.filter(li => !li.accountResourceId);
296
- if (stillMissing.length > 0) {
297
- throw new Error(`Cannot finalize: ${stillMissing.length} line item(s) missing accountResourceId and no default account found.`);
298
- }
299
- }
296
+ // Validate draft readiness reuses shared draft validation (DRY with CLI).
297
+ // fetchAndMerge is only called by finalize tools, so always validate.
298
+ const specs = type === 'invoice' ? INVOICE_REQUIRED_FIELDS
299
+ : type === 'bill' ? BILL_REQUIRED_FIELDS
300
+ : CREDIT_NOTE_REQUIRED_FIELDS;
301
+ const { missingFields, ready } = validateDraft(base, specs);
302
+ if (!ready) {
303
+ throw new Error(`Cannot finalize: missing ${missingFields.join(', ')}. ` +
304
+ `Use search_accounts (filter by accountType) and search_contacts to resolve, ` +
305
+ `then pass the missing fields to this tool.`);
300
306
  }
301
307
  return base;
302
308
  }
309
+ /** Advisory pre-flight: reject payment/refund against DRAFT documents (API is authoritative). */
310
+ async function assertNotDraft(getter, client, resourceId, kind) {
311
+ const res = await getter(client, resourceId);
312
+ if (res.data.status === 'DRAFT') {
313
+ throw new Error(`Cannot pay a DRAFT ${kind}. Finalize it first with finalize_${kind}.`);
314
+ }
315
+ }
303
316
  // ── Tool Definitions ─────────────────────────────────────────────
304
317
  export const TOOL_DEFINITIONS = [
305
318
  // ── Organization ───────────────────────────────────────────────
@@ -470,7 +483,14 @@ export const TOOL_DEFINITIONS = [
470
483
  execute: async (ctx, input) => {
471
484
  const { resourceId: rid, ...updates } = input;
472
485
  const { data: existing } = await getContact(ctx.client, rid);
473
- return updateContact(ctx.client, rid, { ...existing, ...updates });
486
+ // Filter to writable fields only — GET returns read-only fields (resourceId, createdAt, etc.)
487
+ // that cause 400 "Invalid request body" if included in PUT. Same pattern as update_account.
488
+ const CONTACT_WRITABLE = ['billingName', 'name', 'emails', 'customer', 'supplier',
489
+ 'taxRegistrationNumber', 'address', 'phone', 'status'];
490
+ const ex = existing;
491
+ const base = Object.fromEntries(CONTACT_WRITABLE.filter(k => ex[k] !== undefined && ex[k] !== null).map(k => [k, ex[k]]));
492
+ Object.assign(base, updates);
493
+ return updateContact(ctx.client, rid, base);
474
494
  },
475
495
  },
476
496
  // ── Invoices ───────────────────────────────────────────────────
@@ -544,13 +564,13 @@ export const TOOL_DEFINITIONS = [
544
564
  },
545
565
  {
546
566
  name: 'update_invoice',
547
- description: 'Update an existing draft invoice. Cannot update finalized invoices.',
567
+ description: 'Update an existing draft invoice (change reference, dates, line items, notes, custom fields). Use when the user says "update", "change", "fix", or "correct" a draft invoice. Line items CAN be fully replaced — pass the complete updated lineItems array.',
548
568
  params: {
549
569
  resourceId: { type: 'string', description: 'Invoice resourceId' },
550
570
  reference: { type: 'string' },
551
571
  valueDate: { type: 'string' },
552
572
  dueDate: { type: 'string' },
553
- lineItems: { type: 'array', items: { type: 'object' } },
573
+ lineItems: LINE_ITEM_PARAM,
554
574
  notes: { type: 'string' },
555
575
  customFields: CUSTOM_FIELDS_PARAM,
556
576
  },
@@ -591,6 +611,7 @@ export const TOOL_DEFINITIONS = [
591
611
  group: 'invoices',
592
612
  readOnly: false,
593
613
  execute: async (ctx, input) => {
614
+ const resourceId = input.resourceId;
594
615
  const payAmt = Number(input.paymentAmount);
595
616
  if (!Number.isFinite(payAmt) || payAmt <= 0) {
596
617
  throw new Error('paymentAmount must be a positive number');
@@ -599,7 +620,8 @@ export const TOOL_DEFINITIONS = [
599
620
  if (!Number.isFinite(txnAmt) || txnAmt <= 0) {
600
621
  throw new Error('transactionAmount must be a positive number');
601
622
  }
602
- return createInvoicePayment(ctx.client, input.resourceId, {
623
+ await assertNotDraft(getInvoice, ctx.client, resourceId, 'invoice');
624
+ return createInvoicePayment(ctx.client, resourceId, {
603
625
  paymentAmount: payAmt,
604
626
  transactionAmount: txnAmt,
605
627
  accountResourceId: input.accountResourceId,
@@ -620,7 +642,7 @@ export const TOOL_DEFINITIONS = [
620
642
  reference: { type: 'string' },
621
643
  valueDate: { type: 'string' },
622
644
  dueDate: { type: 'string' },
623
- lineItems: { type: 'array', items: { type: 'object' } },
645
+ lineItems: LINE_ITEM_PARAM,
624
646
  notes: { type: 'string' },
625
647
  },
626
648
  required: ['resourceId'],
@@ -634,7 +656,7 @@ export const TOOL_DEFINITIONS = [
634
656
  },
635
657
  {
636
658
  name: 'apply_credits_to_invoice',
637
- description: 'Apply customer credit note(s) to an invoice. Each credit needs creditNoteResourceId and amountApplied.',
659
+ description: 'Apply customer credit note(s) to an invoice. IMPORTANT: The credit note must be FINALIZED first (status UNAPPLIED, not DRAFT). If it is still a draft, call finalize_customer_credit_note first. Then search_customer_credit_notes with status UNAPPLIED to find available credits for the contact, and pass creditNoteResourceId and amountApplied for each.',
638
660
  params: {
639
661
  resourceId: { type: 'string', description: 'Invoice resourceId' },
640
662
  credits: {
@@ -731,13 +753,13 @@ export const TOOL_DEFINITIONS = [
731
753
  },
732
754
  {
733
755
  name: 'update_bill',
734
- description: 'Update an existing draft bill. Cannot update finalized bills.',
756
+ description: 'Update an existing draft bill (change reference, dates, line items, notes, custom fields). Use when the user says "update", "change", "fix", or "correct" a draft bill. Line items CAN be fully replaced — pass the complete updated lineItems array.',
735
757
  params: {
736
758
  resourceId: { type: 'string', description: 'Bill resourceId' },
737
759
  reference: { type: 'string' },
738
760
  valueDate: { type: 'string' },
739
761
  dueDate: { type: 'string' },
740
- lineItems: { type: 'array', items: { type: 'object' } },
762
+ lineItems: LINE_ITEM_PARAM,
741
763
  notes: { type: 'string' },
742
764
  customFields: CUSTOM_FIELDS_PARAM,
743
765
  },
@@ -774,6 +796,7 @@ export const TOOL_DEFINITIONS = [
774
796
  group: 'bills',
775
797
  readOnly: false,
776
798
  execute: async (ctx, input) => {
799
+ const billResourceId = input.resourceId;
777
800
  const billPayAmt = Number(input.paymentAmount);
778
801
  if (!Number.isFinite(billPayAmt) || billPayAmt <= 0) {
779
802
  throw new Error('paymentAmount must be a positive number');
@@ -782,7 +805,8 @@ export const TOOL_DEFINITIONS = [
782
805
  if (!Number.isFinite(billTxnAmt) || billTxnAmt <= 0) {
783
806
  throw new Error('transactionAmount must be a positive number');
784
807
  }
785
- return createBillPayment(ctx.client, input.resourceId, {
808
+ await assertNotDraft(getBill, ctx.client, billResourceId, 'bill');
809
+ return createBillPayment(ctx.client, billResourceId, {
786
810
  paymentAmount: billPayAmt,
787
811
  transactionAmount: billTxnAmt,
788
812
  accountResourceId: input.accountResourceId,
@@ -803,7 +827,7 @@ export const TOOL_DEFINITIONS = [
803
827
  reference: { type: 'string' },
804
828
  valueDate: { type: 'string' },
805
829
  dueDate: { type: 'string' },
806
- lineItems: { type: 'array', items: { type: 'object' } },
830
+ lineItems: LINE_ITEM_PARAM,
807
831
  notes: { type: 'string' },
808
832
  },
809
833
  required: ['resourceId'],
@@ -817,7 +841,7 @@ export const TOOL_DEFINITIONS = [
817
841
  },
818
842
  {
819
843
  name: 'apply_credits_to_bill',
820
- description: 'Apply supplier credit note(s) to a bill. Each credit needs creditNoteResourceId and amountApplied.',
844
+ description: 'Apply supplier credit note(s) to a bill. IMPORTANT: The credit note must be FINALIZED first (status UNAPPLIED, not DRAFT). If it is still a draft, call finalize_supplier_credit_note first. Then search_supplier_credit_notes with status UNAPPLIED to find available credits for the contact, and pass creditNoteResourceId and amountApplied for each.',
821
845
  params: {
822
846
  resourceId: { type: 'string', description: 'Bill resourceId' },
823
847
  credits: {
@@ -875,19 +899,7 @@ export const TOOL_DEFINITIONS = [
875
899
  params: {
876
900
  reference: { type: 'string', description: 'Journal reference' },
877
901
  valueDate: { type: 'string', description: 'Journal date (YYYY-MM-DD)' },
878
- journalEntries: {
879
- type: 'array',
880
- items: {
881
- type: 'object',
882
- properties: {
883
- accountResourceId: { type: 'string' },
884
- type: { type: 'string', enum: ['DEBIT', 'CREDIT'] },
885
- amount: { type: 'number' },
886
- description: { type: 'string' },
887
- },
888
- required: ['accountResourceId', 'type', 'amount'],
889
- },
890
- },
902
+ journalEntries: JOURNAL_ENTRY_PARAM,
891
903
  saveAsDraft: { type: 'boolean' },
892
904
  notes: { type: 'string' },
893
905
  tags: { type: 'array', items: { type: 'string' }, description: 'Tags (string array)' },
@@ -1352,12 +1364,12 @@ export const TOOL_DEFINITIONS = [
1352
1364
  },
1353
1365
  {
1354
1366
  name: 'update_customer_credit_note',
1355
- description: 'Update a draft customer credit note.',
1367
+ description: 'Update a draft customer credit note (change amount, line items, contact, date, notes). Use when the user says "update", "change", "fix", or "correct" a credit note.',
1356
1368
  params: {
1357
1369
  resourceId: { type: 'string', description: 'Customer credit note resourceId' },
1358
1370
  reference: { type: 'string' },
1359
1371
  valueDate: { type: 'string' },
1360
- lineItems: { type: 'array', items: { type: 'object' } },
1372
+ lineItems: LINE_ITEM_PARAM,
1361
1373
  notes: { type: 'string' },
1362
1374
  tag: { type: 'string' },
1363
1375
  customFields: CUSTOM_FIELDS_PARAM,
@@ -1377,7 +1389,7 @@ export const TOOL_DEFINITIONS = [
1377
1389
  resourceId: { type: 'string', description: 'Customer credit note resourceId' },
1378
1390
  reference: { type: 'string' },
1379
1391
  valueDate: { type: 'string' },
1380
- lineItems: { type: 'array', items: { type: 'object' } },
1392
+ lineItems: LINE_ITEM_PARAM,
1381
1393
  notes: { type: 'string' },
1382
1394
  },
1383
1395
  required: ['resourceId'],
@@ -1527,12 +1539,12 @@ export const TOOL_DEFINITIONS = [
1527
1539
  },
1528
1540
  {
1529
1541
  name: 'update_supplier_credit_note',
1530
- description: 'Update a draft supplier credit note.',
1542
+ description: 'Update a draft supplier credit note (change amount, line items, contact, date, notes). Use when the user says "update", "change", "fix", or "correct" a credit note.',
1531
1543
  params: {
1532
1544
  resourceId: { type: 'string', description: 'Supplier credit note resourceId' },
1533
1545
  reference: { type: 'string' },
1534
1546
  valueDate: { type: 'string' },
1535
- lineItems: { type: 'array', items: { type: 'object' } },
1547
+ lineItems: LINE_ITEM_PARAM,
1536
1548
  notes: { type: 'string' },
1537
1549
  tag: { type: 'string' },
1538
1550
  customFields: CUSTOM_FIELDS_PARAM,
@@ -1552,7 +1564,7 @@ export const TOOL_DEFINITIONS = [
1552
1564
  resourceId: { type: 'string', description: 'Supplier credit note resourceId' },
1553
1565
  reference: { type: 'string' },
1554
1566
  valueDate: { type: 'string' },
1555
- lineItems: { type: 'array', items: { type: 'object' } },
1567
+ lineItems: LINE_ITEM_PARAM,
1556
1568
  notes: { type: 'string' },
1557
1569
  },
1558
1570
  required: ['resourceId'],
@@ -1653,7 +1665,7 @@ export const TOOL_DEFINITIONS = [
1653
1665
  },
1654
1666
  {
1655
1667
  name: 'list_currency_rates',
1656
- description: 'List exchange rates for a specific currency.',
1668
+ description: 'List exchange rates for a specific currency. IMPORTANT: You MUST call list_currencies first to discover which currencies the org has enabled — never guess or assume currency codes.',
1657
1669
  params: {
1658
1670
  currencyCode: { type: 'string', description: 'Currency code (e.g., "USD")' },
1659
1671
  ...PAGINATION_PARAMS,
@@ -1669,7 +1681,7 @@ export const TOOL_DEFINITIONS = [
1669
1681
  },
1670
1682
  {
1671
1683
  name: 'add_currency_rate',
1672
- description: 'Add an exchange rate for a currency. Rate is relative to the base currency.',
1684
+ description: 'Add or set an exchange rate for a currency. ALWAYS use this tool even when the user says "update rate" — it handles both new and existing rates. Rate is relative to the base currency. Call list_currencies first to get valid currency codes.',
1673
1685
  params: {
1674
1686
  currencyCode: { type: 'string', description: 'Currency code' },
1675
1687
  rate: { type: 'number', description: 'Exchange rate (e.g., 1.35 for 1 USD = 1.35 SGD)' },
@@ -1687,7 +1699,7 @@ export const TOOL_DEFINITIONS = [
1687
1699
  },
1688
1700
  {
1689
1701
  name: 'update_currency_rate',
1690
- description: 'Update an existing exchange rate.',
1702
+ description: 'Update an existing exchange rate for a currency. Call list_currency_rates first to find the rate resourceId. Use add_currency_rate for new rates.',
1691
1703
  params: {
1692
1704
  currencyCode: { type: 'string', description: 'Currency code' },
1693
1705
  resourceId: { type: 'string', description: 'Rate resourceId' },
@@ -1761,19 +1773,7 @@ WHEN NOT TO USE: moving money between your own bank/cash accounts — use create
1761
1773
  reference: { type: 'string', description: 'Reference number' },
1762
1774
  valueDate: { type: 'string', description: 'Date (YYYY-MM-DD)' },
1763
1775
  accountResourceId: { type: 'string', description: 'Bank/cash account resourceId' },
1764
- journalEntries: {
1765
- type: 'array',
1766
- items: {
1767
- type: 'object',
1768
- properties: {
1769
- accountResourceId: { type: 'string' },
1770
- type: { type: 'string', enum: ['DEBIT', 'CREDIT'] },
1771
- amount: { type: 'number' },
1772
- description: { type: 'string' },
1773
- },
1774
- required: ['accountResourceId', 'type', 'amount'],
1775
- },
1776
- },
1776
+ journalEntries: JOURNAL_ENTRY_PARAM,
1777
1777
  notes: { type: 'string' },
1778
1778
  tags: { type: 'array', items: { type: 'string' }, description: 'Tags (string array)' },
1779
1779
  currency: CURRENCY_PARAM,
@@ -1802,19 +1802,7 @@ WHEN NOT TO USE: moving money between your own bank/cash accounts — use create
1802
1802
  reference: { type: 'string', description: 'Reference number' },
1803
1803
  valueDate: { type: 'string', description: 'Date (YYYY-MM-DD)' },
1804
1804
  accountResourceId: { type: 'string', description: 'Bank/cash account resourceId' },
1805
- journalEntries: {
1806
- type: 'array',
1807
- items: {
1808
- type: 'object',
1809
- properties: {
1810
- accountResourceId: { type: 'string' },
1811
- type: { type: 'string', enum: ['DEBIT', 'CREDIT'] },
1812
- amount: { type: 'number' },
1813
- description: { type: 'string' },
1814
- },
1815
- required: ['accountResourceId', 'type', 'amount'],
1816
- },
1817
- },
1805
+ journalEntries: JOURNAL_ENTRY_PARAM,
1818
1806
  notes: { type: 'string' },
1819
1807
  tags: { type: 'array', items: { type: 'string' }, description: 'Tags (string array)' },
1820
1808
  currency: CURRENCY_PARAM,
@@ -1843,12 +1831,12 @@ WHEN NOT TO USE: moving money between your own bank/cash accounts — use create
1843
1831
  },
1844
1832
  {
1845
1833
  name: 'update_cash_in',
1846
- description: 'Update an existing cash-in entry.',
1834
+ description: 'Update an existing cash-in entry (change amount, date, reference, notes, tags, or journal entries). Use when the user says "update", "change", "fix", or "adjust" a cash-in that was just created or found. Do NOT create a new entry — use this tool instead.',
1847
1835
  params: {
1848
1836
  resourceId: { type: 'string', description: 'Cash-in resourceId' },
1849
1837
  reference: { type: 'string' },
1850
1838
  valueDate: { type: 'string' },
1851
- journalEntries: { type: 'array', items: { type: 'object' } },
1839
+ journalEntries: JOURNAL_ENTRY_PARAM,
1852
1840
  notes: { type: 'string' },
1853
1841
  tags: { type: 'array', items: { type: 'string' }, description: 'Tags (string array)' },
1854
1842
  },
@@ -1873,12 +1861,12 @@ WHEN NOT TO USE: moving money between your own bank/cash accounts — use create
1873
1861
  },
1874
1862
  {
1875
1863
  name: 'update_cash_out',
1876
- description: 'Update an existing cash-out entry.',
1864
+ description: 'Update an existing cash-out entry (change amount, date, reference, notes, tags, or journal entries). Use when the user says "update", "change", "fix", or "adjust" a cash-out that was just created or found. Do NOT create a new entry — use this tool instead.',
1877
1865
  params: {
1878
1866
  resourceId: { type: 'string', description: 'Cash-out resourceId' },
1879
1867
  reference: { type: 'string' },
1880
1868
  valueDate: { type: 'string' },
1881
- journalEntries: { type: 'array', items: { type: 'object' } },
1869
+ journalEntries: JOURNAL_ENTRY_PARAM,
1882
1870
  notes: { type: 'string' },
1883
1871
  tags: { type: 'array', items: { type: 'string' }, description: 'Tags (string array)' },
1884
1872
  },
@@ -1951,7 +1939,7 @@ WHEN NOT TO USE: receiving money from external parties (use create_cash_in) or p
1951
1939
  },
1952
1940
  {
1953
1941
  name: 'search_bank_records',
1954
- description: 'Search bank records (imported bank transactions) for a specific account. Supports filtering by status, date range, description, payer/payee, reference, and amount range.',
1942
+ description: 'Search bank records (imported bank transactions) for a specific account. Use this to find unreconciled items, match deposits to invoices, or identify bank charges. Filter by status (UNRECONCILED/RECONCILED), date range, description, payer/payee, reference, and amount range. Call list_bank_accounts first to get the accountResourceId.',
1955
1943
  params: {
1956
1944
  accountResourceId: { type: 'string', description: 'Bank account resourceId' },
1957
1945
  status: { type: 'string', enum: ['UNRECONCILED', 'RECONCILED', 'ARCHIVED', 'POSSIBLE_DUPLICATE'], description: 'Filter by reconciliation status' },
@@ -2040,7 +2028,7 @@ WHEN NOT TO USE: receiving money from external parties (use create_cash_in) or p
2040
2028
  },
2041
2029
  {
2042
2030
  name: 'update_bookmark',
2043
- description: 'Update an existing bookmark.',
2031
+ description: 'Update an existing bookmark (name, value, or category). Use when modifying a previously created bookmark.',
2044
2032
  params: {
2045
2033
  resourceId: { type: 'string', description: 'Bookmark resourceId' },
2046
2034
  name: { type: 'string', description: 'New name' },
@@ -2512,20 +2500,7 @@ Auto-resolves accounts from chart of accounts. Provide bankAccountName for recip
2512
2500
  resourceId: { type: 'string', description: 'Journal resourceId' },
2513
2501
  reference: { type: 'string', description: 'Updated reference' },
2514
2502
  valueDate: { type: 'string', description: 'Updated date (YYYY-MM-DD)' },
2515
- journalEntries: {
2516
- type: 'array',
2517
- items: {
2518
- type: 'object',
2519
- properties: {
2520
- accountResourceId: { type: 'string' },
2521
- amount: { type: 'number' },
2522
- type: { type: 'string', enum: ['DEBIT', 'CREDIT'] },
2523
- description: { type: 'string' },
2524
- },
2525
- required: ['accountResourceId', 'amount', 'type'],
2526
- },
2527
- description: 'Updated journal entries (debit/credit lines)',
2528
- },
2503
+ journalEntries: JOURNAL_ENTRY_PARAM,
2529
2504
  notes: { type: 'string' },
2530
2505
  tags: { type: 'array', items: { type: 'string' }, description: 'Tags (string array)' },
2531
2506
  saveAsDraft: { type: 'boolean', description: 'Keep as draft (default true)' },
@@ -3059,7 +3034,7 @@ Auto-resolves accounts from chart of accounts. Provide bankAccountName for recip
3059
3034
  },
3060
3035
  {
3061
3036
  name: 'update_bank_rule',
3062
- description: 'Update an existing bank reconciliation rule.',
3037
+ description: 'Update an existing bank reconciliation rule (conditions, actions, or name). Fetches current rule first — only send fields to change.',
3063
3038
  params: {
3064
3039
  resourceId: { type: 'string', description: 'Bank rule resourceId' },
3065
3040
  name: { type: 'string', description: 'New rule name' },
@@ -3160,10 +3135,11 @@ Auto-resolves accounts from chart of accounts. Provide bankAccountName for recip
3160
3135
  },
3161
3136
  {
3162
3137
  name: 'create_fixed_asset',
3163
- description: `Register a new fixed asset. IMPORTANT:
3164
- - purchaseAssetAccountResourceId: the asset account to debit
3165
- - depreciationMethod, effectiveLife, depreciationStartDate: required for depreciation
3166
- - saveAsDraft defaults to true. Set to false to activate immediately.`,
3138
+ description: `Register a fixed asset linked to an existing purchase transaction (bill or journal).
3139
+ - saveAsDraft defaults to true. Set to false to activate — requires ALL fields below.
3140
+ - ACTIVE assets require: purchaseBusinessTransactionType + purchaseBusinessTransactionResourceId
3141
+ (links to the bill/journal that recorded the purchase).
3142
+ - For standalone asset entry (no linked transaction), use transfer_fixed_asset instead.`,
3167
3143
  params: {
3168
3144
  name: { type: 'string', description: 'Asset name' },
3169
3145
  typeName: { type: 'string', description: 'Asset type (e.g., "Buildings", "Vehicles", "Furniture")' },
@@ -3171,17 +3147,19 @@ Auto-resolves accounts from chart of accounts. Provide bankAccountName for recip
3171
3147
  purchaseAmount: { type: 'number', description: 'Purchase cost' },
3172
3148
  purchaseDate: { type: 'string', description: 'Purchase date (YYYY-MM-DD)' },
3173
3149
  purchaseAssetAccountResourceId: { type: 'string', description: 'Asset account resourceId' },
3174
- depreciationStartDate: { type: 'string', description: 'Depreciation start date (YYYY-MM-DD)' },
3175
- depreciationMethod: { type: 'string', description: 'Depreciation method (e.g., "STRAIGHT_LINE", "DIMINISHING_VALUE")' },
3150
+ depreciationStartDate: { type: 'string', description: 'Depreciation start date (YYYY-MM-DD, required for STRAIGHT_LINE)' },
3151
+ depreciationMethod: { type: 'string', enum: ['STRAIGHT_LINE', 'NO_DEPRECIATION'], description: 'Depreciation method' },
3176
3152
  effectiveLife: { type: 'number', description: 'Effective life in months' },
3177
3153
  depreciableValueResidualAmount: { type: 'number', description: 'Residual/salvage value' },
3178
- depreciationExpenseAccountResourceId: { type: 'string', description: 'Depreciation expense account resourceId' },
3179
- accumulatedDepreciationAccountResourceId: { type: 'string', description: 'Accumulated depreciation account resourceId' },
3154
+ depreciationExpenseAccountResourceId: { type: 'string', description: 'Depreciation expense account resourceId (required for STRAIGHT_LINE)' },
3155
+ accumulatedDepreciationAccountResourceId: { type: 'string', description: 'Accumulated depreciation account resourceId (required for STRAIGHT_LINE)' },
3156
+ purchaseBusinessTransactionType: { type: 'string', enum: ['PURCHASE', 'JOURNAL_MANUAL'], description: 'Type of purchase transaction this asset is linked to' },
3157
+ purchaseBusinessTransactionResourceId: { type: 'string', description: 'ResourceId of the purchase bill or journal' },
3180
3158
  internalNotes: { type: 'string', description: 'Internal notes' },
3181
- saveAsDraft: { type: 'boolean', description: 'Save as draft (default true)' },
3159
+ saveAsDraft: { type: 'boolean', description: 'Save as draft (default true). False = activate immediately.' },
3182
3160
  customFields: CUSTOM_FIELDS_PARAM,
3183
3161
  },
3184
- required: ['name', 'purchaseAmount', 'purchaseDate', 'purchaseAssetAccountResourceId'],
3162
+ required: ['name', 'purchaseAmount', 'purchaseDate', 'purchaseAssetAccountResourceId', 'depreciationStartDate'],
3185
3163
  group: 'fixed_assets',
3186
3164
  readOnly: false,
3187
3165
  execute: async (ctx, input) => createFixedAsset(ctx.client, input),
@@ -3241,7 +3219,7 @@ Auto-resolves accounts from chart of accounts. Provide bankAccountName for recip
3241
3219
  },
3242
3220
  {
3243
3221
  name: 'transfer_fixed_asset',
3244
- description: 'Transfer a fixed asset to a different asset type/account. Creates a new asset registration from an existing one.',
3222
+ description: 'Transfer a fixed asset to a different asset type/account, OR create a standalone asset entry (no linked purchase transaction needed). Creates a new asset registration from an existing one.',
3245
3223
  params: {
3246
3224
  resourceId: { type: 'string', description: 'Source fixed asset resourceId' },
3247
3225
  name: { type: 'string', description: 'New asset name' },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jaz-clio",
3
- "version": "4.30.2",
3
+ "version": "4.30.6",
4
4
  "description": "Clio — Command Line Interface Orchestrator for Jaz AI.",
5
5
  "type": "module",
6
6
  "bin": {