@go-labs-sg/bb 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,12 +1,12 @@
1
1
  # bb
2
2
 
3
- Command-line interface for **Budget Builder**. Call budgets, bills, approvals, and suppliers from your terminal or from tools that don’t support MCP (e.g. some AI agents). Responses are JSON on stdout.
3
+ Command-line interface for **Budget Builder**. Call budgets, bills, approvals, suppliers, dashboard, and more from your terminal or from tools that don’t support MCP (e.g. some AI agents). Responses are JSON on stdout.
4
4
 
5
5
  **Registry:** [`@go-labs-sg/bb`](https://www.npmjs.com/package/@go-labs-sg/bb)
6
6
 
7
7
  ## Requirements
8
8
 
9
- - **Node.js** 18+
9
+ - **Node.js** 18+ (ESM; relative imports in `dist` use `.js` extensions)
10
10
  - A **Budget Builder API key** (same key used for the MCP server)
11
11
 
12
12
  ## Install
@@ -48,18 +48,23 @@ bb list-suppliers
48
48
 
49
49
  Global options and flags use `--key=value` or `--key value` (see `bb help`).
50
50
 
51
- ## Commands
51
+ **Authoritative command list:** run `bb help` — it includes every command, positional args, and flags. MCP exposes a subset of the same tRPC surface; the CLI additionally includes a few procedures mainly used by the web UI (e.g. `reorder-budget-items`, `update-budget-item-supplier`). You can also use MCP-style `snake_case` (e.g. `bb list_bills`); it is normalized to kebab-case.
52
52
 
53
- | Command | Description |
53
+ **Mutations with `--payload`:** Commands such as `create-budget`, `create-bill`, `update-supplier`, etc. take a single JSON object (`--payload '<json>'`) matching the corresponding tRPC procedure input. Use ISO strings for date/datetime fields; the CLI coerces them where needed. The API still validates the full shape.
54
+
55
+ ### Command overview
56
+
57
+ | Area | Commands (non-exhaustive) |
54
58
  | --- | --- |
55
- | `list-budgets` | List budgets. Flags: `--projectId`, `--name`, `--status` (comma-separated), `--page`, `--perPage` |
56
- | `get-budget <id>` | Budget details and line items |
57
- | `update-budget-status <id> <status>` | Change budget status (e.g. `APPROVED`, `REJECTED`, `DRAFT`, …) |
58
- | `list-bills` | List bills and/or claims. Flags: `--projectId`, `--budgetId`, `--status` (CSV, like `DRAFT,PAID` or `ALL`), `--search`, `--isClaimable` (`true`\|`false`), `--createdByIds` (CSV), `--sortBy` (`createdAt`\|`amount`\|`status`), `--sortDir` (`asc`\|`desc`), `--page`, `--pageSize` |
59
- | `list-claims` | Reimbursable claims only; same flags as `list-bills` except `--isClaimable` is ignored (always true) |
60
- | `approve-bill <id>` | Approve a pending bill (matches your pending bill approvals) |
61
- | `list-approvals` | Pending approvals. Flag: `--type` = `budget`, `supplier`, `bill`, or `all` |
62
- | `list-suppliers` | List suppliers. Flags: `--name`, `--page`, `--perPage` |
59
+ | **Budgets** | `list-budgets`, `get-budget`, `get-budget-items`, `get-budget-details`, `get-budget-categories`, `get-budget-versions`, `update-budget-status`, `create-budget` / `update-budget` (`--payload`), `delete-budget`, `create-budget-approval`, `add-budget-items`, `update-budget-item`, `remove-budget-item`, `reorder-budget-items`, `update-budget-item-supplier`, `create-budget-category`, `update-budget-category`, `delete-budget-category`, `update-budget-commission`, `update-budget-discount` (`--payload` where noted) |
60
+ | **Bills** | `list-bills`, `list-claims`, `create-bill` (`--payload`), `update-bill` (`--payload`), `delete-bill`, `create-bill-approval`, `update-bill-status`, `patch-bill-payment`, `patch-bill-invoice-number`, `get-bill-attachments`, `get-bill-details` |
61
+ | **Approvals** | `list-approvals` / `get-pending-approvals`, `approve-bill`, `reject-bill`, `approve-budget`, `reject-budget`, `approve-supplier`, `reject-supplier` |
62
+ | **Companies & projects** | `list-companies`, `get-company`, `create-company`, `update-company` (`--payload`), `delete-company`, `list-projects`, `get-project`, `create-project` (optional `--requestQboAccountantNotification false` to skip QBO accountant emails), `update-project` (`--payload`, optional `requestQboAccountantNotification` in JSON), `delete-project`, `update-project-status` |
63
+ | **Contacts** | `list-contacts`, `create-contact-person` (`--payload`), `update-contact-person` (`--payload`) |
64
+ | **Suppliers & items** | `list-suppliers`, `create-supplier` (`--payload`), `update-supplier` (`--payload`), `delete-suppliers` (`--ids` CSV; admin), `create-certification` / `create-payment-method` / `create-supplier-role` / `create-supplier-tag` (`--name`), `get-supplier-details`, `get-supplier-analytics`, `list-items`, `create-item` (`--payload`), `update-item` (`--payload`), `delete-item`, `get-item`, `list-item-categories`, `create-item-category`, `update-item-category`, `delete-item-categories` (`--ids` CSV; admin) |
65
+ | **Dashboard & users** | `list-users`, `get-user-performance`, `get-dashboard`, `get-monthly-metrics`, `get-system-overview`, `get-estimate-performance`, `get-financial-overview` |
66
+ | **Errors** | `get-recent-errors`, `get-error-metrics` |
67
+ | **Historical / benchmarks** | `get-approved-budgets`, `get-budget-category-benchmarks`, `get-item-pricing-history`, `get-supplier-pricing-history` |
63
68
 
64
69
  ## Output
65
70
 
@@ -71,7 +76,7 @@ A `.env` file in the **current working directory** is loaded automatically (for
71
76
 
72
77
  ## Developing in this repo
73
78
 
74
- From the monorepo root:
79
+ From the monorepo root (after `bun install`):
75
80
 
76
81
  ```bash
77
82
  export BB_API_KEY=...
@@ -81,5 +86,15 @@ bun run bb -- list-budgets
81
86
  Or from `packages/cli`:
82
87
 
83
88
  ```bash
89
+ bun run build # emit dist/ via tsc
90
+ bun run typecheck # tsc --noEmit
84
91
  bun run src/index.ts list-budgets
85
92
  ```
93
+
94
+ ### Layout
95
+
96
+ - **`src/filter-enums.ts`** — “extended” filter enums (ALL + Prisma enums), maintained here so the published package does not depend on unpublished workspace packages. Keep in line with `packages/utils/src/filter-enums.ts` when those values change.
97
+
98
+ ### Publish
99
+
100
+ `prepublishOnly` strips `workspace:` / `catalog:` entries from this package’s `package.json`, runs `tsc`, then `postpack` restores the file — so the npm tarball does not contain monorepo protocol dependencies. Types from `@bb/api` are compile-time only (`import type`). Enum **values** in emitted JS import `@bb/db/enums` (workspace `@bb/db` during monorepo development).
package/dist/commands.js CHANGED
@@ -1,6 +1,7 @@
1
- import { BudgetRole, Deals, ExtendedApprovalStatus, ExtendedApprovalType, ExtendedBudgetStatus, TimeFrame, } from "@bb/utils/filter-enums";
2
- import { api } from "./api-client";
3
- import { billStatusesForApi, } from "./parse-cli-enums";
1
+ import { api } from "./api-client.js";
2
+ import { BudgetRole, Deals, ExtendedApprovalStatus, ExtendedApprovalType, ExtendedBudgetStatus, TimeFrame, } from "./filter-enums.js";
3
+ import { billStatusesForApi, } from "./parse-cli-enums.js";
4
+ import { parseBudgetDiscountPayload, parseCompanyUpdatePayload, parseContactCreatePayload, parseContactUpdatePayload, parseCreateBillPayload, parseCreateBudgetPayload, parseItemCreatePayload, parseItemUpdatePayload, parseSupplierCreatePayload, parseSupplierUpdatePayload, parseUpdateBillPayload, parseUpdateBudgetCommissionPayload, parseUpdateBudgetPayload, parseUpdateProjectPayload, } from "./parse-mutation-payload.js";
4
5
  const BUDGET = "BUDGET";
5
6
  const BILL = "BILL";
6
7
  const SUPPLIER = "SUPPLIER";
@@ -200,6 +201,148 @@ export async function getBudgetDetails(budgetId) {
200
201
  const detail = await api.budget.getBudgetDetail.query({ id: budgetId });
201
202
  out(detail);
202
203
  }
204
+ export async function createBudgetFromPayload(raw) {
205
+ const input = parseCreateBudgetPayload(raw);
206
+ const result = await api.budget.createBudget.mutate(input);
207
+ out(result);
208
+ }
209
+ export async function updateBudgetFromPayload(raw) {
210
+ const input = parseUpdateBudgetPayload(raw);
211
+ const result = await api.budget.updateBudget.mutate(input);
212
+ out(result);
213
+ }
214
+ export async function deleteBudgetById(budgetId) {
215
+ const result = await api.budget.deleteBudget.mutate({ id: budgetId });
216
+ out(result);
217
+ }
218
+ export async function deleteProjectById(projectId) {
219
+ const result = await api.project.deleteProject.mutate({ id: projectId });
220
+ out(result);
221
+ }
222
+ export async function deleteCompanyById(companyId) {
223
+ const result = await api.company.deleteCompany.mutate({ id: companyId });
224
+ out(result);
225
+ }
226
+ export async function reorderBudgetItemsCli(opts) {
227
+ const result = await api.budgetItem.reorderBudgetItems.mutate(opts);
228
+ out(result);
229
+ }
230
+ export async function updateBudgetItemSupplierCli(opts) {
231
+ const result = await api.budgetItem.updateBudgetItemSupplier.mutate(opts);
232
+ out(result);
233
+ }
234
+ export async function createBudgetCategory(name) {
235
+ const result = await api.budget.createBudgetCategory.mutate({ name });
236
+ out(result);
237
+ }
238
+ export async function updateBudgetCategory(opts) {
239
+ const result = await api.budget.updateBudgetCategory.mutate(opts);
240
+ out(result);
241
+ }
242
+ export async function deleteBudgetCategory(id) {
243
+ const result = await api.budget.deleteBudgetCategory.mutate({ id });
244
+ out(result);
245
+ }
246
+ export async function updateBudgetCommissionFromPayload(raw) {
247
+ const input = parseUpdateBudgetCommissionPayload(raw);
248
+ const result = await api.budget.updateBudgetCommission.mutate(input);
249
+ out(result);
250
+ }
251
+ export async function updateBudgetDiscountFromPayload(raw) {
252
+ const input = parseBudgetDiscountPayload(raw);
253
+ const result = await api.budget.updateBudgetDiscount.mutate(input);
254
+ out(result);
255
+ }
256
+ export async function deleteItemById(itemId) {
257
+ const result = await api.item.deleteItem.mutate({ id: itemId });
258
+ out(result);
259
+ }
260
+ export async function deleteSuppliersByIds(ids) {
261
+ const result = await api.supplier.deleteSuppliers.mutate({ ids });
262
+ out(result);
263
+ }
264
+ export async function createSupplierCertification(name) {
265
+ const result = await api.supplier.createCertification.mutate({ name });
266
+ out(result);
267
+ }
268
+ export async function createSupplierPaymentMethod(name) {
269
+ const result = await api.supplier.createPaymentMethod.mutate({ name });
270
+ out(result);
271
+ }
272
+ export async function createSupplierRoleOption(name) {
273
+ const result = await api.supplier.createSupplierRole.mutate({ name });
274
+ out(result);
275
+ }
276
+ export async function createSupplierTagOption(name) {
277
+ const result = await api.supplier.createSupplierTag.mutate({ name });
278
+ out(result);
279
+ }
280
+ export async function createItemCategory(name) {
281
+ const result = await api.itemCategory.createItemCategory.mutate({ name });
282
+ out(result);
283
+ }
284
+ export async function updateItemCategory(opts) {
285
+ const result = await api.itemCategory.updateItemCategory.mutate(opts);
286
+ out(result);
287
+ }
288
+ export async function deleteItemCategoriesByIds(ids) {
289
+ const result = await api.itemCategory.deleteItemCategories.mutate({ ids });
290
+ out(result);
291
+ }
292
+ export async function createBillFromPayload(raw) {
293
+ const input = parseCreateBillPayload(raw);
294
+ const result = await api.bill.create.mutate(input);
295
+ out(result);
296
+ }
297
+ export async function updateBillFromPayload(raw) {
298
+ const input = parseUpdateBillPayload(raw);
299
+ const result = await api.bill.update.mutate(input);
300
+ out(result);
301
+ }
302
+ export async function deleteBillById(id) {
303
+ const result = await api.bill.delete.mutate({ id });
304
+ out(result);
305
+ }
306
+ export async function createSupplierFromPayload(raw) {
307
+ const input = parseSupplierCreatePayload(raw);
308
+ const result = await api.supplier.createSupplier.mutate(input);
309
+ out(result);
310
+ }
311
+ export async function updateSupplierFromPayload(raw) {
312
+ const input = parseSupplierUpdatePayload(raw);
313
+ const result = await api.supplier.updateSupplier.mutate(input);
314
+ out(result);
315
+ }
316
+ export async function createItemFromPayload(raw) {
317
+ const input = parseItemCreatePayload(raw);
318
+ const result = await api.item.createItem.mutate(input);
319
+ out(result);
320
+ }
321
+ export async function updateItemFromPayload(raw) {
322
+ const input = parseItemUpdatePayload(raw);
323
+ const result = await api.item.updateItem.mutate(input);
324
+ out(result);
325
+ }
326
+ export async function createContactPersonFromPayload(raw) {
327
+ const input = parseContactCreatePayload(raw);
328
+ const result = await api.contactPerson.createContactPerson.mutate(input);
329
+ out(result);
330
+ }
331
+ export async function updateContactPersonFromPayload(raw) {
332
+ const input = parseContactUpdatePayload(raw);
333
+ const result = await api.contactPerson.updateContactPerson.mutate(input);
334
+ out(result);
335
+ }
336
+ export async function updateProjectFromPayload(raw) {
337
+ const input = parseUpdateProjectPayload(raw);
338
+ const result = await api.project.updateProject.mutate(input);
339
+ out(result);
340
+ }
341
+ export async function updateCompanyFromPayload(raw) {
342
+ const input = parseCompanyUpdatePayload(raw);
343
+ const result = await api.company.updateCompany.mutate(input);
344
+ out(result);
345
+ }
203
346
  // --- Approvals: budgets / suppliers / bills (MCP parity) ---
204
347
  export async function approveBudget(budgetId) {
205
348
  const notifications = await api.budget.getNotifications.query();
@@ -338,6 +481,9 @@ export async function createProject(opts) {
338
481
  from: new Date(opts.startDate),
339
482
  to: opts.endDate ? new Date(opts.endDate) : undefined,
340
483
  },
484
+ ...(opts.requestQboAccountantNotification !== undefined && {
485
+ requestQboAccountantNotification: opts.requestQboAccountantNotification,
486
+ }),
341
487
  });
342
488
  out(result);
343
489
  }
@@ -403,6 +549,39 @@ export async function getDashboard(input) {
403
549
  });
404
550
  out(result);
405
551
  }
552
+ export async function getMonthlyMetrics(input) {
553
+ const result = await api.dashboard.getMonthlyMetrics.query({
554
+ userId: input.userId,
555
+ role: input.role ?? "ALL",
556
+ deals: input.deals ?? "ALL",
557
+ timeFrame: input.timeFrame ?? "ALL",
558
+ startDate: input.startDate,
559
+ endDate: input.endDate,
560
+ });
561
+ out(result);
562
+ }
563
+ export async function getSystemOverview(input) {
564
+ const result = await api.dashboard.getSystemOverview.query({
565
+ userId: input.userId,
566
+ role: input.role ?? "ALL",
567
+ deals: input.deals ?? "ALL",
568
+ timeFrame: input.timeFrame ?? "ALL",
569
+ startDate: input.startDate,
570
+ endDate: input.endDate,
571
+ });
572
+ out(result);
573
+ }
574
+ export async function getEstimatePerformance(input) {
575
+ const result = await api.dashboard.getEstimatePerformance.query({
576
+ userId: input.userId,
577
+ role: input.role ?? "ALL",
578
+ deals: input.deals ?? "ALL",
579
+ timeFrame: input.timeFrame ?? "ALL",
580
+ startDate: input.startDate,
581
+ endDate: input.endDate,
582
+ });
583
+ out(result);
584
+ }
406
585
  export async function getFinancialOverview(input) {
407
586
  const result = await api.dashboard.getFinancialOverview.query({
408
587
  userId: input.userId,
@@ -0,0 +1,58 @@
1
+ import { ApprovalStatus, ApprovalType, BillStatus, BudgetStatus, ErrorSeverity, ErrorStatus, EstimationMode, ProjectStatus, } from "@bb/db/enums";
2
+ export const ExtendedApprovalStatus = {
3
+ ...ApprovalStatus,
4
+ ALL: "ALL",
5
+ };
6
+ export const ExtendedApprovalType = {
7
+ ...ApprovalType,
8
+ ALL: "ALL",
9
+ };
10
+ export const ExtendedProjectStatus = {
11
+ ...ProjectStatus,
12
+ ALL: "ALL",
13
+ };
14
+ export const ExtendedBudgetStatus = {
15
+ ...BudgetStatus,
16
+ ALL: "ALL",
17
+ };
18
+ export const ExtendedBillStatus = {
19
+ ...BillStatus,
20
+ ALL: "ALL",
21
+ };
22
+ export const ExtendedEstimationMode = {
23
+ ...EstimationMode,
24
+ ALL: "ALL",
25
+ };
26
+ export const BudgetRole = {
27
+ ALL: "ALL",
28
+ BD: "BD",
29
+ CREATOR: "CREATOR",
30
+ INSIDE_SALES: "INSIDE_SALES",
31
+ };
32
+ export const Deals = {
33
+ ALL: "ALL",
34
+ SUCCESSFUL: "SUCCESSFUL",
35
+ LOST: "LOST",
36
+ };
37
+ export const TimeFrame = {
38
+ ALL: "ALL",
39
+ LAST_YEAR: "LAST_YEAR",
40
+ LAST_6_MONTHS: "LAST_6_MONTHS",
41
+ LAST_3_MONTHS: "LAST_3_MONTHS",
42
+ CUSTOM: "CUSTOM",
43
+ };
44
+ export const ChartTimeFrame = {
45
+ LAST_30_DAYS: "last_30_days",
46
+ LAST_3_MONTHS: "last_3_months",
47
+ LAST_6_MONTHS: "last_6_months",
48
+ LAST_1_YEAR: "last_1_year",
49
+ ALL_TIME: "all_time",
50
+ };
51
+ export const ExtendedErrorSeverity = {
52
+ ALL: "ALL",
53
+ ...ErrorSeverity,
54
+ };
55
+ export const ExtendedErrorStatus = {
56
+ ALL: "ALL",
57
+ ...ErrorStatus,
58
+ };
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import "./load-env";
3
- import { requireApiKey } from "./api-client";
4
- import { addBudgetItems, approveBill, approveBudget, approveSupplier, createBillApproval, createBudgetApproval, createCompany, createProject, getApprovedBudgets, getBillAttachments, getBillDetails, getBudget, getBudgetCategories, getBudgetCategoryBenchmarks, getBudgetDetails, getBudgetItemsOnly, getBudgetVersions, getCompany, getDashboard, getErrorMetrics, getFinancialOverview, getItem, getItemPricingHistory, getProject, getRecentErrors, getSupplierAnalytics, getSupplierDetails, getSupplierPricingHistory, getUserPerformance, listApprovals, listBills, listBudgets, listCompanies, listContacts, listItemCategories, listItems, listProjects, listSuppliers, listUsers, patchBillInvoiceNumber, patchBillPayment, rejectBill, rejectBudget, rejectSupplier, removeBudgetItem, updateBillStatus, updateBudgetItem, updateBudgetStatus, updateProjectStatus, } from "./commands.js";
2
+ import "./load-env.js";
3
+ import { requireApiKey } from "./api-client.js";
4
+ import { addBudgetItems, approveBill, approveBudget, approveSupplier, createBillApproval, createBillFromPayload, createBudgetApproval, createBudgetCategory, createBudgetFromPayload, createCompany, createContactPersonFromPayload, createItemCategory, createItemFromPayload, createProject, createSupplierCertification, createSupplierFromPayload, createSupplierPaymentMethod, createSupplierRoleOption, createSupplierTagOption, deleteBillById, deleteBudgetById, deleteBudgetCategory, deleteCompanyById, deleteItemById, deleteItemCategoriesByIds, deleteProjectById, deleteSuppliersByIds, getApprovedBudgets, getBillAttachments, getBillDetails, getBudget, getBudgetCategories, getBudgetCategoryBenchmarks, getBudgetDetails, getBudgetItemsOnly, getBudgetVersions, getCompany, getDashboard, getErrorMetrics, getEstimatePerformance, getFinancialOverview, getItem, getItemPricingHistory, getMonthlyMetrics, getProject, getRecentErrors, getSupplierAnalytics, getSupplierDetails, getSupplierPricingHistory, getSystemOverview, getUserPerformance, listApprovals, listBills, listBudgets, listCompanies, listContacts, listItemCategories, listItems, listProjects, listSuppliers, listUsers, patchBillInvoiceNumber, patchBillPayment, rejectBill, rejectBudget, rejectSupplier, removeBudgetItem, reorderBudgetItemsCli, updateBillFromPayload, updateBillStatus, updateBudgetCategory, updateBudgetCommissionFromPayload, updateBudgetDiscountFromPayload, updateBudgetFromPayload, updateBudgetItem, updateBudgetItemSupplierCli, updateBudgetStatus, updateCompanyFromPayload, updateContactPersonFromPayload, updateItemCategory, updateItemFromPayload, updateProjectFromPayload, updateProjectStatus, updateSupplierFromPayload, } from "./commands.js";
5
5
  import { getFlag, parseArgs } from "./parse-args.js";
6
6
  import { billStatusesForUpdateHelp, budgetStatusesForHelp, isBudgetStatusUpdate, parseApprovalTypeFlag, parseBillStatusForUpdate, parseCommaSeparatedBillStatuses, parseCommaSeparatedBudgetStatuses, parseCommaSeparatedIds, parseOptionalBillListSortBy, parseOptionalBillListSortDir, parseOptionalDashboardRole, parseOptionalDeals, parseOptionalErrorSeverity, parseOptionalErrorStatus, parseOptionalExtendedProjectStatus, parseOptionalFinancialRole, parseOptionalSupplierAnalyticsTimeFrame, parseOptionalTimeFrame, parseProjectStatusForUpdate, projectStatusesForHelp, } from "./parse-cli-enums.js";
7
7
  import { parseJsonFlag, parseOptionalNumber as parseOptNum, } from "./parse-json-flag.js";
@@ -44,6 +44,18 @@ function parseListBillsFlags(flags) {
44
44
  pageSize: parsePositiveIntFlag(getFlag(flags, "pageSize"), "--pageSize"),
45
45
  };
46
46
  }
47
+ function parseDashboardQueryFlags(flags) {
48
+ const roleRaw = getFlag(flags, "role");
49
+ const timeFrameRaw = getFlag(flags, "timeFrame");
50
+ return {
51
+ userId: getFlag(flags, "userId"),
52
+ role: parseOptionalDashboardRole(roleRaw !== undefined ? String(roleRaw) : undefined),
53
+ deals: parseOptionalDeals(getFlag(flags, "deals")),
54
+ timeFrame: parseOptionalTimeFrame(timeFrameRaw !== undefined ? String(timeFrameRaw) : undefined),
55
+ startDate: getFlag(flags, "startDate"),
56
+ endDate: getFlag(flags, "endDate"),
57
+ };
58
+ }
47
59
  function printHelp() {
48
60
  const help = `
49
61
  bb — Budget Builder CLI (parity with MCP tools)
@@ -61,15 +73,28 @@ Budgets
61
73
  get-budget-versions <budgetId>
62
74
  update-budget-status <id> <status>
63
75
  status: ${budgetStatusesForHelp.join(", ")}
76
+ create-budget --payload '<json>' (budget.createBudget; ISO dates for date/startTime/endTime)
77
+ update-budget --payload '<json>' (budget.updateBudget; must include id)
78
+ delete-budget <budgetId>
64
79
  create-budget-approval <budgetId>
65
80
  add-budget-items --budgetId --items '[{"itemId":"…","quantity":1,"markup":30},…]'
66
81
  update-budget-item --id <budgetItemId> [--description] [--note] [--quantity] [--markup] [--cost] [--unitPrice] [--isFreeOfCharge true|false] [--gstInclusive true|false]
67
82
  remove-budget-item <budgetItemId>
83
+ reorder-budget-items <budgetId> --itemIds <csv>
84
+ update-budget-item-supplier --budgetItemId --supplierId
85
+ create-budget-category --name <text>
86
+ update-budget-category --id --name
87
+ delete-budget-category <id>
88
+ update-budget-commission --payload '<json>' (budget.updateBudgetCommission)
89
+ update-budget-discount --payload '<json>' (budget.updateBudgetDiscount; type percentage|amount)
68
90
 
69
91
  Bills
70
92
  list-bills [--projectId] [--budgetId] [--status CSV] [--search <text>] [--isClaimable true|false] [--createdByIds <csv>] [--sortBy createdAt|amount|status] [--sortDir asc|desc] [--page] [--pageSize]
71
93
  list-claims same flags as list-bills; only reimbursable claims (ignores --isClaimable)
72
94
  create-bill-approval <billId>
95
+ create-bill --payload '<json>' (bill.create; attachments may be [])
96
+ update-bill --payload '<json>' (bill.update; must include id)
97
+ delete-bill <billId>
73
98
  update-bill-status <id> <status> [--rejectionReason] [--paymentTrackingUrl] [--paymentReference]
74
99
  status (${billStatusesForUpdateHelp.join(", ")}) — not PENDING_APPROVAL; use create-bill-approval
75
100
  patch-bill-payment <billId> [--paymentTrackingUrl] [--paymentReference] [--quickbooksBillId]
@@ -90,27 +115,49 @@ Companies & projects
90
115
  list-companies [--name] [--createdBy] [--page] [--perPage]
91
116
  get-company <id>
92
117
  create-company --name --website <url>
118
+ update-company --payload '<json>' (company.updateCompany; must include id, name, website)
119
+ delete-company <id>
93
120
  list-projects [--companyId] [--name] [--status] [--page] [--perPage]
94
121
  get-project <id>
95
- create-project --name --companyId --contactPersonId --startDate <ISO> [--endDate] [--description]
122
+ create-project --name --companyId --contactPersonId --startDate <ISO> [--endDate] [--description] [--requestQboAccountantNotification false]
123
+ update-project --payload '<json>' (project.updateProject; optional requestQboAccountantNotification; default notify like web)
124
+ delete-project <id>
96
125
  update-project-status <id> <status>
97
126
  status: ${projectStatusesForHelp.join(", ")}
98
127
 
99
128
  Contacts
100
129
  list-contacts <companyId>
130
+ create-contact-person --payload '<json>' (contactPerson.createContactPerson; title MR|MRS|MS|DR)
131
+ update-contact-person --payload '<json>' (contactPerson.updateContactPerson; must include id)
101
132
 
102
133
  Suppliers & items
103
134
  list-suppliers [--name] [--page] [--perPage]
135
+ create-supplier --payload '<json>' (supplier.createSupplier)
136
+ update-supplier --payload '<json>' (supplier.updateSupplier; must include id)
137
+ delete-suppliers --ids <csv> (supplier.deleteSuppliers; admin)
138
+ create-certification --name (supplier.createCertification)
139
+ create-payment-method --name (supplier.createPaymentMethod)
140
+ create-supplier-role --name (supplier.createSupplierRole)
141
+ create-supplier-tag --name (supplier.createSupplierTag)
142
+ create-item --payload '<json>' (item.createItem)
143
+ update-item --payload '<json>' (item.updateItem; must include id)
144
+ delete-item <id>
104
145
  get-supplier-details <supplierId>
105
146
  get-supplier-analytics [--name] [--page] [--perPage] [--timeFrame ALL|LAST_YEAR|…]
106
147
  list-items [--name] [--page] [--perPage]
107
148
  get-item <id>
108
149
  list-item-categories [--page] [--perPage]
150
+ create-item-category --name
151
+ update-item-category --id --name
152
+ delete-item-categories --ids <csv> (itemCategory.deleteItemCategories; admin; empty categories only)
109
153
 
110
154
  Dashboard & users
111
155
  list-users
112
156
  get-user-performance [--userId]
113
157
  get-dashboard [--userId] [--role BD|CREATOR|INSIDE_SALES|ALL] [--deals ALL|SUCCESSFUL|LOST] [--timeFrame] [--startDate] [--endDate]
158
+ get-monthly-metrics [same optional flags as get-dashboard] (dashboard.getMonthlyMetrics)
159
+ get-system-overview [same optional flags as get-dashboard] (dashboard.getSystemOverview)
160
+ get-estimate-performance [same optional flags as get-dashboard] (dashboard.getEstimatePerformance)
114
161
  get-financial-overview [same optional flags as dashboard; role uses BudgetRole enum]
115
162
 
116
163
  Errors
@@ -125,8 +172,9 @@ Historical / benchmarks
125
172
 
126
173
  Output: JSON to stdout. Errors to stderr; exit 1 on failure.
127
174
 
128
- MCP parity: every MCP tool maps to one command above. You may use MCP-style
129
- snake_case (e.g. bb list_bills) — it is normalized to kebab-case.
175
+ MCP parity: every MCP tool maps to one command above. The CLI also exposes
176
+ reorder-budget-items and update-budget-item-supplier (web UI–oriented tRPC).
177
+ You may use MCP-style snake_case (e.g. bb list_bills) — normalized to kebab-case.
130
178
 
131
179
  Note: MCP also exposes a Prisma schema resource; the CLI has no equivalent.
132
180
 
@@ -215,6 +263,31 @@ async function main() {
215
263
  await createBudgetApproval(id);
216
264
  break;
217
265
  }
266
+ case "create-budget": {
267
+ const payloadRaw = getFlag(flags, "payload");
268
+ if (!payloadRaw) {
269
+ throw new Error("create-budget requires --payload '<json>'");
270
+ }
271
+ const raw = parseJsonFlag(String(payloadRaw), "--payload");
272
+ await createBudgetFromPayload(raw);
273
+ break;
274
+ }
275
+ case "update-budget": {
276
+ const payloadRaw = getFlag(flags, "payload");
277
+ if (!payloadRaw) {
278
+ throw new Error("update-budget requires --payload '<json>'");
279
+ }
280
+ const raw = parseJsonFlag(String(payloadRaw), "--payload");
281
+ await updateBudgetFromPayload(raw);
282
+ break;
283
+ }
284
+ case "delete-budget": {
285
+ const id = positional[0];
286
+ if (!id)
287
+ throw new Error("delete-budget requires <budgetId>");
288
+ await deleteBudgetById(id);
289
+ break;
290
+ }
218
291
  case "add-budget-items": {
219
292
  const budgetId = getFlag(flags, "budgetId");
220
293
  if (!budgetId)
@@ -247,6 +320,74 @@ async function main() {
247
320
  await removeBudgetItem(id);
248
321
  break;
249
322
  }
323
+ case "reorder-budget-items": {
324
+ const budgetId = positional[0];
325
+ const itemIdsRaw = getFlag(flags, "itemIds");
326
+ if (!budgetId || itemIdsRaw === undefined) {
327
+ throw new Error("reorder-budget-items requires <budgetId> --itemIds <csv>");
328
+ }
329
+ const itemIds = parseCommaSeparatedIds(String(itemIdsRaw));
330
+ if (!itemIds?.length) {
331
+ throw new Error("reorder-budget-items requires non-empty --itemIds <csv>");
332
+ }
333
+ await reorderBudgetItemsCli({ budgetId, itemIds });
334
+ break;
335
+ }
336
+ case "update-budget-item-supplier": {
337
+ const budgetItemId = getFlag(flags, "budgetItemId");
338
+ const supplierId = getFlag(flags, "supplierId");
339
+ if (!budgetItemId || !supplierId) {
340
+ throw new Error("update-budget-item-supplier requires --budgetItemId and --supplierId");
341
+ }
342
+ await updateBudgetItemSupplierCli({
343
+ budgetItemId: String(budgetItemId),
344
+ supplierId: String(supplierId),
345
+ });
346
+ break;
347
+ }
348
+ case "create-budget-category": {
349
+ const name = getFlag(flags, "name");
350
+ if (!name) {
351
+ throw new Error("create-budget-category requires --name");
352
+ }
353
+ await createBudgetCategory(String(name));
354
+ break;
355
+ }
356
+ case "update-budget-category": {
357
+ const id = getFlag(flags, "id");
358
+ const name = getFlag(flags, "name");
359
+ if (!id || !name) {
360
+ throw new Error("update-budget-category requires --id and --name");
361
+ }
362
+ await updateBudgetCategory({ id: String(id), name: String(name) });
363
+ break;
364
+ }
365
+ case "delete-budget-category": {
366
+ const id = positional[0];
367
+ if (!id) {
368
+ throw new Error("delete-budget-category requires <id>");
369
+ }
370
+ await deleteBudgetCategory(id);
371
+ break;
372
+ }
373
+ case "update-budget-commission": {
374
+ const payloadRaw = getFlag(flags, "payload");
375
+ if (!payloadRaw) {
376
+ throw new Error("update-budget-commission requires --payload '<json>'");
377
+ }
378
+ const raw = parseJsonFlag(String(payloadRaw), "--payload");
379
+ await updateBudgetCommissionFromPayload(raw);
380
+ break;
381
+ }
382
+ case "update-budget-discount": {
383
+ const payloadRaw = getFlag(flags, "payload");
384
+ if (!payloadRaw) {
385
+ throw new Error("update-budget-discount requires --payload '<json>'");
386
+ }
387
+ const raw = parseJsonFlag(String(payloadRaw), "--payload");
388
+ await updateBudgetDiscountFromPayload(raw);
389
+ break;
390
+ }
250
391
  case "list-bills": {
251
392
  await listBills(parseListBillsFlags(flags));
252
393
  break;
@@ -265,6 +406,31 @@ async function main() {
265
406
  await createBillApproval(id);
266
407
  break;
267
408
  }
409
+ case "create-bill": {
410
+ const payloadRaw = getFlag(flags, "payload");
411
+ if (!payloadRaw) {
412
+ throw new Error("create-bill requires --payload '<json>'");
413
+ }
414
+ const raw = parseJsonFlag(String(payloadRaw), "--payload");
415
+ await createBillFromPayload(raw);
416
+ break;
417
+ }
418
+ case "update-bill": {
419
+ const payloadRaw = getFlag(flags, "payload");
420
+ if (!payloadRaw) {
421
+ throw new Error("update-bill requires --payload '<json>'");
422
+ }
423
+ const raw = parseJsonFlag(String(payloadRaw), "--payload");
424
+ await updateBillFromPayload(raw);
425
+ break;
426
+ }
427
+ case "delete-bill": {
428
+ const id = positional[0];
429
+ if (!id)
430
+ throw new Error("delete-bill requires <billId>");
431
+ await deleteBillById(id);
432
+ break;
433
+ }
268
434
  case "update-bill-status": {
269
435
  const [id, statusRaw] = positional;
270
436
  if (!id || !statusRaw) {
@@ -402,6 +568,22 @@ async function main() {
402
568
  await createCompany({ name, website });
403
569
  break;
404
570
  }
571
+ case "update-company": {
572
+ const payloadRaw = getFlag(flags, "payload");
573
+ if (!payloadRaw) {
574
+ throw new Error("update-company requires --payload '<json>'");
575
+ }
576
+ const raw = parseJsonFlag(String(payloadRaw), "--payload");
577
+ await updateCompanyFromPayload(raw);
578
+ break;
579
+ }
580
+ case "delete-company": {
581
+ const id = positional[0];
582
+ if (!id)
583
+ throw new Error("delete-company requires <id>");
584
+ await deleteCompanyById(id);
585
+ break;
586
+ }
405
587
  case "list-projects": {
406
588
  const statusRaw = getFlag(flags, "status");
407
589
  await listProjects({
@@ -435,9 +617,26 @@ async function main() {
435
617
  startDate,
436
618
  endDate: getFlag(flags, "endDate"),
437
619
  description: getFlag(flags, "description"),
620
+ requestQboAccountantNotification: parseOptionalBoolFlag(flags, "requestQboAccountantNotification"),
438
621
  });
439
622
  break;
440
623
  }
624
+ case "update-project": {
625
+ const payloadRaw = getFlag(flags, "payload");
626
+ if (!payloadRaw) {
627
+ throw new Error("update-project requires --payload '<json>'");
628
+ }
629
+ const raw = parseJsonFlag(String(payloadRaw), "--payload");
630
+ await updateProjectFromPayload(raw);
631
+ break;
632
+ }
633
+ case "delete-project": {
634
+ const id = positional[0];
635
+ if (!id)
636
+ throw new Error("delete-project requires <id>");
637
+ await deleteProjectById(id);
638
+ break;
639
+ }
441
640
  case "update-project-status": {
442
641
  const [id, statusRaw] = positional;
443
642
  if (!id || !statusRaw) {
@@ -453,6 +652,24 @@ async function main() {
453
652
  await listContacts(id);
454
653
  break;
455
654
  }
655
+ case "create-contact-person": {
656
+ const payloadRaw = getFlag(flags, "payload");
657
+ if (!payloadRaw) {
658
+ throw new Error("create-contact-person requires --payload '<json>'");
659
+ }
660
+ const raw = parseJsonFlag(String(payloadRaw), "--payload");
661
+ await createContactPersonFromPayload(raw);
662
+ break;
663
+ }
664
+ case "update-contact-person": {
665
+ const payloadRaw = getFlag(flags, "payload");
666
+ if (!payloadRaw) {
667
+ throw new Error("update-contact-person requires --payload '<json>'");
668
+ }
669
+ const raw = parseJsonFlag(String(payloadRaw), "--payload");
670
+ await updateContactPersonFromPayload(raw);
671
+ break;
672
+ }
456
673
  case "list-suppliers": {
457
674
  await listSuppliers({
458
675
  name: getFlag(flags, "name"),
@@ -461,6 +678,64 @@ async function main() {
461
678
  });
462
679
  break;
463
680
  }
681
+ case "create-supplier": {
682
+ const payloadRaw = getFlag(flags, "payload");
683
+ if (!payloadRaw) {
684
+ throw new Error("create-supplier requires --payload '<json>'");
685
+ }
686
+ const raw = parseJsonFlag(String(payloadRaw), "--payload");
687
+ await createSupplierFromPayload(raw);
688
+ break;
689
+ }
690
+ case "update-supplier": {
691
+ const payloadRaw = getFlag(flags, "payload");
692
+ if (!payloadRaw) {
693
+ throw new Error("update-supplier requires --payload '<json>'");
694
+ }
695
+ const raw = parseJsonFlag(String(payloadRaw), "--payload");
696
+ await updateSupplierFromPayload(raw);
697
+ break;
698
+ }
699
+ case "delete-suppliers": {
700
+ const idsRaw = getFlag(flags, "ids");
701
+ if (idsRaw === undefined) {
702
+ throw new Error("delete-suppliers requires --ids <csv>");
703
+ }
704
+ const ids = parseCommaSeparatedIds(String(idsRaw));
705
+ if (!ids?.length) {
706
+ throw new Error("delete-suppliers requires non-empty --ids");
707
+ }
708
+ await deleteSuppliersByIds(ids);
709
+ break;
710
+ }
711
+ case "create-certification": {
712
+ const name = getFlag(flags, "name");
713
+ if (!name)
714
+ throw new Error("create-certification requires --name");
715
+ await createSupplierCertification(String(name));
716
+ break;
717
+ }
718
+ case "create-payment-method": {
719
+ const name = getFlag(flags, "name");
720
+ if (!name)
721
+ throw new Error("create-payment-method requires --name");
722
+ await createSupplierPaymentMethod(String(name));
723
+ break;
724
+ }
725
+ case "create-supplier-role": {
726
+ const name = getFlag(flags, "name");
727
+ if (!name)
728
+ throw new Error("create-supplier-role requires --name");
729
+ await createSupplierRoleOption(String(name));
730
+ break;
731
+ }
732
+ case "create-supplier-tag": {
733
+ const name = getFlag(flags, "name");
734
+ if (!name)
735
+ throw new Error("create-supplier-tag requires --name");
736
+ await createSupplierTagOption(String(name));
737
+ break;
738
+ }
464
739
  case "get-supplier-analytics": {
465
740
  const tfRaw = getFlag(flags, "timeFrame");
466
741
  await getSupplierAnalytics({
@@ -486,6 +761,31 @@ async function main() {
486
761
  await getItem(id);
487
762
  break;
488
763
  }
764
+ case "create-item": {
765
+ const payloadRaw = getFlag(flags, "payload");
766
+ if (!payloadRaw) {
767
+ throw new Error("create-item requires --payload '<json>'");
768
+ }
769
+ const raw = parseJsonFlag(String(payloadRaw), "--payload");
770
+ await createItemFromPayload(raw);
771
+ break;
772
+ }
773
+ case "update-item": {
774
+ const payloadRaw = getFlag(flags, "payload");
775
+ if (!payloadRaw) {
776
+ throw new Error("update-item requires --payload '<json>'");
777
+ }
778
+ const raw = parseJsonFlag(String(payloadRaw), "--payload");
779
+ await updateItemFromPayload(raw);
780
+ break;
781
+ }
782
+ case "delete-item": {
783
+ const id = positional[0];
784
+ if (!id)
785
+ throw new Error("delete-item requires <id>");
786
+ await deleteItemById(id);
787
+ break;
788
+ }
489
789
  case "list-item-categories": {
490
790
  await listItemCategories({
491
791
  page: parsePositiveIntFlag(getFlag(flags, "page"), "--page"),
@@ -493,6 +793,34 @@ async function main() {
493
793
  });
494
794
  break;
495
795
  }
796
+ case "create-item-category": {
797
+ const name = getFlag(flags, "name");
798
+ if (!name)
799
+ throw new Error("create-item-category requires --name");
800
+ await createItemCategory(String(name));
801
+ break;
802
+ }
803
+ case "update-item-category": {
804
+ const id = getFlag(flags, "id");
805
+ const name = getFlag(flags, "name");
806
+ if (!id || !name) {
807
+ throw new Error("update-item-category requires --id and --name");
808
+ }
809
+ await updateItemCategory({ id: String(id), name: String(name) });
810
+ break;
811
+ }
812
+ case "delete-item-categories": {
813
+ const idsRaw = getFlag(flags, "ids");
814
+ if (idsRaw === undefined) {
815
+ throw new Error("delete-item-categories requires --ids <csv>");
816
+ }
817
+ const ids = parseCommaSeparatedIds(String(idsRaw));
818
+ if (!ids?.length) {
819
+ throw new Error("delete-item-categories requires non-empty --ids");
820
+ }
821
+ await deleteItemCategoriesByIds(ids);
822
+ break;
823
+ }
496
824
  case "list-users": {
497
825
  await listUsers();
498
826
  break;
@@ -502,14 +830,19 @@ async function main() {
502
830
  break;
503
831
  }
504
832
  case "get-dashboard": {
505
- await getDashboard({
506
- userId: getFlag(flags, "userId"),
507
- role: parseOptionalDashboardRole(getFlag(flags, "role")),
508
- deals: parseOptionalDeals(getFlag(flags, "deals")),
509
- timeFrame: parseOptionalTimeFrame(getFlag(flags, "timeFrame")),
510
- startDate: getFlag(flags, "startDate"),
511
- endDate: getFlag(flags, "endDate"),
512
- });
833
+ await getDashboard(parseDashboardQueryFlags(flags));
834
+ break;
835
+ }
836
+ case "get-monthly-metrics": {
837
+ await getMonthlyMetrics(parseDashboardQueryFlags(flags));
838
+ break;
839
+ }
840
+ case "get-system-overview": {
841
+ await getSystemOverview(parseDashboardQueryFlags(flags));
842
+ break;
843
+ }
844
+ case "get-estimate-performance": {
845
+ await getEstimatePerformance(parseDashboardQueryFlags(flags));
513
846
  break;
514
847
  }
515
848
  case "get-financial-overview": {
@@ -1,5 +1,5 @@
1
1
  import { BillStatus, BudgetStatus, ProjectStatus } from "@bb/db/enums";
2
- import { BudgetRole, Deals, ExtendedBillStatus, ExtendedErrorSeverity, ExtendedErrorStatus, ExtendedProjectStatus, TimeFrame, } from "@bb/utils/filter-enums";
2
+ import { BudgetRole, Deals, ExtendedBillStatus, ExtendedErrorSeverity, ExtendedErrorStatus, ExtendedProjectStatus, TimeFrame, } from "./filter-enums.js";
3
3
  const BUDGET_STATUS_VALUES = new Set(Object.values(BudgetStatus));
4
4
  const BILL_STATUS_VALUES = new Set(Object.values(BillStatus));
5
5
  const PROJECT_STATUS_VALUES = new Set(Object.values(ProjectStatus));
@@ -0,0 +1,238 @@
1
+ function requireObject(raw, label) {
2
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
3
+ throw new Error(`${label} must be a JSON object.`);
4
+ }
5
+ return raw;
6
+ }
7
+ function toDate(value, field) {
8
+ if (value instanceof Date && !Number.isNaN(value.getTime())) {
9
+ return value;
10
+ }
11
+ if (typeof value === "string") {
12
+ const d = new Date(value);
13
+ if (!Number.isNaN(d.getTime()))
14
+ return d;
15
+ }
16
+ throw new Error(`${field} must be an ISO date/datetime string.`);
17
+ }
18
+ function optionalString(v) {
19
+ if (v === undefined || v === null)
20
+ return undefined;
21
+ const s = String(v);
22
+ return s === "" ? undefined : s;
23
+ }
24
+ /** Required string fields: rejects null/undefined, whitespace-only, and avoids String(undefined) → "undefined" passing Zod .min(1). */
25
+ function requiredString(value, fieldLabel) {
26
+ if (value === undefined || value === null) {
27
+ throw new Error(`${fieldLabel} is required.`);
28
+ }
29
+ const s = typeof value === "string" ? value : String(value);
30
+ const t = s.trim();
31
+ if (t === "") {
32
+ throw new Error(`${fieldLabel} is required.`);
33
+ }
34
+ return t;
35
+ }
36
+ /** Matches budget.createBudget — dates may be ISO strings in JSON. */
37
+ export function parseCreateBudgetPayload(raw) {
38
+ const o = requireObject(raw, "create-budget payload");
39
+ return {
40
+ name: requiredString(o.name, "create-budget payload.name"),
41
+ date: toDate(o.date, "date"),
42
+ startTime: toDate(o.startTime, "startTime"),
43
+ endTime: toDate(o.endTime, "endTime"),
44
+ pax: requiredString(o.pax, "create-budget payload.pax"),
45
+ venue: requiredString(o.venue, "create-budget payload.venue"),
46
+ budget: requiredString(o.budget, "create-budget payload.budget"),
47
+ paymentTerm: requiredString(o.paymentTerm, "create-budget payload.paymentTerm"),
48
+ categoryId: requiredString(o.categoryId, "create-budget payload.categoryId"),
49
+ projectId: requiredString(o.projectId, "create-budget payload.projectId"),
50
+ insideSalesId: requiredString(o.insideSalesId, "create-budget payload.insideSalesId"),
51
+ businessDevelopmentId: requiredString(o.businessDevelopmentId, "create-budget payload.businessDevelopmentId"),
52
+ asanaTaskId: requiredString(o.asanaTaskId, "create-budget payload.asanaTaskId"),
53
+ sourceBudgetId: optionalString(o.sourceBudgetId),
54
+ pipedriveDealId: optionalString(o.pipedriveDealId),
55
+ };
56
+ }
57
+ /** Matches budget.updateBudget — same as create plus id. */
58
+ export function parseUpdateBudgetPayload(raw) {
59
+ const o = requireObject(raw, "update-budget payload");
60
+ const id = requiredString(o.id, "update-budget payload.id");
61
+ const base = parseCreateBudgetPayload(raw);
62
+ return { ...base, id };
63
+ }
64
+ export function parseUpdateBudgetCommissionPayload(raw) {
65
+ const o = requireObject(raw, "update-budget-commission payload");
66
+ return {
67
+ budgetId: requiredString(o.budgetId, "update-budget-commission payload.budgetId"),
68
+ name: requiredString(o.name, "update-budget-commission payload.name"),
69
+ percentage: requiredString(o.percentage, "update-budget-commission payload.percentage"),
70
+ totalSellingAfterDiscount: Number(o.totalSellingAfterDiscount),
71
+ };
72
+ }
73
+ export function parseBudgetDiscountPayload(raw) {
74
+ const o = requireObject(raw, "update-budget-discount payload");
75
+ const type = o.type;
76
+ if (type !== "percentage" && type !== "amount") {
77
+ throw new Error('update-budget-discount payload.type must be "percentage" or "amount".');
78
+ }
79
+ return {
80
+ budgetId: requiredString(o.budgetId, "update-budget-discount payload.budgetId"),
81
+ name: requiredString(o.name, "update-budget-discount payload.name"),
82
+ type,
83
+ value: requiredString(o.value, "update-budget-discount payload.value"),
84
+ totalSellingBeforeDiscount: Number(o.totalSellingBeforeDiscount),
85
+ };
86
+ }
87
+ const billAttachment = (x, i) => {
88
+ const label = `create-bill payload.attachments[${i}]`;
89
+ const a = requireObject(x, label);
90
+ return {
91
+ id: requiredString(a.id, `${label}.id`),
92
+ name: requiredString(a.name, `${label}.name`),
93
+ key: requiredString(a.key, `${label}.key`),
94
+ size: Number(a.size),
95
+ };
96
+ };
97
+ /** Matches bill.create — invoiceDate may be ISO string; attachments default to []. */
98
+ export function parseCreateBillPayload(raw) {
99
+ const o = requireObject(raw, "create-bill payload");
100
+ const isClaimable = o.isClaimable;
101
+ if (typeof isClaimable !== "boolean") {
102
+ throw new Error("create-bill payload.isClaimable must be boolean.");
103
+ }
104
+ const budgetItemIds = o.budgetItemIds;
105
+ if (!Array.isArray(budgetItemIds) || budgetItemIds.length === 0) {
106
+ throw new Error("create-bill payload.budgetItemIds must be a non-empty string array.");
107
+ }
108
+ const attachmentsRaw = o.attachments;
109
+ const attachments = Array.isArray(attachmentsRaw)
110
+ ? attachmentsRaw.map((x, i) => billAttachment(x, i))
111
+ : [];
112
+ let verificationResults;
113
+ const vr = o.verificationResults;
114
+ if (vr !== undefined && vr !== null) {
115
+ const v = requireObject(vr, "verificationResults");
116
+ verificationResults = {
117
+ status: v.status,
118
+ extractedAmount: v.extractedAmount === null || v.extractedAmount === undefined
119
+ ? null
120
+ : Number(v.extractedAmount),
121
+ difference: v.difference === null || v.difference === undefined
122
+ ? null
123
+ : Number(v.difference),
124
+ confidence: v.confidence === null || v.confidence === undefined
125
+ ? null
126
+ : Number(v.confidence),
127
+ };
128
+ }
129
+ const verificationStatus = o.verificationStatus;
130
+ let vs;
131
+ if (verificationStatus === "PENDING" ||
132
+ verificationStatus === "PASS" ||
133
+ verificationStatus === "FAIL" ||
134
+ verificationStatus === "MANUAL_REVIEW") {
135
+ vs = verificationStatus;
136
+ }
137
+ else if (verificationStatus !== undefined && verificationStatus !== null) {
138
+ throw new Error("create-bill payload.verificationStatus must be PENDING | PASS | FAIL | MANUAL_REVIEW.");
139
+ }
140
+ const amount = Number(o.amount);
141
+ if (!Number.isFinite(amount) || amount < 0) {
142
+ throw new Error("create-bill payload.amount must be a number >= 0.");
143
+ }
144
+ return {
145
+ budgetId: requiredString(o.budgetId, "create-bill payload.budgetId"),
146
+ projectId: requiredString(o.projectId, "create-bill payload.projectId"),
147
+ supplierId: requiredString(o.supplierId, "create-bill payload.supplierId"),
148
+ amount,
149
+ budgetItemIds: budgetItemIds.map((id, i) => requiredString(id, `create-bill payload.budgetItemIds[${i}]`)),
150
+ comment: optionalString(o.comment),
151
+ isClaimable,
152
+ attachments,
153
+ extractedSupplierName: optionalString(o.extractedSupplierName),
154
+ extractedAmount: o.extractedAmount === undefined || o.extractedAmount === null
155
+ ? undefined
156
+ : Number(o.extractedAmount),
157
+ invoiceNumber: o.invoiceNumber === undefined || o.invoiceNumber === null
158
+ ? ""
159
+ : typeof o.invoiceNumber === "string"
160
+ ? o.invoiceNumber
161
+ : String(o.invoiceNumber),
162
+ invoiceDate: o.invoiceDate === undefined || o.invoiceDate === null
163
+ ? undefined
164
+ : toDate(o.invoiceDate, "invoiceDate"),
165
+ verificationStatus: vs,
166
+ verificationResults,
167
+ };
168
+ }
169
+ /** Matches bill.update */
170
+ export function parseUpdateBillPayload(raw) {
171
+ const o = requireObject(raw, "update-bill payload");
172
+ return {
173
+ ...parseCreateBillPayload(raw),
174
+ id: requiredString(o.id, "update-bill payload.id"),
175
+ };
176
+ }
177
+ /** Matches project.updateProject — dateRange.from/to may be ISO strings. */
178
+ export function parseUpdateProjectPayload(raw) {
179
+ const o = requireObject(raw, "update-project payload");
180
+ const dr = o.dateRange;
181
+ if (dr === null || typeof dr !== "object" || Array.isArray(dr)) {
182
+ throw new Error("update-project payload.dateRange must be an object.");
183
+ }
184
+ const range = dr;
185
+ const notification = o.requestQboAccountantNotification;
186
+ let requestQboAccountantNotification;
187
+ if (typeof notification === "boolean") {
188
+ requestQboAccountantNotification = notification;
189
+ }
190
+ else if (notification !== undefined && notification !== null) {
191
+ throw new Error("update-project payload.requestQboAccountantNotification must be a boolean when set.");
192
+ }
193
+ return {
194
+ id: requiredString(o.id, "update-project payload.id"),
195
+ name: requiredString(o.name, "update-project payload.name"),
196
+ description: optionalString(o.description),
197
+ companyId: requiredString(o.companyId, "update-project payload.companyId"),
198
+ contactPersonId: requiredString(o.contactPersonId, "update-project payload.contactPersonId"),
199
+ dateRange: {
200
+ from: toDate(range.from, "dateRange.from"),
201
+ to: range.to === undefined || range.to === null
202
+ ? undefined
203
+ : toDate(range.to, "dateRange.to"),
204
+ },
205
+ ...(requestQboAccountantNotification !== undefined && {
206
+ requestQboAccountantNotification,
207
+ }),
208
+ };
209
+ }
210
+ /** Server validates with Zod; CLI only checks top-level shape is object. */
211
+ export function parseSupplierCreatePayload(raw) {
212
+ requireObject(raw, "create-supplier payload");
213
+ return raw;
214
+ }
215
+ export function parseSupplierUpdatePayload(raw) {
216
+ requireObject(raw, "update-supplier payload");
217
+ return raw;
218
+ }
219
+ export function parseItemCreatePayload(raw) {
220
+ requireObject(raw, "create-item payload");
221
+ return raw;
222
+ }
223
+ export function parseItemUpdatePayload(raw) {
224
+ requireObject(raw, "update-item payload");
225
+ return raw;
226
+ }
227
+ export function parseContactCreatePayload(raw) {
228
+ requireObject(raw, "create-contact-person payload");
229
+ return raw;
230
+ }
231
+ export function parseContactUpdatePayload(raw) {
232
+ requireObject(raw, "update-contact-person payload");
233
+ return raw;
234
+ }
235
+ export function parseCompanyUpdatePayload(raw) {
236
+ requireObject(raw, "update-company payload");
237
+ return raw;
238
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@go-labs-sg/bb",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Budget Builder CLI — list budgets, bills, approvals, suppliers; approve bills; change status. For AI agents (e.g. Chuck/OpenClaw).",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -21,7 +21,7 @@
21
21
  "postpack": "node scripts/restore-package-json.js"
22
22
  },
23
23
  "dependencies": {
24
- "@trpc/client": "^11.15.0",
24
+ "@trpc/client": "^11.16.0",
25
25
  "dotenv": "^17.2.4",
26
26
  "superjson": "^2.2.6"
27
27
  },