@go-labs-sg/bb 1.6.0 → 1.7.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,6 +1,10 @@
1
1
  # bb
2
2
 
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.
3
+ Command-line interface for **Budget Builder**. Call budgets, bills, claims, 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
+
5
+ For AI agents: bills and claims are stored as the same underlying bill records. Use the `isClaimable` column/flag to distinguish them: `isClaimable=false` means a normal bill, while `isClaimable=true` means a reimbursable claim.
6
+
7
+ For AI agents using this CLI: always check whether a newer `@go-labs-sg/bb` version is available before running Budget Builder commands, then update the CLI if needed so command flags and API behavior stay current.
4
8
 
5
9
  **Registry:** [`@go-labs-sg/bb`](https://www.npmjs.com/package/@go-labs-sg/bb)
6
10
 
@@ -39,7 +43,7 @@ The CLI talks to the production API: `https://budget-builder.getout.events`.
39
43
  bb help
40
44
  bb list-budgets
41
45
  bb get-budget <budget-id>
42
- bb update-budget-status <budget-id> <status>
46
+ bb update-budget-status <budget-id> <status> [--markProjectWon true]
43
47
  bb list-bills
44
48
  bb approve-bill <bill-id>
45
49
  bb list-approvals
@@ -56,12 +60,12 @@ Global options and flags use `--key=value` or `--key value` (see `bb help`).
56
60
 
57
61
  | Area | Commands (non-exhaustive) |
58
62
  | --- | --- |
59
- | **Budgets** | `list-budgets` (full payload by default; `--summary` or `--includeDetails false` for slim list), `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` (also sends approval request emails), `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), `upload-budget-attachment` (`<budgetId>` + local file path; uses `attachment.requestBudgetAttachmentUpload` + PUT + `attachment.confirmBudgetAttachment`) |
60
- | **Bills** | `list-bills`, `list-claims`, `create-bill` (`--payload`), `update-bill` (`--payload`), `delete-bill`, `create-bill-approval` (also sends approval request emails), `update-bill-status`, `patch-bill-payment` (PAID bills: `--paymentTrackingUrl`, `--paymentReference`, `--quickbooksBillId`, `--paymentDate` ISO; clear with `--clearPaymentTrackingUrl` / `--clearPaymentReference` / `--clearQuickbooksBillId` / `--clearPaymentDate` `true`), `patch-bill-invoice-number`, `get-bill-attachments`, `upload-bill-attachment` (`<billId>` + local path), `get-bill-details` |
63
+ | **Budgets** | `list-budgets` (full payload by default; `--summary` or `--includeDetails false` for slim list), `get-budget`, `get-budget-items`, `get-budget-details`, `get-budget-categories`, `get-budget-versions`, `update-budget-status` (`--markProjectWon true` can move the parent project to `WON` for accepted/closed estimates), `mark-budget-won` (`<budgetId>` + proof file path; optional `--markProjectWon true`), `create-budget` / `update-budget` (`--payload`), `delete-budget`, `create-budget-approval` (also sends approval request emails), `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), `upload-budget-attachment` (`<budgetId>` + local file path; uses `attachment.requestBudgetAttachmentUpload` + PUT + `attachment.confirmBudgetAttachment`) |
64
+ | **Bills / claims** | `list-bills` (`--isClaimable false` for bills, `--isClaimable true` for claims, omit for both), `list-claims` (claims only), `create-bill` (`--payload`; set `isClaimable=false` for a bill or `isClaimable=true` for a claim), `update-bill` (`--payload`), `delete-bill`, `create-bill-approval` (also sends approval request emails), `update-bill-status`, `patch-bill-payment` (PAID bills: `--paymentTrackingUrl`, `--paymentReference`, `--quickbooksBillId`, `--paymentDate` ISO; clear with `--clearPaymentTrackingUrl` / `--clearPaymentReference` / `--clearQuickbooksBillId` / `--clearPaymentDate` `true`), `patch-bill-invoice-number`, `get-bill-attachments`, `upload-bill-attachment` (`<billId>` + local path), `get-bill-details` |
61
65
  | **Approvals** | `list-approvals` / `get-pending-approvals`, `approve-bill` / `reject-bill` (send reply email), `approve-budget` / `reject-budget` (send reply email), `approve-supplier` / `reject-supplier` (send reply email) |
62
- | **Companies & projects** | `list-companies`, `get-company`, `create-company`, `update-company` (`--payload`), `delete-company`, `list-projects`, `get-project`, `create-project` (required: `--name`, `--companyId`, `--contactPersonId`, `--insideSalesId`, `--businessDevelopmentId`, `--venue`, `--pax`, `--asanaTaskId`, `--slackChannelId`, `--slackChannelUrl`, `--slackChannelName`, `--startDate` as ISO datetime for project/window start; optional `--endDate`; optional `--description`; optional `--requestQboAccountantNotification false` to skip QBO accountant emails), `update-project` (`--payload` with `dateRange.from` / `dateRange.to` for the project window; optional `requestQboAccountantNotification` in JSON), `delete-project`, `update-project-status` (`<id>` `<status>`: `PITCH` \| `WON` \| `COMPLETED` \| `LOST`; for `PITCH` → `WON` also pass `--projectManagerId` or `--projectManagerName`) |
66
+ | **Companies & projects** | `list-companies`, `get-company`, `create-company`, `update-company` (`--payload`), `delete-company`, `list-projects`, `get-project`, `create-project` (required: `--name`, `--companyId`, `--contactPersonId`, `--insideSalesId`, `--businessDevelopmentId`, `--venue`, `--asanaTaskId`, `--slackChannelId`, `--slackChannelUrl`, `--slackChannelName`, `--startDate` as ISO datetime for project/window start; optional `--pax`; optional `--endDate`; optional `--description`; optional `--requestQboAccountantNotification false` to skip QBO accountant emails), `update-project` (`--payload` with `dateRange.from` / `dateRange.to` for the project window; optional `requestQboAccountantNotification` in JSON), `delete-project`, `update-project-status` (`<id>` `<status>`: `PITCH` \| `WON` \| `COMPLETED` \| `LOST`; for `PITCH` → `WON` also pass `--projectManagerId` or `--projectManagerName`; when marking `WON` without an accepted/closed budget or proof, pass `--wonOverrideReason`) |
63
67
  | **Contacts** | `list-contacts`, `create-contact-person` (`--payload`), `update-contact-person` (`--payload`) |
64
- | **Suppliers & items** | `list-suppliers`, `create-supplier` (`--payload`; when supplier status is `PENDING_APPROVAL`, also runs `supplier.createSupplierApproval` and `email.sendSupplierApprovalRequestEmail`), `update-supplier` (`--payload`; when supplier status is `PENDING_APPROVAL`, also runs `supplier.createSupplierApproval` and `email.sendSupplierApprovalRequestEmail`), `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) |
68
+ | **Suppliers & items** | `list-suppliers` (defaults to active suppliers, `--perPage 10`, sorted by `createdAt` desc; supports `--name`, `--sortBy` for scalar supplier fields, `--sortDir`, `--createdBy`, `--gstRegistered`, `--status`, `--supplierTags`, `--active false` for archived suppliers), `create-supplier` (`--payload`; when supplier status is `PENDING_APPROVAL`, also runs `supplier.createSupplierApproval` and `email.sendSupplierApprovalRequestEmail`), `update-supplier` (`--payload`; when supplier status is `PENDING_APPROVAL`, also runs `supplier.createSupplierApproval` and `email.sendSupplierApprovalRequestEmail`), `delete-suppliers` (`--ids` CSV; admin; archives/deactivates related items), `reactivate-suppliers` (`--ids` CSV; admin; reactivates related items), `create-certification` / `create-payment-method` / `create-supplier-role` / `create-supplier-tag` (`--name`), `get-supplier-details` (includes `supplierApprovalSummary` for pending approvers, superseded approvers, and the actual responder/respondedAt metadata), `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
69
  | **Dashboard & users** | `list-users`, `get-user-performance`, `get-dashboard`, `get-monthly-metrics`, `get-system-overview`, `get-estimate-performance`, `get-financial-overview` |
66
70
  | **Errors** | `get-recent-errors`, `get-error-metrics` |
67
71
  | **Historical / benchmarks** | `get-approved-budgets`, `get-budget-category-benchmarks`, `get-item-pricing-history`, `get-supplier-pricing-history` |
package/dist/commands.js CHANGED
@@ -48,13 +48,13 @@ export async function getBudget(id) {
48
48
  ]);
49
49
  out({ budget, items });
50
50
  }
51
- export async function updateBudgetStatus(budgetId, status) {
52
- if (status === "ESTIMATE_ACCEPTED") {
53
- throw new Error("Use mark-budget-won <budgetId> <filePath> so proof upload is included before setting ESTIMATE_ACCEPTED.");
54
- }
51
+ export async function updateBudgetStatus(budgetId, status, opts) {
55
52
  const result = await api.budget.updateBudgetStatus.mutate({
56
53
  id: budgetId,
57
54
  status,
55
+ ...(opts?.markProjectWon !== undefined && {
56
+ markProjectWon: opts.markProjectWon,
57
+ }),
58
58
  });
59
59
  out(result);
60
60
  }
@@ -110,7 +110,7 @@ const runBudgetPreflight = async (budgetId, preflight) => {
110
110
  insideSalesId: project.insideSalesId ?? "",
111
111
  businessDevelopmentId: project.businessDevelopmentId ?? "",
112
112
  venue: project.venue ?? "",
113
- pax: String(project.pax),
113
+ pax: String(project.pax ?? ""),
114
114
  ...(project.slackChannelId &&
115
115
  project.slackChannelUrl &&
116
116
  project.slackChannelName
@@ -217,8 +217,14 @@ export async function listApprovals(opts) {
217
217
  export async function listSuppliers(opts) {
218
218
  const result = await api.supplier.getSuppliers.query({
219
219
  page: opts.page ?? 1,
220
- perPage: opts.perPage ?? 20,
220
+ perPage: opts.perPage ?? 10,
221
+ sort: opts.sort ?? [{ id: "createdAt", desc: true }],
221
222
  name: opts.name,
223
+ createdBy: opts.createdBy,
224
+ gstRegistered: opts.gstRegistered ?? null,
225
+ status: opts.status,
226
+ supplierTags: opts.supplierTags,
227
+ active: opts.active ?? true,
222
228
  });
223
229
  out(result);
224
230
  }
@@ -338,7 +344,7 @@ export async function uploadBudgetAttachmentFromPath(budgetId, filePath) {
338
344
  });
339
345
  out(confirmed);
340
346
  }
341
- export async function uploadBudgetWinProofFromPath(budgetId, filePath) {
347
+ export async function uploadBudgetWinProofFromPath(budgetId, filePath, opts) {
342
348
  const buf = await readFile(filePath);
343
349
  const fileName = basename(filePath);
344
350
  const size = buf.byteLength;
@@ -364,11 +370,14 @@ export async function uploadBudgetWinProofFromPath(budgetId, filePath) {
364
370
  const result = await api.budget.updateBudgetStatus.mutate({
365
371
  id: budgetId,
366
372
  status: "ESTIMATE_ACCEPTED",
373
+ ...(opts?.markProjectWon !== undefined && {
374
+ markProjectWon: opts.markProjectWon,
375
+ }),
367
376
  });
368
377
  out(result);
369
378
  }
370
- export async function markBudgetWonWithProof(budgetId, filePath) {
371
- await uploadBudgetWinProofFromPath(budgetId, filePath);
379
+ export async function markBudgetWonWithProof(budgetId, filePath, opts) {
380
+ await uploadBudgetWinProofFromPath(budgetId, filePath, opts);
372
381
  }
373
382
  export async function getBillDetails(id) {
374
383
  const result = await api.bill.getById.query({ id });
@@ -508,6 +517,10 @@ export async function deleteSuppliersByIds(ids) {
508
517
  const result = await api.supplier.deleteSuppliers.mutate({ ids });
509
518
  out(result);
510
519
  }
520
+ export async function reactivateSuppliersByIds(ids) {
521
+ const result = await api.supplier.reactivateSuppliers.mutate({ ids });
522
+ out(result);
523
+ }
511
524
  export async function createSupplierCertification(name) {
512
525
  const result = await api.supplier.createCertification.mutate({ name });
513
526
  out(result);
@@ -742,7 +755,62 @@ export async function getSupplierDetails(supplierId) {
742
755
  const supplier = await api.supplier.getSupplierById.query({
743
756
  id: supplierId,
744
757
  });
745
- out(supplier);
758
+ const respondedStatuses = new Set([
759
+ ExtendedApprovalStatus.APPROVED,
760
+ ExtendedApprovalStatus.REJECTED,
761
+ ]);
762
+ const latestRespondedApproval = [
763
+ ...supplier.approvals.filter((approval) => respondedStatuses.has(approval.status)),
764
+ ].sort((a, b) => new Date(b.respondedAt ?? b.updatedAt).getTime() -
765
+ new Date(a.respondedAt ?? a.updatedAt).getTime() ||
766
+ new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())[0];
767
+ const pendingApprovals = supplier.approvals.filter((approval) => approval.status === ExtendedApprovalStatus.PENDING_APPROVAL);
768
+ const supersededApprovals = supplier.approvals.filter((approval) => approval.status === ExtendedApprovalStatus.SUPERSEDED);
769
+ const respondedByApproval = latestRespondedApproval ?? null;
770
+ const responseUser = respondedByApproval
771
+ ? (respondedByApproval.responder ?? respondedByApproval.approver)
772
+ : null;
773
+ out({
774
+ ...supplier,
775
+ supplierApprovalSummary: {
776
+ pendingApprovalCount: pendingApprovals.length,
777
+ pendingApprovers: pendingApprovals.map((approval) => ({
778
+ approvalId: approval.id,
779
+ approverId: approval.approverId,
780
+ name: approval.approver.name,
781
+ email: approval.approver.email,
782
+ createdAt: approval.createdAt,
783
+ updatedAt: approval.updatedAt,
784
+ })),
785
+ supersededApprovalCount: supersededApprovals.length,
786
+ supersededApprovers: supersededApprovals.map((approval) => ({
787
+ approvalId: approval.id,
788
+ approverId: approval.approverId,
789
+ name: approval.approver.name,
790
+ email: approval.approver.email,
791
+ respondedAt: approval.respondedAt,
792
+ responder: approval.responder
793
+ ? {
794
+ id: approval.responder.id,
795
+ name: approval.responder.name,
796
+ email: approval.responder.email,
797
+ }
798
+ : null,
799
+ })),
800
+ respondedBy: respondedByApproval && responseUser
801
+ ? {
802
+ approvalId: respondedByApproval.id,
803
+ status: respondedByApproval.status,
804
+ approverId: respondedByApproval.approverId,
805
+ responderId: respondedByApproval.responderId,
806
+ name: responseUser.name,
807
+ email: responseUser.email,
808
+ respondedAt: respondedByApproval.respondedAt,
809
+ updatedAt: respondedByApproval.updatedAt,
810
+ }
811
+ : null,
812
+ },
813
+ });
746
814
  }
747
815
  export async function approveSupplier(supplierId) {
748
816
  const notifications = await api.budget.getNotifications.query();
@@ -910,6 +978,9 @@ export async function updateProjectStatus(id, status, opts) {
910
978
  ...(opts?.projectManagerName && {
911
979
  projectManagerName: opts.projectManagerName,
912
980
  }),
981
+ ...(opts?.wonOverrideReason && {
982
+ wonOverrideReason: opts.wonOverrideReason,
983
+ }),
913
984
  });
914
985
  out(result);
915
986
  }
package/dist/index.js CHANGED
@@ -2,9 +2,9 @@
2
2
  import "./load-env.js";
3
3
  import { requireApiKey } from "./api-client.js";
4
4
  import { consumeCliQuietFlags, logCliAction, sanitizeFlagsForTrace, setCliQuiet, shouldLogCliActions, } from "./cli-trace.js";
5
- import { addBudgetItems, approveBill, approveBudget, approveSupplier, createBillApproval, createBillFromPayload, createBudgetApproval, createBudgetCategory, createBudgetFromPayload, createCompany, createContactPersonFromPayload, createEstimate, 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, markBudgetWonWithProof, patchBillInvoiceNumber, patchBillPayment, rejectBill, rejectBudget, rejectSupplier, removeBudgetItem, reorderBudgetItemsCli, updateBillFromPayload, updateBillStatus, updateBudgetCategory, updateBudgetCommissionFromPayload, updateBudgetDiscountFromPayload, updateBudgetFromPayload, updateBudgetItem, updateBudgetItemSupplierCli, updateBudgetStatus, updateCompanyFromPayload, updateContactPersonFromPayload, updateItemCategory, updateItemFromPayload, updateProjectFromPayload, updateProjectStatus, updateSupplierFromPayload, uploadBillAttachmentFromPath, uploadBudgetAttachmentFromPath, } from "./commands.js";
5
+ import { addBudgetItems, approveBill, approveBudget, approveSupplier, createBillApproval, createBillFromPayload, createBudgetApproval, createBudgetCategory, createBudgetFromPayload, createCompany, createContactPersonFromPayload, createEstimate, 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, markBudgetWonWithProof, patchBillInvoiceNumber, patchBillPayment, reactivateSuppliersByIds, rejectBill, rejectBudget, rejectSupplier, removeBudgetItem, reorderBudgetItemsCli, updateBillFromPayload, updateBillStatus, updateBudgetCategory, updateBudgetCommissionFromPayload, updateBudgetDiscountFromPayload, updateBudgetFromPayload, updateBudgetItem, updateBudgetItemSupplierCli, updateBudgetStatus, updateCompanyFromPayload, updateContactPersonFromPayload, updateItemCategory, updateItemFromPayload, updateProjectFromPayload, updateProjectStatus, updateSupplierFromPayload, uploadBillAttachmentFromPath, uploadBudgetAttachmentFromPath, } from "./commands.js";
6
6
  import { getFlag, parseArgs } from "./parse-args.js";
7
- 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
+ import { billStatusesForUpdateHelp, budgetStatusesForHelp, isBudgetStatusUpdate, parseApprovalTypeFlag, parseBillStatusForUpdate, parseCommaSeparatedBillStatuses, parseCommaSeparatedBudgetStatuses, parseCommaSeparatedIds, parseCommaSeparatedSupplierStatuses, parseOptionalBillListSortBy, parseOptionalBillListSortDir, parseOptionalDashboardRole, parseOptionalDeals, parseOptionalErrorSeverity, parseOptionalErrorStatus, parseOptionalExtendedProjectStatus, parseOptionalFinancialRole, parseOptionalSupplierAnalyticsTimeFrame, parseOptionalTimeFrame, parseProjectStatusForUpdate, projectStatusesForHelp, } from "./parse-cli-enums.js";
8
8
  import { parseJsonFlag, parseOptionalNumber as parseOptNum, } from "./parse-json-flag.js";
9
9
  function parsePositiveIntFlag(value, flagName) {
10
10
  if (value === undefined)
@@ -45,6 +45,45 @@ function parseListBillsFlags(flags) {
45
45
  pageSize: parsePositiveIntFlag(getFlag(flags, "pageSize"), "--pageSize"),
46
46
  };
47
47
  }
48
+ const SUPPLIER_SORT_FIELDS = [
49
+ "createdAt",
50
+ "updatedAt",
51
+ "name",
52
+ "email",
53
+ "phone",
54
+ "gstRegistered",
55
+ "status",
56
+ "active",
57
+ ];
58
+ function parseOptionalSupplierSortBy(raw) {
59
+ if (raw === undefined || raw === "")
60
+ return undefined;
61
+ if (!SUPPLIER_SORT_FIELDS.includes(raw)) {
62
+ throw new Error(`Invalid --sortBy "${raw}". Use one of: ${SUPPLIER_SORT_FIELDS.join(", ")}.`);
63
+ }
64
+ return raw;
65
+ }
66
+ function parseListSuppliersFlags(flags) {
67
+ const createdByRaw = getFlag(flags, "createdBy");
68
+ const statusRaw = getFlag(flags, "status");
69
+ const supplierTagsRaw = getFlag(flags, "supplierTags");
70
+ const sortBy = parseOptionalSupplierSortBy(getFlag(flags, "sortBy"));
71
+ const sortDirRaw = getFlag(flags, "sortDir");
72
+ const sortDir = parseOptionalBillListSortDir(sortDirRaw !== undefined ? String(sortDirRaw) : undefined);
73
+ return {
74
+ name: getFlag(flags, "name"),
75
+ page: parsePositiveIntFlag(getFlag(flags, "page"), "--page"),
76
+ perPage: parsePositiveIntFlag(getFlag(flags, "perPage"), "--perPage"),
77
+ sort: sortBy !== undefined || sortDir !== undefined
78
+ ? [{ id: sortBy ?? "createdAt", desc: (sortDir ?? "desc") === "desc" }]
79
+ : undefined,
80
+ createdBy: parseCommaSeparatedIds(createdByRaw !== undefined ? String(createdByRaw) : undefined),
81
+ gstRegistered: parseOptionalBoolFlag(flags, "gstRegistered"),
82
+ status: parseCommaSeparatedSupplierStatuses(statusRaw !== undefined ? String(statusRaw) : undefined),
83
+ supplierTags: parseCommaSeparatedIds(supplierTagsRaw !== undefined ? String(supplierTagsRaw) : undefined),
84
+ active: parseOptionalBoolFlag(flags, "active"),
85
+ };
86
+ }
48
87
  function parseDashboardQueryFlags(flags) {
49
88
  const roleRaw = getFlag(flags, "role");
50
89
  const timeFrameRaw = getFlag(flags, "timeFrame");
@@ -59,7 +98,7 @@ function parseDashboardQueryFlags(flags) {
59
98
  }
60
99
  function printHelp() {
61
100
  const help = `
62
- bb — Budget Builder CLI (parity with MCP tools)
101
+ bb — Budget Builder CLI for AI agents (parity with MCP tools)
63
102
 
64
103
  Usage: bb <command> [options] [args]
65
104
 
@@ -74,9 +113,9 @@ Budgets
74
113
  get-budget-details <id> Full detail (margins, suppliers, etc.)
75
114
  get-budget-categories
76
115
  get-budget-versions <budgetId>
77
- update-budget-status <id> <status>
116
+ update-budget-status <id> <status> [--markProjectWon true|false]
78
117
  status: ${budgetStatusesForHelp.join(", ")}
79
- mark-budget-won <budgetId> <filePath> Upload signed quote/PO proof and set status to ESTIMATE_ACCEPTED
118
+ mark-budget-won <budgetId> <filePath> [--markProjectWon true|false] Upload signed quote/PO proof and set status to ESTIMATE_ACCEPTED
80
119
  create-budget --payload '<json>' (budget.createBudget; Asana deal card is on the project)
81
120
  update-budget --payload '<json>' (budget.updateBudget; must include id)
82
121
  delete-budget <budgetId>
@@ -97,8 +136,9 @@ Budgets
97
136
  Bills
98
137
  list-bills [--projectId] [--budgetId] [--status CSV] [--search <text>] [--isClaimable true|false] [--createdByIds <csv>] [--sortBy createdAt|amount|status] [--sortDir asc|desc] [--page] [--pageSize]
99
138
  list-claims same flags as list-bills; only reimbursable claims (ignores --isClaimable)
139
+ isClaimable differentiates the shared bill/claim records: false = bill, true = claim.
100
140
  create-bill-approval <billId> (also sends approval request emails)
101
- create-bill --payload '<json>' (bill.create; use upload-bill-attachment after create to add files)
141
+ create-bill --payload '<json>' (bill.create; payload.isClaimable false = bill, true = claim; use upload-bill-attachment after create to add files)
102
142
  update-bill --payload '<json>' (bill.update; must include id)
103
143
  delete-bill <billId>
104
144
  update-bill-status <id> <status> [--rejectionReason] [--paymentTrackingUrl] [--paymentReference]
@@ -127,11 +167,12 @@ Companies & projects
127
167
  delete-company <id>
128
168
  list-projects [--companyId] [--name] [--status] [--page] [--perPage]
129
169
  get-project <id>
130
- create-project --name --companyId --contactPersonId --insideSalesId --businessDevelopmentId --venue --pax --asanaTaskId --slackChannelId --slackChannelUrl --slackChannelName --startDate <ISO> [--endDate <ISO>] [--description] [--requestQboAccountantNotification false]
170
+ create-project --name --companyId --contactPersonId --insideSalesId --businessDevelopmentId --venue --asanaTaskId --slackChannelId --slackChannelUrl --slackChannelName --startDate <ISO> [--pax] [--endDate <ISO>] [--description] [--requestQboAccountantNotification false]
131
171
  update-project --payload '<json>' (project.updateProject; optional requestQboAccountantNotification; default notify like web)
132
172
  delete-project <id>
133
- update-project-status <id> <status> [--projectManagerId <id>] [--projectManagerName <name>]
173
+ update-project-status <id> <status> [--projectManagerId <id>] [--projectManagerName <name>] [--wonOverrideReason <text>]
134
174
  Required when moving PITCH → WON: supply --projectManagerId or --projectManagerName.
175
+ Required when moving to WON without accepted/closed budget or win proof: --wonOverrideReason (10+ chars).
135
176
  status: ${projectStatusesForHelp.join(", ")}
136
177
 
137
178
  Contacts
@@ -140,10 +181,12 @@ Contacts
140
181
  update-contact-person --payload '<json>' (contactPerson.updateContactPerson; must include id)
141
182
 
142
183
  Suppliers & items
143
- list-suppliers [--name] [--page] [--perPage]
184
+ list-suppliers [--name] [--page] [--perPage] [--sortBy createdAt|updatedAt|name|email|phone|gstRegistered|status|active] [--sortDir asc|desc] [--createdBy <csv>] [--gstRegistered true|false] [--status CSV] [--supplierTags <csv>] [--active true|false]
185
+ Defaults: --active true, --perPage 10, --sortBy createdAt, --sortDir desc (same as the web list); pass --active false for archived suppliers.
144
186
  create-supplier --payload '<json>' (supplier.createSupplier; auto-requests approval + emails when status is PENDING_APPROVAL)
145
187
  update-supplier --payload '<json>' (supplier.updateSupplier; must include id; auto-requests approval + emails when status is PENDING_APPROVAL)
146
- delete-suppliers --ids <csv> (supplier.deleteSuppliers; admin)
188
+ delete-suppliers --ids <csv> (supplier.deleteSuppliers; admin; archives/deactivates supplier items)
189
+ reactivate-suppliers --ids <csv> (supplier.reactivateSuppliers; admin; reactivates supplier items)
147
190
  create-certification --name (supplier.createCertification)
148
191
  create-payment-method --name (supplier.createPaymentMethod)
149
192
  create-supplier-role --name (supplier.createSupplierRole)
@@ -256,7 +299,9 @@ async function main() {
256
299
  if (!budgetId || !filePath) {
257
300
  throw new Error("mark-budget-won requires <budgetId> <filePath>");
258
301
  }
259
- await markBudgetWonWithProof(budgetId, filePath);
302
+ await markBudgetWonWithProof(budgetId, filePath, {
303
+ markProjectWon: parseOptionalBoolFlag(flags, "markProjectWon"),
304
+ });
260
305
  break;
261
306
  }
262
307
  case "get-budget-items": {
@@ -292,7 +337,9 @@ async function main() {
292
337
  if (!isBudgetStatusUpdate(status)) {
293
338
  throw new Error(`Invalid status. Use one of: ${budgetStatusesForHelp.join(", ")}`);
294
339
  }
295
- await updateBudgetStatus(id, status);
340
+ await updateBudgetStatus(id, status, {
341
+ markProjectWon: parseOptionalBoolFlag(flags, "markProjectWon"),
342
+ });
296
343
  break;
297
344
  }
298
345
  case "create-budget-approval": {
@@ -750,13 +797,12 @@ async function main() {
750
797
  !insideSalesId ||
751
798
  !businessDevelopmentId ||
752
799
  !venue ||
753
- !pax ||
754
800
  !asanaTaskId ||
755
801
  !slackChannelId ||
756
802
  !slackChannelUrl ||
757
803
  !slackChannelName ||
758
804
  !startDate) {
759
- throw new Error("create-project requires --name --companyId --contactPersonId --insideSalesId --businessDevelopmentId --venue --pax --asanaTaskId --slackChannelId --slackChannelUrl --slackChannelName --startDate (ISO/datetime flags as appropriate); optional --endDate");
805
+ throw new Error("create-project requires --name --companyId --contactPersonId --insideSalesId --businessDevelopmentId --venue --asanaTaskId --slackChannelId --slackChannelUrl --slackChannelName --startDate (ISO/datetime flags as appropriate); optional --pax and --endDate");
760
806
  }
761
807
  await createProject({
762
808
  name,
@@ -800,9 +846,13 @@ async function main() {
800
846
  }
801
847
  const pmIdFlag = getFlag(flags, "projectManagerId");
802
848
  const pmNameFlag = getFlag(flags, "projectManagerName");
849
+ const wonOverrideReasonFlag = getFlag(flags, "wonOverrideReason");
803
850
  await updateProjectStatus(id, parseProjectStatusForUpdate(statusRaw), {
804
851
  ...(pmIdFlag && { projectManagerId: String(pmIdFlag) }),
805
852
  ...(pmNameFlag && { projectManagerName: String(pmNameFlag) }),
853
+ ...(wonOverrideReasonFlag && {
854
+ wonOverrideReason: String(wonOverrideReasonFlag),
855
+ }),
806
856
  });
807
857
  break;
808
858
  }
@@ -832,11 +882,7 @@ async function main() {
832
882
  break;
833
883
  }
834
884
  case "list-suppliers": {
835
- await listSuppliers({
836
- name: getFlag(flags, "name"),
837
- page: parsePositiveIntFlag(getFlag(flags, "page"), "--page"),
838
- perPage: parsePositiveIntFlag(getFlag(flags, "perPage"), "--perPage"),
839
- });
885
+ await listSuppliers(parseListSuppliersFlags(flags));
840
886
  break;
841
887
  }
842
888
  case "create-supplier": {
@@ -869,6 +915,18 @@ async function main() {
869
915
  await deleteSuppliersByIds(ids);
870
916
  break;
871
917
  }
918
+ case "reactivate-suppliers": {
919
+ const idsRaw = getFlag(flags, "ids");
920
+ if (idsRaw === undefined) {
921
+ throw new Error("reactivate-suppliers requires --ids <csv>");
922
+ }
923
+ const ids = parseCommaSeparatedIds(String(idsRaw));
924
+ if (!ids?.length) {
925
+ throw new Error("reactivate-suppliers requires non-empty --ids");
926
+ }
927
+ await reactivateSuppliersByIds(ids);
928
+ break;
929
+ }
872
930
  case "create-certification": {
873
931
  const name = getFlag(flags, "name");
874
932
  if (!name)
@@ -1,4 +1,4 @@
1
- import { BudgetRole, Deals, ExtendedBillStatus, ExtendedErrorSeverity, ExtendedErrorStatus, ExtendedProjectStatus, TimeFrame, } from "./filter-enums.js";
1
+ import { BudgetRole, Deals, ExtendedApprovalStatus, ExtendedBillStatus, ExtendedErrorSeverity, ExtendedErrorStatus, ExtendedProjectStatus, TimeFrame, } from "./filter-enums.js";
2
2
  import { BillStatus, BudgetStatus, ProjectStatus } from "./prisma-enums.js";
3
3
  const BUDGET_STATUS_VALUES = new Set(Object.values(BudgetStatus));
4
4
  const BILL_STATUS_VALUES = new Set(Object.values(BillStatus));
@@ -20,6 +20,7 @@ const SUPPLIER_ANALYTICS_TF = new Set([
20
20
  "LAST_3_MONTHS",
21
21
  "CUSTOM",
22
22
  ]);
23
+ const SUPPLIER_APPROVAL_STATUS_VALUES = new Set(Object.values(ExtendedApprovalStatus));
23
24
  const EXT_ERROR_SEVERITY_VALUES = new Set(Object.values(ExtendedErrorSeverity));
24
25
  const EXT_ERROR_STATUS_VALUES = new Set(Object.values(ExtendedErrorStatus));
25
26
  export function isBudgetStatusListToken(s) {
@@ -144,6 +145,24 @@ export function parseCommaSeparatedBudgetStatuses(raw) {
144
145
  }
145
146
  return out;
146
147
  }
148
+ export function parseCommaSeparatedSupplierStatuses(raw) {
149
+ if (raw === undefined)
150
+ return undefined;
151
+ const parts = raw
152
+ .split(",")
153
+ .map((x) => x.trim())
154
+ .filter(Boolean);
155
+ if (parts.length === 0)
156
+ return undefined;
157
+ const out = [];
158
+ for (const p of parts) {
159
+ if (!SUPPLIER_APPROVAL_STATUS_VALUES.has(p)) {
160
+ throw new Error(`Invalid supplier approval status "${p}". Use one of: ${[...SUPPLIER_APPROVAL_STATUS_VALUES].join(", ")}.`);
161
+ }
162
+ out.push(p);
163
+ }
164
+ return out;
165
+ }
147
166
  export function parseBillStatusFlag(raw) {
148
167
  if (raw === undefined)
149
168
  return undefined;
@@ -214,7 +214,7 @@ export function parseUpdateProjectPayload(raw) {
214
214
  insideSalesId: requiredString(o.insideSalesId, "update-project payload.insideSalesId"),
215
215
  businessDevelopmentId: requiredString(o.businessDevelopmentId, "update-project payload.businessDevelopmentId"),
216
216
  venue: requiredString(o.venue, "update-project payload.venue"),
217
- pax: requiredString(o.pax, "update-project payload.pax"),
217
+ pax: optionalString(o.pax),
218
218
  dateRange: {
219
219
  from: toDate(range.from, "dateRange.from"),
220
220
  to: range.to === undefined || range.to === null
@@ -27,6 +27,7 @@ export const ApprovalStatus = {
27
27
  PENDING_APPROVAL: "PENDING_APPROVAL",
28
28
  APPROVED: "APPROVED",
29
29
  REJECTED: "REJECTED",
30
+ SUPERSEDED: "SUPERSEDED",
30
31
  };
31
32
  export const ApprovalType = {
32
33
  BUDGET: "BUDGET",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@go-labs-sg/bb",
3
- "version": "1.6.0",
4
- "description": "Budget Builder CLI — list budgets, bills, approvals, suppliers; approve bills; change status. For AI agents (e.g. Chuck/OpenClaw).",
3
+ "version": "1.7.0",
4
+ "description": "Budget Builder CLI for AI agents manage budgets, bills, and claims; bill records use isClaimable=false for bills and isClaimable=true for claims.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "bin": {