jaz-clio 4.30.0 → 4.30.2

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.0
3
+ version: 4.30.2
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.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: jaz-conversion
3
- version: 4.30.0
3
+ version: 4.30.2
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.0
3
+ version: 4.30.2
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.0
3
+ version: 4.30.2
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,6 +1,6 @@
1
1
  import chalk from 'chalk';
2
2
  import { listAccounts, searchAccounts, createAccount, deleteAccount } from '../core/api/chart-of-accounts.js';
3
- import { findExistingAccount } from '../core/api/guards.js';
3
+ import { findExistingAccount, normalizeAccountType } from '../core/api/guards.js';
4
4
  import { apiAction } from './api-action.js';
5
5
  import { outputList } from './output.js';
6
6
  import { parsePositiveInt, parseNonNegativeInt, readBodyInput, requireFields } from './parsers.js';
@@ -93,7 +93,7 @@ export function registerAccountsCommand(program) {
93
93
  const data = {
94
94
  code: opts.code,
95
95
  name: opts.name,
96
- accountType: opts.type,
96
+ accountType: normalizeAccountType(opts.type),
97
97
  };
98
98
  if (opts.currency)
99
99
  data.currencyCode = opts.currency;
@@ -4,6 +4,9 @@ export async function listAccounts(client, params) {
4
4
  export async function searchAccounts(client, params) {
5
5
  return client.search('/api/v1/chart-of-accounts/search', params);
6
6
  }
7
+ export async function getAccount(client, resourceId) {
8
+ return client.get(`/api/v1/chart-of-accounts/${resourceId}`);
9
+ }
7
10
  export async function createAccount(client, data) {
8
11
  return client.post('/api/v1/chart-of-accounts', data);
9
12
  }
@@ -1,7 +1,9 @@
1
1
  const DEFAULT_BASE_URL = 'https://api.getjaz.com';
2
2
  const DEFAULT_TIMEOUT = 30_000;
3
3
  const MAX_RETRIES = 3;
4
- const RETRY_STATUS_CODES = [429, 503];
4
+ /** Status codes safe to retry. 500/502/503 only retried on idempotent methods (GET/HEAD). */
5
+ const ALWAYS_RETRY = [429];
6
+ const IDEMPOTENT_RETRY = [500, 502, 503];
5
7
  export class JazApiError extends Error {
6
8
  status;
7
9
  body;
@@ -72,7 +74,10 @@ export class JazClient {
72
74
  const attemptStart = Date.now();
73
75
  try {
74
76
  const response = await fetch(url.toString(), fetchOptions);
75
- if (RETRY_STATUS_CODES.includes(response.status) && attempt < this.maxRetries) {
77
+ const isIdempotent = ['GET', 'HEAD'].includes(method);
78
+ const shouldRetry = ALWAYS_RETRY.includes(response.status)
79
+ || (isIdempotent && IDEMPOTENT_RETRY.includes(response.status));
80
+ if (shouldRetry && attempt < this.maxRetries) {
76
81
  this.onRequest?.({
77
82
  method, path, statusCode: response.status,
78
83
  durationMs: Date.now() - attemptStart, attempt, retried: true,
@@ -1,6 +1,7 @@
1
1
  import { searchContacts } from './contacts.js';
2
2
  import { searchItems } from './items.js';
3
3
  import { searchAccounts } from './chart-of-accounts.js';
4
+ import { searchCapsules } from './capsules.js';
4
5
  /** Find existing contact by name. Returns first match or undefined. */
5
6
  export async function findExistingContact(client, name) {
6
7
  if (!name)
@@ -15,6 +16,38 @@ export async function findExistingItem(client, itemCode) {
15
16
  const result = await searchItems(client, { filter: { itemCode: { eq: itemCode } }, limit: 1 });
16
17
  return result.totalElements > 0 ? result.data[0] : undefined;
17
18
  }
19
+ // ── Account type normalization ───────────────────────────────────
20
+ /** Normalize common accountType variations to exact API values.
21
+ * Pure map lookup — no network calls, no perf impact. */
22
+ const ACCOUNT_TYPE_MAP = {
23
+ 'current assets': 'Current Asset', 'current asset': 'Current Asset',
24
+ 'fixed assets': 'Fixed Asset', 'fixed asset': 'Fixed Asset',
25
+ 'bank account': 'Bank Accounts', 'bank accounts': 'Bank Accounts', 'bank': 'Bank Accounts',
26
+ 'current liabilities': 'Current Liability', 'current liability': 'Current Liability',
27
+ 'non-current liabilities': 'Non-current Liability', 'non-current liability': 'Non-current Liability',
28
+ 'equity': 'Shareholders Equity', 'shareholders equity': 'Shareholders Equity',
29
+ "shareholders' equity": 'Shareholders Equity', "shareholder's equity": 'Shareholders Equity',
30
+ 'revenue': 'Operating Revenue', 'operating revenue': 'Operating Revenue',
31
+ 'other revenue': 'Other Revenue', 'other income': 'Other Revenue',
32
+ 'expense': 'Operating Expense', 'operating expense': 'Operating Expense', 'expenses': 'Operating Expense',
33
+ 'direct costs': 'Direct Costs', 'cost of goods sold': 'Direct Costs', 'cogs': 'Direct Costs',
34
+ 'cash': 'Cash', 'inventory': 'Inventory',
35
+ };
36
+ export function normalizeAccountType(raw) {
37
+ if (typeof raw !== 'string')
38
+ return String(raw ?? '');
39
+ const trimmed = raw.trim();
40
+ return ACCOUNT_TYPE_MAP[trimmed.toLowerCase()] ?? trimmed;
41
+ }
42
+ // ── Duplicate detection ─────────────────────────────────────────
43
+ /** Find existing capsule by title. Normalizes whitespace/case for comparison. */
44
+ export async function findExistingCapsule(client, title) {
45
+ if (!title?.trim())
46
+ return undefined;
47
+ const normalized = title.trim().toLowerCase();
48
+ const result = await searchCapsules(client, { filter: { title: { contains: title.trim() } }, limit: 20 });
49
+ return result.data.find(c => c.title?.trim().toLowerCase() === normalized);
50
+ }
18
51
  /** Find existing account by name. Returns first match or undefined. */
19
52
  export async function findExistingAccount(client, name) {
20
53
  if (!name)
@@ -1,47 +1,145 @@
1
1
  export const TOOL_NAMESPACES = [
2
+ // ── Transactions ────────────────────────────────────────────
2
3
  {
3
- name: 'sales',
4
- description: 'Sales invoices (INV/SI), customer credit notes, scheduled invoices. Create, search, update, delete, pay, finalize, apply credits, download PDF. Also: receivables, AR, billing, overdue invoices.',
5
- groups: ['invoices', 'customer_credit_notes'],
4
+ name: 'invoices',
5
+ description: 'Sales invoices (INV/SI). Create, search, get, update, delete, pay, finalize, apply credits, download PDF. Also: receivables, AR, billing, overdue invoices.',
6
+ groups: ['invoices'],
6
7
  },
7
8
  {
8
- name: 'purchases',
9
- description: 'Purchase bills (PO/PI), supplier credit notes, scheduled bills. Create, search, update, delete, pay, finalize, apply credits. Also: payables, AP, vendor, supplier, overdue bills.',
10
- groups: ['bills', 'supplier_credit_notes'],
9
+ name: 'customer_credit_notes',
10
+ description: 'Customer credit notes (CN). Create, search, update, delete, finalize, refund, download PDF. Also: sales returns, customer CN.',
11
+ groups: ['customer_credit_notes'],
11
12
  },
12
13
  {
13
- name: 'journals_and_cash',
14
- description: 'Journal entries (JE), cash-in receipts, cash-out disbursements, cash transfers, petty cash, scheduled journals. Create, search, update, delete.',
15
- groups: ['journals', 'cash_entries', 'cash_transfers', 'schedulers'],
14
+ name: 'bills',
15
+ description: 'Purchase bills (PO/PI). Create, search, get, update, delete, pay, finalize, apply credits. Also: payables, AP, vendor invoices, supplier bills.',
16
+ groups: ['bills'],
16
17
  },
17
18
  {
18
- name: 'banking',
19
- description: 'Bank accounts, bank statement imports, bank records, bank reconciliation rules, auto-reconciliation, unreconciled transactions.',
20
- groups: ['bank', 'bank_rules'],
19
+ name: 'supplier_credit_notes',
20
+ description: 'Supplier credit notes. Create, search, update, delete, finalize, refund. Also: purchase returns, debit notes, supplier CN.',
21
+ groups: ['supplier_credit_notes'],
21
22
  },
22
23
  {
23
- name: 'reports',
24
- description: 'Financial reports: trial balance (TB), balance sheet (BS/B/S), profit & loss (PnL/P&L/PL/income statement), cash flow, aged receivables (AR), aged payables (AP), cash balance/position, general ledger (GL), VAT ledger, equity movement, bank balance, fixed asset reports. Data exports (CSV/Excel). Also: how profitable, how much owed, overdue.',
25
- groups: ['reports', 'exports'],
24
+ name: 'journals',
25
+ description: 'Journal entries (JE). Create, search, update, delete manual journals. Also: adjusting entries, accruals, reclassifications, corrections.',
26
+ groups: ['journals'],
26
27
  },
27
28
  {
28
- name: 'contacts_and_items',
29
- description: 'Contacts (customers/suppliers/vendors), items (products/services/inventory), tags, contact groups, custom fields.',
30
- groups: ['contacts', 'items', 'tags', 'contact_groups', 'custom_fields', 'inventory'],
29
+ name: 'cashflows',
30
+ description: 'Cash-in receipts, cash-out disbursements, cash transfers between own accounts, petty cash. WHEN TO USE: external cash received → cash-in. External cash paid → cash-out. Moving funds between own accounts → cash transfer.',
31
+ groups: ['cash_entries', 'cash_transfers'],
31
32
  },
33
+ // ── Banking ─────────────────────────────────────────────────
32
34
  {
33
- name: 'accounting_setup',
34
- description: 'Chart of accounts (COA), currencies, exchange rates (FX/forex), tax profiles, withholding tax (WHT) codes, bookmarks.',
35
- groups: ['accounts', 'currencies', 'tax_profiles', 'bookmarks'],
35
+ name: 'bank_accounts',
36
+ description: 'Bank accounts, bank statement imports (CSV/OFX), bank records search, auto-reconciliation. For unreconciled queries: ALWAYS search bank records with status UNRECONCILED after listing accounts. Also: bank feeds, bank balance.',
37
+ groups: ['bank'],
36
38
  },
39
+ {
40
+ name: 'bank_rules',
41
+ description: 'Bank reconciliation rules (action shortcuts). Create, search, update, delete bank rules. Configure auto-matching rules for bank records.',
42
+ groups: ['bank_rules'],
43
+ },
44
+ // ── Reports ─────────────────────────────────────────────────
45
+ {
46
+ name: 'financial_reports',
47
+ description: 'Core financial statements: trial balance (TB), balance sheet (BS/B/S), profit & loss (PnL/P&L/income statement), cash flow, general ledger (GL), cash balance/position, equity movement, VAT/GST ledger. Also: how profitable, what is the balance.',
48
+ groups: ['financial_reports'],
49
+ },
50
+ {
51
+ name: 'operational_reports',
52
+ description: 'Aging and operational reports: aged receivables (AR aging), aged payables (AP aging), AR report, bank balance summary, bank reconciliation reports, fixed asset (FA) summary, FA reconciliation. Data exports (CSV/Excel). Also: overdue analysis, how much owed.',
53
+ groups: ['operational_reports', 'exports'],
54
+ },
55
+ // ── Master Data ─────────────────────────────────────────────
56
+ {
57
+ name: 'contacts',
58
+ description: 'Contacts (customers/suppliers/vendors), contact groups. Create, search, get, update, delete, bulk create contacts. List/create contact groups.',
59
+ groups: ['contacts', 'contact_groups'],
60
+ },
61
+ {
62
+ name: 'items_and_inventory',
63
+ description: 'Products, services, inventory items. Create, search, get, update, delete items. Check inventory balance. Also: SKU, catalog, stock.',
64
+ groups: ['items', 'inventory'],
65
+ },
66
+ {
67
+ name: 'tags_and_custom_fields',
68
+ description: 'Tags for categorizing transactions. Custom fields for adding metadata (text, date, dropdown). Create, search, delete tags and custom fields.',
69
+ groups: ['tags', 'custom_fields'],
70
+ },
71
+ // ── Accounting Setup ────────────────────────────────────────
72
+ {
73
+ name: 'chart_of_accounts',
74
+ description: 'Chart of accounts (COA/GL accounts). Create, search, update accounts. Bookmarks (favorites/shortcuts). Also: ledger codes, account types.',
75
+ groups: ['accounts', 'bookmarks'],
76
+ },
77
+ {
78
+ name: 'currencies',
79
+ description: 'Currencies, exchange rates (FX/forex). List/add org currencies. Set, update, import currency rates. Also: multi-currency, FX rates.',
80
+ groups: ['currencies'],
81
+ },
82
+ {
83
+ name: 'tax_profiles',
84
+ description: 'Tax profiles (GST/VAT/sales tax), withholding tax codes (WHT/ATC). Search, create, update tax profiles. List WHT codes.',
85
+ groups: ['tax_profiles'],
86
+ },
87
+ // ── Capsules & Recipes ──────────────────────────────────────
37
88
  {
38
89
  name: 'capsules_and_recipes',
39
- description: 'Capsules (transaction groupings), recurring/scheduled transactions, subscriptions, and financial recipes: amortization, depreciation, deferred revenue, loan schedules, IFRS 16 leases, hire purchase, fixed deposits, FX revaluation, ECL provisioning, IAS 37 provisions, dividends, leave accrual, asset disposal.',
40
- groups: ['capsules', 'recipes', 'subscriptions'],
90
+ description: 'Capsules (transaction groupings/capsule types). Financial recipes: amortization, depreciation, deferred revenue, IFRS 16 leases, hire purchase, fixed deposits, FX revaluation, loan schedules, ECL/expected credit loss, IAS 37 provisions, asset disposal. Plan and execute recipes. Keywords: calculate, provision, schedule, expected credit loss, revaluation.',
91
+ groups: ['capsules', 'recipes'],
92
+ },
93
+ // ── Scheduling ──────────────────────────────────────────────
94
+ {
95
+ name: 'scheduled_transactions',
96
+ description: 'Scheduled/recurring invoices, bills, journals. Create scheduled invoices/bills/journals, search scheduled transactions. Also: recurring, auto-generate.',
97
+ groups: ['schedulers'],
98
+ },
99
+ {
100
+ name: 'subscriptions',
101
+ description: 'Subscriptions (recurring billing/payment plans). Create, update, cancel, search subscriptions. Also: recurring charges, subscription schedules.',
102
+ groups: ['subscriptions'],
103
+ },
104
+ // ── Organization ────────────────────────────────────────────
105
+ {
106
+ name: 'organization',
107
+ description: 'Organization info (name, currency, country, fiscal year). User management: invite, update, remove, search org users. Bulk invite.',
108
+ groups: ['organization', 'org_users'],
109
+ },
110
+ // ── Documents & AI ──────────────────────────────────────────
111
+ {
112
+ name: 'document_ai',
113
+ description: 'File attachments, AI document extraction (magic/OCR). Upload/list attachments. Create transactions from PDFs/images (invoice scanning, bill extraction). Track extraction workflows.',
114
+ groups: ['attachments', 'magic'],
115
+ },
116
+ // ── Fixed Assets ────────────────────────────────────────────
117
+ {
118
+ name: 'fixed_assets',
119
+ description: 'Fixed assets (PP&E/property, plant, equipment). Search, create, update, discard, sell, transfer, undo disposal. Also: depreciation, asset register.',
120
+ groups: ['fixed_assets'],
121
+ },
122
+ // ── Payments & Search ───────────────────────────────────────
123
+ {
124
+ name: 'payments_and_search',
125
+ description: 'Payments search/list across all transaction types. Cashflow transaction ledger. Universal cross-entity search. Transaction summary (fetch any transaction + attachments + payment history in one call).',
126
+ groups: ['payments', 'cashflow', 'search'],
127
+ },
128
+ // ── Drafts ──────────────────────────────────────────────────
129
+ {
130
+ name: 'drafts',
131
+ description: 'Draft validation for invoices, bills, journals, credit notes. Bulk finalize multiple drafts at once. Check if drafts are ready to finalize.',
132
+ groups: ['drafts'],
133
+ },
134
+ // ── Job Blueprints ──────────────────────────────────────────
135
+ {
136
+ name: 'close_procedures',
137
+ description: 'Period-end close checklists: month-end, quarter-end, year-end close. Bank reconciliation job. GST/VAT filing job. Audit preparation. Returns structured blueprints.',
138
+ groups: ['close_jobs'],
41
139
  },
42
140
  {
43
- name: 'operations',
44
- description: 'Organization info, user management (invite/remove users), file attachments/uploads, payments search, cashflow ledger, fixed assets (list/create/dispose/sell), AI extraction from documents (magic/OCR), universal search, accounting jobs and drafts.',
45
- groups: ['organization', 'org_users', 'attachments', 'payments', 'cashflow', 'magic', 'fixed_assets', 'search', 'jobs', 'drafts'],
141
+ name: 'operational_jobs',
142
+ description: 'Operational job checklists: payment runs, credit control/collections, supplier reconciliation, fixed asset review, document collection, statutory filing. Returns structured blueprints.',
143
+ groups: ['operational_jobs'],
46
144
  },
47
145
  ];
@@ -1,6 +1,6 @@
1
- import { findExistingContact, findExistingItem, findExistingAccount } from '../api/guards.js';
1
+ import { findExistingContact, findExistingItem, findExistingAccount, findExistingCapsule, normalizeAccountType } from '../api/guards.js';
2
2
  import { getOrganization } from '../api/organization.js';
3
- import { listAccounts, searchAccounts, createAccount, updateAccount, deleteAccount, } from '../api/chart-of-accounts.js';
3
+ import { listAccounts, searchAccounts, getAccount, createAccount, updateAccount, deleteAccount, } from '../api/chart-of-accounts.js';
4
4
  import { listContacts, searchContacts, getContact, createContact, updateContact, deleteContact, } from '../api/contacts.js';
5
5
  import { listInvoices, searchInvoices, getInvoice, createInvoice, updateInvoice, deleteInvoice, createInvoicePayment, createScheduledInvoice, finalizeInvoice, applyCreditsToInvoice, downloadInvoicePdf, } from '../api/invoices.js';
6
6
  import { listBills, searchBills, getBill, createBill, updateBill, deleteBill, createBillPayment, createScheduledBill, finalizeBill, applyCreditsToBill, } from '../api/bills.js';
@@ -61,8 +61,7 @@ const PAGINATION_PARAMS = {
61
61
  };
62
62
  const SEARCH_PARAMS = {
63
63
  ...PAGINATION_PARAMS,
64
- sortBy: { type: 'string', description: 'Sort field (always sent with queries). Common: valueDate, reference, status, name. Default: valueDate' },
65
- order: { type: 'string', enum: ['ASC', 'DESC'], description: 'Sort direction' },
64
+ // sortBy/order removed hardcoded per-tool to prevent LLM sending invalid sort fields
66
65
  };
67
66
  const CURRENCY_PARAM = {
68
67
  type: 'object',
@@ -124,10 +123,8 @@ const LINE_ITEM_PARAM = {
124
123
  function extractPaginationInput(input) {
125
124
  const limit = input.limit;
126
125
  const offset = input.offset;
127
- const sortBy = input.sortBy;
128
- const rawOrder = input.order;
129
- const sortOrder = rawOrder === 'ASC' || rawOrder === 'DESC' ? rawOrder : undefined;
130
- return { limit, offset, sortBy, sortOrder };
126
+ // sortBy/order not exposed to LLM — each search tool uses its own hardcoded default
127
+ return { limit, offset, sortBy: undefined, sortOrder: undefined };
131
128
  }
132
129
  // ── List tool factory (DRY — all 10 list tools share this) ───────
133
130
  function listTool(name, description, group, fetcher) {
@@ -171,14 +168,137 @@ function deleteTool(name, description, group, deleter) {
171
168
  },
172
169
  };
173
170
  }
174
- /** Fetch lineItems from an existing transaction (API requires them on finalize PUT). */
175
- async function fetchLineItems(client, type, resourceId) {
171
+ /**
172
+ * Writable fields per entity type (allowlist — only these go to PUT).
173
+ * Derived from the finalize/update function signatures in src/core/api/.
174
+ */
175
+ const WRITABLE_FIELDS = {
176
+ invoice: new Set([
177
+ 'reference', 'valueDate', 'dueDate', 'contactResourceId',
178
+ 'lineItems', 'notes', 'invoiceNotes', 'internalNotes', 'tag', 'tags',
179
+ 'isTaxVatApplicable', 'isTaxVATApplicable', 'taxInclusion',
180
+ 'terms', 'currency', 'customFields', 'capsuleResourceId',
181
+ 'taxProfileResourceId', 'customerPaymentProfileResourceId',
182
+ ]),
183
+ bill: new Set([
184
+ 'reference', 'valueDate', 'dueDate', 'contactResourceId',
185
+ 'lineItems', 'notes', 'internalNotes', 'tag', 'tags',
186
+ 'isTaxVatApplicable', 'isTaxVATApplicable', 'taxInclusion',
187
+ 'terms', 'currency', 'customFields', 'capsuleResourceId',
188
+ 'taxProfileResourceId',
189
+ ]),
190
+ customer_credit_note: new Set([
191
+ 'reference', 'valueDate', 'contactResourceId',
192
+ 'lineItems', 'notes', 'tag', 'tags',
193
+ 'isTaxVatApplicable', 'isTaxVATApplicable', 'taxInclusion',
194
+ 'currency', 'customFields', 'capsuleResourceId',
195
+ 'taxProfileResourceId',
196
+ ]),
197
+ supplier_credit_note: new Set([
198
+ 'reference', 'valueDate', 'contactResourceId',
199
+ 'lineItems', 'notes', 'tag', 'tags',
200
+ 'isTaxVatApplicable', 'isTaxVATApplicable', 'taxInclusion',
201
+ 'currency', 'customFields', 'capsuleResourceId',
202
+ 'taxProfileResourceId',
203
+ ]),
204
+ };
205
+ /** Normalize a date string to YYYY-MM-DD (strip time component). */
206
+ function toDateOnly(v) {
207
+ if (typeof v !== 'string')
208
+ return v;
209
+ if (/^\d{4}-\d{2}-\d{2}$/.test(v))
210
+ return v;
211
+ const m = /^(\d{4}-\d{2}-\d{2})T/.exec(v);
212
+ return m ? m[1] : v;
213
+ }
214
+ /** Writable line item fields for PUT (allowlist). */
215
+ const WRITABLE_LI_FIELDS = new Set([
216
+ 'name', 'quantity', 'unitPrice', 'unit', 'accountResourceId',
217
+ 'taxProfileResourceId', 'description', 'classifierConfig',
218
+ 'itemResourceId', 'discount',
219
+ ]);
220
+ /** Normalize GET line items to PUT format (field name asymmetries + allowlist). */
221
+ function normalizeLineItems(items) {
222
+ if (!Array.isArray(items))
223
+ return items;
224
+ return items.map((li) => {
225
+ const out = {};
226
+ for (const [k, v] of Object.entries(li)) {
227
+ if (v === null || v === undefined)
228
+ continue;
229
+ // GET returns organizationAccountResourceId, PUT wants accountResourceId
230
+ if (k === 'organizationAccountResourceId') {
231
+ out.accountResourceId = v;
232
+ }
233
+ else if (k === 'taxProfile' && typeof v === 'object' && v !== null) {
234
+ const rid = v.resourceId;
235
+ if (rid)
236
+ out.taxProfileResourceId = rid;
237
+ }
238
+ else if (k === 'discount' && typeof v === 'object' && v !== null) {
239
+ // Only include discount if it has a non-zero value (zero defaults cause 400)
240
+ const rv = v.rateValue;
241
+ if (rv && Number(rv) !== 0)
242
+ out.discount = v;
243
+ }
244
+ else if (WRITABLE_LI_FIELDS.has(k)) {
245
+ out[k] = v;
246
+ }
247
+ }
248
+ return out;
249
+ });
250
+ }
251
+ /**
252
+ * Fetch full entity, pick only writable fields, normalize GET→PUT asymmetries,
253
+ * then merge LLM overrides on top. Bulletproof for finalize/update PUTs.
254
+ */
255
+ async function fetchAndMerge(client, type, resourceId, overrides) {
176
256
  const fetcher = type === 'invoice' ? getInvoice
177
257
  : type === 'bill' ? getBill
178
258
  : type === 'customer_credit_note' ? getCustomerCreditNote
179
259
  : getSupplierCreditNote;
180
260
  const result = await fetcher(client, resourceId);
181
- return result.data.lineItems ?? [];
261
+ const existing = result.data;
262
+ const allowed = WRITABLE_FIELDS[type];
263
+ // Pick only writable fields from existing entity
264
+ const base = {};
265
+ for (const [k, v] of Object.entries(existing)) {
266
+ if (allowed.has(k) && v !== null && v !== undefined)
267
+ base[k] = v;
268
+ }
269
+ // Normalize dates to YYYY-MM-DD (GET returns ISO datetime)
270
+ if (base.valueDate)
271
+ base.valueDate = toDateOnly(base.valueDate);
272
+ if (base.dueDate)
273
+ base.dueDate = toDateOnly(base.dueDate);
274
+ // Normalize line items (field name asymmetries)
275
+ if (base.lineItems)
276
+ base.lineItems = normalizeLineItems(base.lineItems);
277
+ // Overrides win (LLM may supply updated lineItems, notes, etc.)
278
+ for (const [k, v] of Object.entries(overrides)) {
279
+ if (v !== undefined)
280
+ base[k] = v;
281
+ }
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
+ }
300
+ }
301
+ return base;
182
302
  }
183
303
  // ── Tool Definitions ─────────────────────────────────────────────
184
304
  export const TOOL_DEFINITIONS = [
@@ -233,22 +353,7 @@ export const TOOL_DEFINITIONS = [
233
353
  if (existingAcct) {
234
354
  return { _guard: 'duplicate_skipped', message: `Account "${acctName}" already exists.`, existing: existingAcct };
235
355
  }
236
- // Normalize common LLM variations to exact API values
237
- const typeMap = {
238
- 'current assets': 'Current Asset', 'current asset': 'Current Asset',
239
- 'fixed assets': 'Fixed Asset', 'fixed asset': 'Fixed Asset',
240
- 'bank account': 'Bank Accounts', 'bank accounts': 'Bank Accounts', 'bank': 'Bank Accounts',
241
- 'current liabilities': 'Current Liability', 'current liability': 'Current Liability',
242
- 'non-current liabilities': 'Non-current Liability', 'non-current liability': 'Non-current Liability',
243
- 'equity': 'Shareholders Equity', 'shareholders equity': 'Shareholders Equity', "shareholders' equity": 'Shareholders Equity', "shareholder's equity": 'Shareholders Equity',
244
- 'revenue': 'Operating Revenue', 'operating revenue': 'Operating Revenue',
245
- 'other revenue': 'Other Revenue', 'other income': 'Other Revenue',
246
- 'expense': 'Operating Expense', 'operating expense': 'Operating Expense', 'expenses': 'Operating Expense',
247
- 'direct costs': 'Direct Costs', 'cost of goods sold': 'Direct Costs', 'cogs': 'Direct Costs',
248
- 'cash': 'Cash', 'inventory': 'Inventory',
249
- };
250
- const rawType = input.accountType;
251
- const accountType = typeMap[rawType.toLowerCase()] ?? rawType;
356
+ const accountType = normalizeAccountType(input.accountType);
252
357
  return createAccount(ctx.client, {
253
358
  code: input.code,
254
359
  name: acctName,
@@ -268,10 +373,22 @@ export const TOOL_DEFINITIONS = [
268
373
  required: ['resourceId'],
269
374
  group: 'accounts',
270
375
  readOnly: false,
271
- execute: async (ctx, input) => updateAccount(ctx.client, input.resourceId, {
272
- name: input.name,
273
- code: input.code,
274
- }),
376
+ execute: async (ctx, input) => {
377
+ // Fetch existing account — only keep writable fields (not server-managed ones)
378
+ const rid = input.resourceId;
379
+ const existing = (await getAccount(ctx.client, rid)).data;
380
+ const ACCOUNT_WRITABLE = ['name', 'code', 'classificationType', 'taxProfileResourceId', 'currency', 'description'];
381
+ const merged = Object.fromEntries(ACCOUNT_WRITABLE.filter(k => existing[k] !== undefined && existing[k] !== null).map(k => [k, existing[k]]));
382
+ // GET→PUT asymmetry: GET returns accountType, PUT requires classificationType (Rule 21)
383
+ if (!merged.classificationType && existing.accountType) {
384
+ merged.classificationType = existing.accountType;
385
+ }
386
+ if (input.name !== undefined)
387
+ merged.name = input.name;
388
+ if (input.code !== undefined)
389
+ merged.code = input.code;
390
+ return updateAccount(ctx.client, rid, merged);
391
+ },
275
392
  },
276
393
  // ── Contacts ───────────────────────────────────────────────────
277
394
  listTool('list_contacts', 'List contacts (customers/suppliers). Returns billingName, name, emails, status. Paginated — response includes totalElements. Use limit/offset to page.', 'contacts', (client, off, lim) => listContacts(client, { limit: lim, offset: off })),
@@ -404,6 +521,7 @@ export const TOOL_DEFINITIONS = [
404
521
  description: `Create a new invoice. IMPORTANT:
405
522
  - Line items use "name" (not "description") for the item label.
406
523
  - saveAsDraft defaults to true. Set to false only if user says "finalize".
524
+ - accountResourceId is REQUIRED on each lineItem for finalized invoices. Search accounts first (e.g. Operating Revenue).
407
525
  - Currency format: { sourceCurrency: "USD", exchangeRate: 1.35 }
408
526
  - contactResourceId is required — search contacts first to get the ID.
409
527
  - Dates must be YYYY-MM-DD format.`,
@@ -496,7 +614,7 @@ export const TOOL_DEFINITIONS = [
496
614
  },
497
615
  {
498
616
  name: 'finalize_invoice',
499
- description: 'Finalize a draft invoice (set saveAsDraft=false). Can optionally update fields in the same call.',
617
+ description: 'Finalize a draft invoice (set saveAsDraft=false). Can optionally update fields in the same call. IMPORTANT: Every lineItem MUST have accountResourceId. If the draft was created without it, pass lineItems with accountResourceId added (search accounts first, e.g. Operating Revenue for sales).',
500
618
  params: {
501
619
  resourceId: { type: 'string', description: 'Invoice resourceId' },
502
620
  reference: { type: 'string' },
@@ -509,10 +627,9 @@ export const TOOL_DEFINITIONS = [
509
627
  group: 'invoices',
510
628
  readOnly: false,
511
629
  execute: async (ctx, input) => {
512
- const { resourceId: rid, ...data } = input;
513
- if (!data.lineItems)
514
- data.lineItems = await fetchLineItems(ctx.client, 'invoice', rid);
515
- return finalizeInvoice(ctx.client, rid, data);
630
+ const { resourceId: rid, ...overrides } = input;
631
+ const merged = await fetchAndMerge(ctx.client, 'invoice', rid, overrides);
632
+ return finalizeInvoice(ctx.client, rid, merged);
516
633
  },
517
634
  },
518
635
  {
@@ -594,7 +711,7 @@ export const TOOL_DEFINITIONS = [
594
711
  },
595
712
  {
596
713
  name: 'create_bill',
597
- description: 'Create a new bill. Same rules as create_invoice (name not description, saveAsDraft default).',
714
+ description: 'Create a new bill. Same rules as create_invoice: name not description, saveAsDraft defaults true, accountResourceId REQUIRED on lineItems for finalized bills (search accounts first, e.g. Operating Expense).',
598
715
  params: {
599
716
  reference: { type: 'string' },
600
717
  valueDate: { type: 'string' },
@@ -680,7 +797,7 @@ export const TOOL_DEFINITIONS = [
680
797
  },
681
798
  {
682
799
  name: 'finalize_bill',
683
- description: 'Finalize a draft bill (set saveAsDraft=false). Can optionally update fields in the same call.',
800
+ description: 'Finalize a draft bill (set saveAsDraft=false). Can optionally update fields in the same call. IMPORTANT: Every lineItem MUST have accountResourceId. If the draft was created without it, pass lineItems with accountResourceId added (search accounts first, e.g. Operating Expense for purchases).',
684
801
  params: {
685
802
  resourceId: { type: 'string', description: 'Bill resourceId' },
686
803
  reference: { type: 'string' },
@@ -693,10 +810,9 @@ export const TOOL_DEFINITIONS = [
693
810
  group: 'bills',
694
811
  readOnly: false,
695
812
  execute: async (ctx, input) => {
696
- const { resourceId: rid, ...data } = input;
697
- if (!data.lineItems)
698
- data.lineItems = await fetchLineItems(ctx.client, 'bill', rid);
699
- return finalizeBill(ctx.client, rid, data);
813
+ const { resourceId: rid, ...overrides } = input;
814
+ const merged = await fetchAndMerge(ctx.client, 'bill', rid, overrides);
815
+ return finalizeBill(ctx.client, rid, merged);
700
816
  },
701
817
  },
702
818
  {
@@ -792,7 +908,7 @@ export const TOOL_DEFINITIONS = [
792
908
  readOnly: false,
793
909
  execute: async (ctx, input) => deleteJournal(ctx.client, input.resourceId),
794
910
  },
795
- // ── Reports ────────────────────────────────────────────────────
911
+ // ── Financial Reports (core statements) ──────────────────────
796
912
  {
797
913
  name: 'generate_trial_balance',
798
914
  description: 'Generate a trial balance report.',
@@ -801,7 +917,7 @@ export const TOOL_DEFINITIONS = [
801
917
  currencyCode: { type: 'string', description: 'Currency override' },
802
918
  },
803
919
  required: ['endDate'],
804
- group: 'reports',
920
+ group: 'financial_reports',
805
921
  readOnly: true,
806
922
  execute: async (ctx, input) => generateTrialBalance(ctx.client, input),
807
923
  },
@@ -813,7 +929,7 @@ export const TOOL_DEFINITIONS = [
813
929
  currencyCode: { type: 'string' },
814
930
  },
815
931
  required: [],
816
- group: 'reports',
932
+ group: 'financial_reports',
817
933
  readOnly: true,
818
934
  execute: async (ctx, input) => {
819
935
  const date = input.snapshotDate ?? new Date().toISOString().slice(0, 10);
@@ -829,7 +945,7 @@ export const TOOL_DEFINITIONS = [
829
945
  currencyCode: { type: 'string' },
830
946
  },
831
947
  required: ['startDate', 'endDate'],
832
- group: 'reports',
948
+ group: 'financial_reports',
833
949
  readOnly: true,
834
950
  execute: async (ctx, input) => generateProfitAndLoss(ctx.client, input),
835
951
  },
@@ -841,7 +957,7 @@ export const TOOL_DEFINITIONS = [
841
957
  endDate: { type: 'string' },
842
958
  },
843
959
  required: ['startDate', 'endDate'],
844
- group: 'reports',
960
+ group: 'financial_reports',
845
961
  readOnly: true,
846
962
  execute: async (ctx, input) => generateCashflow(ctx.client, input),
847
963
  },
@@ -852,7 +968,7 @@ export const TOOL_DEFINITIONS = [
852
968
  endDate: { type: 'string' },
853
969
  },
854
970
  required: ['endDate'],
855
- group: 'reports',
971
+ group: 'operational_reports',
856
972
  readOnly: true,
857
973
  execute: async (ctx, input) => generateArSummary(ctx.client, input),
858
974
  },
@@ -863,7 +979,7 @@ export const TOOL_DEFINITIONS = [
863
979
  endDate: { type: 'string' },
864
980
  },
865
981
  required: ['endDate'],
866
- group: 'reports',
982
+ group: 'operational_reports',
867
983
  readOnly: true,
868
984
  execute: async (ctx, input) => generateApSummary(ctx.client, input),
869
985
  },
@@ -874,7 +990,7 @@ export const TOOL_DEFINITIONS = [
874
990
  endDate: { type: 'string', description: 'Snapshot date (YYYY-MM-DD)' },
875
991
  },
876
992
  required: ['endDate'],
877
- group: 'reports',
993
+ group: 'financial_reports',
878
994
  readOnly: true,
879
995
  execute: async (ctx, input) => generateCashBalance(ctx.client, input),
880
996
  },
@@ -887,7 +1003,7 @@ export const TOOL_DEFINITIONS = [
887
1003
  groupBy: { type: 'string', description: 'Group by: ACCOUNT (default), TRANSACTION, or CAPSULE' },
888
1004
  },
889
1005
  required: ['startDate', 'endDate'],
890
- group: 'reports',
1006
+ group: 'financial_reports',
891
1007
  readOnly: true,
892
1008
  execute: async (ctx, input) => {
893
1009
  const i = input;
@@ -1108,11 +1224,19 @@ export const TOOL_DEFINITIONS = [
1108
1224
  required: ['capsuleTypeResourceId', 'title'],
1109
1225
  group: 'capsules',
1110
1226
  readOnly: false,
1111
- execute: async (ctx, input) => createCapsule(ctx.client, {
1112
- capsuleTypeResourceId: input.capsuleTypeResourceId,
1113
- title: input.title,
1114
- description: input.description,
1115
- }),
1227
+ execute: async (ctx, input) => {
1228
+ const capsuleTitle = input.title;
1229
+ // Guard: check for existing capsule with same title
1230
+ const existingCapsule = await findExistingCapsule(ctx.client, capsuleTitle);
1231
+ if (existingCapsule) {
1232
+ return { _guard: 'duplicate_skipped', message: `Capsule "${capsuleTitle}" already exists.`, existing: existingCapsule };
1233
+ }
1234
+ return createCapsule(ctx.client, {
1235
+ capsuleTypeResourceId: input.capsuleTypeResourceId,
1236
+ title: capsuleTitle,
1237
+ description: input.description,
1238
+ });
1239
+ },
1116
1240
  },
1117
1241
  {
1118
1242
  name: 'update_capsule',
@@ -1260,8 +1384,9 @@ export const TOOL_DEFINITIONS = [
1260
1384
  group: 'customer_credit_notes',
1261
1385
  readOnly: false,
1262
1386
  execute: async (ctx, input) => {
1263
- const { resourceId: rid, ...data } = input;
1264
- return finalizeCustomerCreditNote(ctx.client, rid, data);
1387
+ const { resourceId: rid, ...overrides } = input;
1388
+ const merged = await fetchAndMerge(ctx.client, 'customer_credit_note', rid, overrides);
1389
+ return finalizeCustomerCreditNote(ctx.client, rid, merged);
1265
1390
  },
1266
1391
  },
1267
1392
  {
@@ -1434,8 +1559,9 @@ export const TOOL_DEFINITIONS = [
1434
1559
  group: 'supplier_credit_notes',
1435
1560
  readOnly: false,
1436
1561
  execute: async (ctx, input) => {
1437
- const { resourceId: rid, ...data } = input;
1438
- return finalizeSupplierCreditNote(ctx.client, rid, data);
1562
+ const { resourceId: rid, ...overrides } = input;
1563
+ const merged = await fetchAndMerge(ctx.client, 'supplier_credit_note', rid, overrides);
1564
+ return finalizeSupplierCreditNote(ctx.client, rid, merged);
1439
1565
  },
1440
1566
  },
1441
1567
  {
@@ -1508,7 +1634,22 @@ export const TOOL_DEFINITIONS = [
1508
1634
  required: ['currencies'],
1509
1635
  group: 'currencies',
1510
1636
  readOnly: false,
1511
- execute: async (ctx, input) => addCurrency(ctx.client, input.currencies),
1637
+ execute: async (ctx, input) => {
1638
+ const requested = input.currencies;
1639
+ // Guard: fetch all enabled currencies once, filter locally (not N API calls)
1640
+ const allCurrencies = await listCurrencies(ctx.client);
1641
+ const enabledSet = new Set(allCurrencies.data.map(c => c.currencyCode.toUpperCase()));
1642
+ const alreadyEnabled = requested.filter(code => enabledSet.has(code.toUpperCase()));
1643
+ const toAdd = requested.filter(code => !enabledSet.has(code.toUpperCase()));
1644
+ if (toAdd.length === 0) {
1645
+ return { _guard: 'duplicate_skipped', message: `All currencies already enabled: ${requested.join(', ')}.`, existing: alreadyEnabled };
1646
+ }
1647
+ const result = await addCurrency(ctx.client, toAdd);
1648
+ if (alreadyEnabled.length > 0) {
1649
+ return { ...result, _note: `Skipped already-enabled: ${alreadyEnabled.join(', ')}` };
1650
+ }
1651
+ return result;
1652
+ },
1512
1653
  },
1513
1654
  {
1514
1655
  name: 'list_currency_rates',
@@ -1610,10 +1751,12 @@ export const TOOL_DEFINITIONS = [
1610
1751
  listTool('list_cash_in', 'List cash-in entries (direct cash receipts). Paginated.', 'cash_entries', (client, off, lim) => listCashIn(client, { limit: lim, offset: off })),
1611
1752
  {
1612
1753
  name: 'create_cash_in',
1613
- description: `Record money received INTO a bank account (customer payment, refund received, deposit). NOT for transfers between your own accounts — use create_cash_transfer instead. IMPORTANT:
1754
+ description: `Record money received INTO a bank account from an EXTERNAL source (customer payment, refund received, deposit from outside).
1755
+ WHEN TO USE: customer payments received, refunds from suppliers, insurance payouts, external deposits.
1756
+ WHEN NOT TO USE: moving money between your own bank/cash accounts — use create_cash_transfer instead.
1614
1757
  - accountResourceId is the bank/cash account receiving the money.
1615
1758
  - journalEntries are the offsetting entries (e.g., revenue account). Each needs accountResourceId, type (DEBIT/CREDIT), and amount.
1616
- - The API enforces account separation: cash-in accounts cannot be used for cash-out and vice versa.`,
1759
+ - The API enforces account separation: cash-in accounts cannot be used for cash-out.`,
1617
1760
  params: {
1618
1761
  reference: { type: 'string', description: 'Reference number' },
1619
1762
  valueDate: { type: 'string', description: 'Date (YYYY-MM-DD)' },
@@ -1649,10 +1792,12 @@ export const TOOL_DEFINITIONS = [
1649
1792
  listTool('list_cash_out', 'List cash-out entries (direct cash disbursements). Paginated.', 'cash_entries', (client, off, lim) => listCashOut(client, { limit: lim, offset: off })),
1650
1793
  {
1651
1794
  name: 'create_cash_out',
1652
- description: `Record money paid OUT FROM a bank account (expense, supplier payment, reimbursement, withdrawal). NOT for transfers between your own accounts — use create_cash_transfer instead. IMPORTANT:
1795
+ description: `Record money paid OUT FROM a bank account to an EXTERNAL party (expense, supplier payment, reimbursement, withdrawal).
1796
+ WHEN TO USE: expenses paid, supplier payments, reimbursements, withdrawals to external parties.
1797
+ WHEN NOT TO USE: moving money between your own bank/cash accounts — use create_cash_transfer instead.
1653
1798
  - accountResourceId is the bank/cash account disbursing the money.
1654
1799
  - journalEntries are the offsetting entries (e.g., expense account). Each needs accountResourceId, type (DEBIT/CREDIT), and amount.
1655
- - The API enforces account separation: cash-out accounts cannot be used for cash-in and vice versa.`,
1800
+ - The API enforces account separation: cash-out accounts cannot be used for cash-in.`,
1656
1801
  params: {
1657
1802
  reference: { type: 'string', description: 'Reference number' },
1658
1803
  valueDate: { type: 'string', description: 'Date (YYYY-MM-DD)' },
@@ -1749,9 +1894,11 @@ export const TOOL_DEFINITIONS = [
1749
1894
  listTool('list_cash_transfers', 'List cash transfer entries. Paginated.', 'cash_transfers', (client, off, lim) => listCashTransfers(client, { limit: lim, offset: off })),
1750
1895
  {
1751
1896
  name: 'create_cash_transfer',
1752
- description: `Move money between two of YOUR OWN bank/cash accounts (internal transfer). NOT for recording payments to/from contacts use create_cash_in or create_cash_out instead.
1753
- - cashOut = source (money leaves), cashIn = destination (money arrives).
1754
- - Each side needs accountResourceId + amount. That's it for simple transfers.
1897
+ description: `Move money between two of YOUR OWN bank/cash accounts (internal transfer, e.g., main bank to petty cash, USD account to SGD account).
1898
+ WHEN TO USE: petty cash top-ups from bank, inter-account transfers, currency conversions between own accounts.
1899
+ WHEN NOT TO USE: receiving money from external parties (use create_cash_in) or paying external parties (use create_cash_out).
1900
+ - cashOut = source account (money leaves), cashIn = destination account (money arrives).
1901
+ - Each side needs accountResourceId + amount.
1755
1902
  - reference is auto-generated if not provided.
1756
1903
  - Do NOT send currency/exchangeRate — derived server-side from bank account currencies.`,
1757
1904
  params: {
@@ -2077,13 +2224,14 @@ Available export types: trial-balance, balance-sheet, profit-and-loss, general-l
2077
2224
  description: `Plan a transaction recipe — run a financial calculator and show what accounts, contacts, and bank accounts are needed. Read-only (no API calls).
2078
2225
  Supported recipes: ${RECIPE_TYPES.join(', ')}
2079
2226
  Returns: capsule type/name, required accounts, step breakdown (journal/bill/invoice/cash-in/cash-out), and full calculator results.
2080
- Use this BEFORE execute_recipe to verify requirements. Parameters vary by recipe — see recipe skill docs for per-recipe params.`,
2227
+ Use this BEFORE execute_recipe to verify requirements. Parameters vary by recipe — see recipe skill docs for per-recipe params.
2228
+ CRITICAL: ALWAYS call this tool for ANY calculation involving: depreciation, amortization, ECL/expected credit loss, FX revaluation, lease/IFRS 16, hire purchase, loan schedules, provisions, deferred revenue, fixed deposits, or asset disposal. Never compute these manually.`,
2081
2229
  params: {
2082
2230
  recipe: { type: 'string', enum: [...RECIPE_TYPES], description: 'Recipe type' },
2083
2231
  // Universal params
2084
2232
  amount: { type: 'number', description: 'Amount (for amortization, accrued-expense, dividend, ecl, provision, fx-reval)' },
2085
2233
  principal: { type: 'number', description: 'Principal (for loan, fixed-deposit)' },
2086
- startDate: { type: 'string', description: 'Start date YYYY-MM-DD (enables blueprint generation for most recipes)' },
2234
+ startDate: { type: 'string', description: 'Start date YYYY-MM-DD (REQUIRED for blueprint generation default to today if user does not specify)' },
2087
2235
  currency: { type: 'string', description: 'Currency code (e.g. SGD, USD)' },
2088
2236
  periods: { type: 'number', description: 'Number of periods (for amortization, accrued-expense, leave-accrual)' },
2089
2237
  frequency: { type: 'string', enum: ['monthly', 'quarterly', 'annual'], description: 'Frequency (for depreciation, amortization, accrued-expense)' },
@@ -2609,7 +2757,7 @@ Auto-resolves accounts from chart of accounts. Provide bankAccountName for recip
2609
2757
  }),
2610
2758
  },
2611
2759
  // ══════════════════════════════════════════════════════════════
2612
- // ── Job Blueprint Tools (offline — no API calls) ─────────────
2760
+ // ── Close Procedure Jobs (offline — no API calls) ────────────
2613
2761
  // ══════════════════════════════════════════════════════════════
2614
2762
  {
2615
2763
  name: 'generate_month_end_blueprint',
@@ -2619,7 +2767,7 @@ Auto-resolves accounts from chart of accounts. Provide bankAccountName for recip
2619
2767
  currency: { type: 'string', description: 'Base currency (e.g., "SGD"). Defaults to SGD.' },
2620
2768
  },
2621
2769
  required: ['period'],
2622
- group: 'jobs',
2770
+ group: 'close_jobs',
2623
2771
  readOnly: true,
2624
2772
  execute: async (_ctx, input) => generateMonthEndBlueprint({
2625
2773
  period: input.period,
@@ -2635,7 +2783,7 @@ Auto-resolves accounts from chart of accounts. Provide bankAccountName for recip
2635
2783
  incremental: { type: 'boolean', description: 'Skip month-end phases (assumes months already closed)' },
2636
2784
  },
2637
2785
  required: ['period'],
2638
- group: 'jobs',
2786
+ group: 'close_jobs',
2639
2787
  readOnly: true,
2640
2788
  execute: async (_ctx, input) => generateQuarterEndBlueprint({
2641
2789
  period: input.period,
@@ -2652,7 +2800,7 @@ Auto-resolves accounts from chart of accounts. Provide bankAccountName for recip
2652
2800
  incremental: { type: 'boolean', description: 'Skip quarter/month phases (assumes already closed)' },
2653
2801
  },
2654
2802
  required: ['period'],
2655
- group: 'jobs',
2803
+ group: 'close_jobs',
2656
2804
  readOnly: true,
2657
2805
  execute: async (_ctx, input) => generateYearEndBlueprint({
2658
2806
  period: input.period,
@@ -2669,7 +2817,7 @@ Auto-resolves accounts from chart of accounts. Provide bankAccountName for recip
2669
2817
  currency: { type: 'string', description: 'Base currency (e.g., "SGD"). Defaults to SGD.' },
2670
2818
  },
2671
2819
  required: [],
2672
- group: 'jobs',
2820
+ group: 'close_jobs',
2673
2821
  readOnly: true,
2674
2822
  execute: async (_ctx, input) => generateBankReconBlueprint({
2675
2823
  account: input.account,
@@ -2685,7 +2833,7 @@ Auto-resolves accounts from chart of accounts. Provide bankAccountName for recip
2685
2833
  currency: { type: 'string', description: 'Base currency (e.g., "SGD"). Defaults to SGD.' },
2686
2834
  },
2687
2835
  required: ['period'],
2688
- group: 'jobs',
2836
+ group: 'close_jobs',
2689
2837
  readOnly: true,
2690
2838
  execute: async (_ctx, input) => generateGstVatBlueprint({
2691
2839
  period: input.period,
@@ -2700,7 +2848,7 @@ Auto-resolves accounts from chart of accounts. Provide bankAccountName for recip
2700
2848
  currency: { type: 'string', description: 'Base currency (e.g., "SGD"). Defaults to SGD.' },
2701
2849
  },
2702
2850
  required: [],
2703
- group: 'jobs',
2851
+ group: 'operational_jobs',
2704
2852
  readOnly: true,
2705
2853
  execute: async (_ctx, input) => generatePaymentRunBlueprint({
2706
2854
  dueBefore: input.dueBefore,
@@ -2715,7 +2863,7 @@ Auto-resolves accounts from chart of accounts. Provide bankAccountName for recip
2715
2863
  currency: { type: 'string', description: 'Base currency (e.g., "SGD"). Defaults to SGD.' },
2716
2864
  },
2717
2865
  required: [],
2718
- group: 'jobs',
2866
+ group: 'operational_jobs',
2719
2867
  readOnly: true,
2720
2868
  execute: async (_ctx, input) => generateCreditControlBlueprint({
2721
2869
  overdueDays: input.overdueDays,
@@ -2731,7 +2879,7 @@ Auto-resolves accounts from chart of accounts. Provide bankAccountName for recip
2731
2879
  currency: { type: 'string', description: 'Base currency (e.g., "SGD"). Defaults to SGD.' },
2732
2880
  },
2733
2881
  required: [],
2734
- group: 'jobs',
2882
+ group: 'operational_jobs',
2735
2883
  readOnly: true,
2736
2884
  execute: async (_ctx, input) => generateSupplierReconBlueprint({
2737
2885
  supplier: input.supplier,
@@ -2747,7 +2895,7 @@ Auto-resolves accounts from chart of accounts. Provide bankAccountName for recip
2747
2895
  currency: { type: 'string', description: 'Base currency (e.g., "SGD"). Defaults to SGD.' },
2748
2896
  },
2749
2897
  required: ['period'],
2750
- group: 'jobs',
2898
+ group: 'close_jobs',
2751
2899
  readOnly: true,
2752
2900
  execute: async (_ctx, input) => generateAuditPrepBlueprint({
2753
2901
  period: input.period,
@@ -2761,7 +2909,7 @@ Auto-resolves accounts from chart of accounts. Provide bankAccountName for recip
2761
2909
  currency: { type: 'string', description: 'Base currency (e.g., "SGD"). Defaults to SGD.' },
2762
2910
  },
2763
2911
  required: [],
2764
- group: 'jobs',
2912
+ group: 'operational_jobs',
2765
2913
  readOnly: true,
2766
2914
  execute: async (_ctx, input) => generateFaReviewBlueprint({
2767
2915
  currency: input.currency,
@@ -2774,7 +2922,7 @@ Auto-resolves accounts from chart of accounts. Provide bankAccountName for recip
2774
2922
  currency: { type: 'string', description: 'Base currency (e.g., "SGD"). Defaults to SGD.' },
2775
2923
  },
2776
2924
  required: [],
2777
- group: 'jobs',
2925
+ group: 'operational_jobs',
2778
2926
  readOnly: true,
2779
2927
  execute: async (_ctx, input) => generateDocumentCollectionBlueprint({
2780
2928
  currency: input.currency,
@@ -2789,7 +2937,7 @@ Auto-resolves accounts from chart of accounts. Provide bankAccountName for recip
2789
2937
  currency: { type: 'string', description: 'Base currency (e.g., "SGD"). Defaults to SGD.' },
2790
2938
  },
2791
2939
  required: [],
2792
- group: 'jobs',
2940
+ group: 'operational_jobs',
2793
2941
  readOnly: true,
2794
2942
  execute: async (_ctx, input) => generateStatutoryFilingBlueprint({
2795
2943
  ya: input.ya,
@@ -3189,12 +3337,12 @@ Auto-resolves accounts from chart of accounts. Provide bankAccountName for recip
3189
3337
  return handlePagination((off, lim) => searchScheduledTransactions(ctx.client, {
3190
3338
  filter: Object.keys(filter).length > 0 ? filter : undefined,
3191
3339
  limit: lim, offset: off,
3192
- sort: { sortBy: [sortBy ?? 'valueDate'], order: (sortOrder ?? 'DESC') },
3340
+ sort: { sortBy: [sortBy ?? 'startDate'], order: (sortOrder ?? 'DESC') },
3193
3341
  }), limit, offset, 20);
3194
3342
  },
3195
3343
  },
3196
3344
  // ══════════════════════════════════════════════════════════════
3197
- // ── Reports (8 new) ────────────────────────────────────────
3345
+ // ── Financial Reports (continued) ─────────────────────────
3198
3346
  // ══════════════════════════════════════════════════════════════
3199
3347
  {
3200
3348
  name: 'generate_vat_ledger',
@@ -3204,7 +3352,7 @@ Auto-resolves accounts from chart of accounts. Provide bankAccountName for recip
3204
3352
  endDate: { type: 'string', description: 'End date (YYYY-MM-DD)' },
3205
3353
  },
3206
3354
  required: ['startDate', 'endDate'],
3207
- group: 'reports',
3355
+ group: 'financial_reports',
3208
3356
  readOnly: true,
3209
3357
  execute: async (ctx, input) => generateVatLedger(ctx.client, {
3210
3358
  startDate: input.startDate,
@@ -3222,7 +3370,7 @@ Auto-resolves accounts from chart of accounts. Provide bankAccountName for recip
3222
3370
  compareCount: { type: 'number', description: 'Number of comparison periods' },
3223
3371
  },
3224
3372
  required: ['primarySnapshotStartDate', 'primarySnapshotEndDate'],
3225
- group: 'reports',
3373
+ group: 'financial_reports',
3226
3374
  readOnly: true,
3227
3375
  execute: async (ctx, input) => generateEquityMovement(ctx.client, input),
3228
3376
  },
@@ -3234,7 +3382,7 @@ Auto-resolves accounts from chart of accounts. Provide bankAccountName for recip
3234
3382
  currencyCode: { type: 'string', description: 'Currency code (e.g., "SGD")' },
3235
3383
  },
3236
3384
  required: ['primarySnapshotDate'],
3237
- group: 'reports',
3385
+ group: 'operational_reports',
3238
3386
  readOnly: true,
3239
3387
  execute: async (ctx, input) => generateBankBalanceSummary(ctx.client, {
3240
3388
  primarySnapshotDate: input.primarySnapshotDate,
@@ -3252,7 +3400,7 @@ Auto-resolves accounts from chart of accounts. Provide bankAccountName for recip
3252
3400
  tags: { type: 'array', items: { type: 'string' }, description: 'Filter by tags' },
3253
3401
  },
3254
3402
  required: ['bankAccountResourceId', 'primarySnapshotStartDate', 'primarySnapshotEndDate'],
3255
- group: 'reports',
3403
+ group: 'operational_reports',
3256
3404
  readOnly: true,
3257
3405
  execute: async (ctx, input) => generateBankReconSummary(ctx.client, input),
3258
3406
  },
@@ -3267,7 +3415,7 @@ Auto-resolves accounts from chart of accounts. Provide bankAccountName for recip
3267
3415
  currencyCode: { type: 'string', description: 'Currency code' },
3268
3416
  },
3269
3417
  required: ['bankAccountResourceId', 'primarySnapshotStartDate', 'primarySnapshotEndDate', 'filter'],
3270
- group: 'reports',
3418
+ group: 'operational_reports',
3271
3419
  readOnly: true,
3272
3420
  execute: async (ctx, input) => generateBankReconDetails(ctx.client, input),
3273
3421
  },
@@ -3281,7 +3429,7 @@ Auto-resolves accounts from chart of accounts. Provide bankAccountName for recip
3281
3429
  currencyCode: { type: 'string', description: 'Currency code' },
3282
3430
  },
3283
3431
  required: ['primarySnapshotStartDate', 'primarySnapshotEndDate', 'groupBy'],
3284
- group: 'reports',
3432
+ group: 'operational_reports',
3285
3433
  readOnly: true,
3286
3434
  execute: async (ctx, input) => generateFaSummary(ctx.client, input),
3287
3435
  },
@@ -3295,7 +3443,7 @@ Auto-resolves accounts from chart of accounts. Provide bankAccountName for recip
3295
3443
  currencyCode: { type: 'string', description: 'Currency code' },
3296
3444
  },
3297
3445
  required: ['primarySnapshotStartDate', 'primarySnapshotEndDate'],
3298
- group: 'reports',
3446
+ group: 'operational_reports',
3299
3447
  readOnly: true,
3300
3448
  execute: async (ctx, input) => generateFaReconSummary(ctx.client, input),
3301
3449
  },
@@ -3306,7 +3454,7 @@ Auto-resolves accounts from chart of accounts. Provide bankAccountName for recip
3306
3454
  endDate: { type: 'string', description: 'Report date (YYYY-MM-DD) — point-in-time snapshot' },
3307
3455
  },
3308
3456
  required: ['endDate'],
3309
- group: 'reports',
3457
+ group: 'operational_reports',
3310
3458
  readOnly: true,
3311
3459
  execute: async (ctx, input) => generateArReport(ctx.client, { endDate: input.endDate }),
3312
3460
  },
@@ -3535,7 +3683,7 @@ Works for invoices, bills, customer credit notes, supplier credit notes, and jou
3535
3683
  resourceId: { type: 'string', description: 'Transaction resourceId' },
3536
3684
  },
3537
3685
  required: ['transactionType', 'resourceId'],
3538
- group: 'invoices',
3686
+ group: 'search',
3539
3687
  readOnly: true,
3540
3688
  execute: async (ctx, input) => {
3541
3689
  const type = input.transactionType;
@@ -3602,18 +3750,18 @@ Works for invoices, bills, customer credit notes, supplier credit notes, and jou
3602
3750
  const results = [];
3603
3751
  for (const item of items) {
3604
3752
  try {
3605
- const lineItems = await fetchLineItems(ctx.client, item.type, item.resourceId);
3753
+ const merged = await fetchAndMerge(ctx.client, item.type, item.resourceId, {});
3606
3754
  if (item.type === 'invoice') {
3607
- await finalizeInvoice(ctx.client, item.resourceId, { lineItems });
3755
+ await finalizeInvoice(ctx.client, item.resourceId, merged);
3608
3756
  }
3609
3757
  else if (item.type === 'bill') {
3610
- await finalizeBill(ctx.client, item.resourceId, { lineItems });
3758
+ await finalizeBill(ctx.client, item.resourceId, merged);
3611
3759
  }
3612
3760
  else if (item.type === 'customer_credit_note') {
3613
- await finalizeCustomerCreditNote(ctx.client, item.resourceId, { lineItems });
3761
+ await finalizeCustomerCreditNote(ctx.client, item.resourceId, merged);
3614
3762
  }
3615
3763
  else if (item.type === 'supplier_credit_note') {
3616
- await finalizeSupplierCreditNote(ctx.client, item.resourceId, { lineItems });
3764
+ await finalizeSupplierCreditNote(ctx.client, item.resourceId, merged);
3617
3765
  }
3618
3766
  results.push({ ...item, success: true });
3619
3767
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jaz-clio",
3
- "version": "4.30.0",
3
+ "version": "4.30.2",
4
4
  "description": "Clio — Command Line Interface Orchestrator for Jaz AI.",
5
5
  "type": "module",
6
6
  "bin": {