@go-labs-sg/bb 2.16.0 → 2.18.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
@@ -183,17 +183,23 @@ external provider.
183
183
 
184
184
  ### Mutation safety
185
185
 
186
+ While an estimate email is `QUEUED`, `PROCESSING`, or awaiting provider
187
+ reconciliation, the API makes that Budget read-only. Budget-changing CLI
188
+ commands return a conflict until delivery becomes `SENT` or `FAILED`.
189
+
186
190
  Every canonical command is classified by effect: `state-change`, `email`, `external-write`, `delete`, and/or `financial-write`. Interactive runs require one exact `CONFIRM`. Non-interactive runs require the corresponding flags (`--allow-state-change`, `--allow-email`, `--allow-external-write`, `--allow-delete`, and `--allow-financial-write`); commands with multiple effects require every matching flag. Agents must still get user confirmation in chat first—the runtime gate is not user authorization.
187
191
 
188
192
  **Approval email exclusions:** Approval requests create pending database records for every eligible approver, including configured non-recipient admin accounts. Automated approval-request emails skip those accounts.
189
193
 
190
194
  **Locked estimate budget changes:** Legacy flat commands retain their additional interactive `yes` prompt when the current budget is locked. Canonical v2 commands use the single effect-aware confirmation gate; non-interactive execution requires `--allow-state-change` plus any other effects declared for that command.
191
195
 
192
- **Mutations with `--payload`:** Commands such as `bb budget create`, `bb bill create`, and `bb supplier update` 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. Plain `description` fields for item create/update are converted to `descriptionRichText`; pass `descriptionRichText` directly when formatted Tiptap JSON is required. For **`bb project update`**, the project window is `dateRange.from` and `dateRange.to` (optional end); there are no separate event-date fields on the project payload. **`bb budget create` / `bb budget update`** do not accept `asanaTaskId`; configure the deal card on the project (`bb project update` / project settings).
196
+ **Mutations with `--payload`:** Commands such as `bb budget create`, `bb bill create`, and `bb supplier update` 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. Plain `description` fields for item create/update are converted to `descriptionRichText`; pass `descriptionRichText` directly when formatted Tiptap JSON is required. For **`bb project update`**, the project window is `dateRange.from` and `dateRange.to` (optional end); there are no separate event-date fields on the project payload. **`bb budget create` / `bb budget update`** do not accept `asanaTaskId`; configure the deal card on the project (`bb project update` / project settings). When `bb budget create` clones a `sourceBudgetId` containing unavailable catalogue lines, the API preserves those snapshots for review and rejects the clone until the caller explicitly sets `unavailableItemReviewAcknowledged: true`.
197
+
198
+ **Unavailable catalogue lines:** Budget Builder-only `bb item archive --ids <csv>` may retire catalogue items referenced by open estimates. The estimate snapshots remain intact, but sending or accepting is blocked until each unavailable line is resolved with `bb budget item replace --budgetItemId <id> --replacementItemId <active-item-id>`, `bb budget item remove <id>`, or the Lead/Admin-only `bb budget item exception approve --budgetItemId <id> --reason <text>`. The approved-exception reason is audited. `bb item delete <id>` also attempts QuickBooks deactivation and therefore remains blocked while an open estimate references the item.
193
199
 
194
- **Contact-person estimate email:** `bb budget estimate send --payload '<json>'` calls the same `email.sendEstimateToContactPerson` procedure as the web composer. The payload requires `budgetId`, `estimateId`, optional `estimateDocNumber`, `to`, `cc`, `replyTo`, `subject`, HTML `content`, and HTML `signature`. It sends the raw PDF returned by QuickBooks together with the standard terms and Budget Builder budget attachments, then marks the budget `ESTIMATE_SENT`. The command requires interactive `CONFIRM`; inspect the budget, recipients, and HTML first, and do not retry blindly after an ambiguous delivery failure.
200
+ **Contact-person estimate email:** `bb budget estimate send --payload '<json>'` calls the same `email.sendEstimateToContactPerson` procedure as the web composer. The payload requires `budgetId`, `estimateId`, optional `estimateDocNumber`, `to`, `cc`, `replyTo`, `subject`, HTML `content`, and HTML `signature`. It durably queues worker delivery of the QuickBooks PDF, standard terms, and Budget Builder budget attachments, and returns the outbound-email and operation IDs immediately. Worker retries reuse one provider idempotency key; the budget becomes `ESTIMATE_SENT` only after Resend accepts the email. The command requires interactive `CONFIRM`; inspect the budget, recipients, and HTML first.
195
201
 
196
- **Customer-invoice workflow parity:** `bb customer-invoice list` uses the same global/project list procedure and metrics as the web pages; omit filters for the global list or use `--projectId` for project scope. `bb customer-invoice send` uses the same protected email workflow as the web composer and marks a successful invoice `SENT`. `bb customer-invoice approve` and `bb customer-invoice reject` are Admin-only batch operations: callers select a batch ID, and the API resolves its internal pending Admin approval record. `bb customer-invoice sync` follows QuickBooks' paid state and zero balance, restoring `SENT` or `APPROVED` if that payment is reversed. Invoice approval refuses voided invoices and closes the estimate only when approved invoice coverage totals 100%; deletion, voiding, rejection, expiry, and QBO synchronization use the same estimate-reopening and live-payment guards as the web app.
202
+ **Customer-invoice workflow parity:** `bb customer-invoice list` uses the same global/project list procedure and metrics as the web pages; omit filters for the global list or use `--projectId` for project scope. `bb customer-invoice send --payload '<json>'` uses the same protected email workflow as the web composer, sends to the payload's `to` address, saves that address as the project's configured billing email after a successful send, and marks the invoice `SENT`. `bb customer-invoice approve` and `bb customer-invoice reject` are Admin-only batch operations: callers select a batch ID, and the API resolves its internal pending Admin approval record. `bb customer-invoice sync` follows QuickBooks' paid state and zero balance, restoring `SENT` or `APPROVED` if that payment is reversed. Invoice approval refuses voided invoices and closes the estimate only when approved invoice coverage totals 100%; deletion, voiding, rejection, expiry, and QBO synchronization use the same estimate-reopening and live-payment guards as the web app.
197
203
 
198
204
  For agent-driven invoice work, use this read-before-write sequence:
199
205
 
@@ -207,11 +213,14 @@ For agent-driven invoice work, use this read-before-write sequence:
207
213
  Project Hub setup and commercial-document reconciliation are durable, asynchronous Budget Builder operations. Read status before requesting either mutation:
208
214
 
209
215
  ```bash
216
+ bb project hub list --q <project-or-company> --status WON,COMPLETED
210
217
  bb project hub status <project-id>
211
218
  bb project hub setup <project-id>
212
219
  bb project hub sync <project-id>
213
220
  ```
214
221
 
222
+ `list` is read-only and returns active projects with a Project Hub integration, including setup state and the stored Hub URL when available. It supports project/company search, project-status filters, pagination, and sorting.
223
+
215
224
  `status` reports eligibility, whether setup can be requested, the current setup status, the latest setup error, and the client Hub URL. `setup` uses the same eligibility and accepted-estimate checks as the web button and may create the operational Asana project before provisioning Drive and Project Hub. `sync` requires an existing `READY` native-layout integration and queues a complete reconciliation of accepted estimates, commercial attachments, win proofs, and eligible customer invoices into Asana and Drive.
216
225
 
217
226
  Both mutation commands write external state asynchronously and therefore require `state-change` and `external-write` confirmation. In non-interactive use, pass both `--allow-state-change` and `--allow-external-write` only after obtaining explicit user approval. A successful `queued` response means the durable operation was accepted. `status` tracks setup readiness; the sync response returns its outbox operation ID, which an Admin can inspect with the integration-operation commands when completion or failure diagnostics are required.
@@ -226,17 +235,17 @@ Project and budget query payloads keep the Asana identities explicit: `asanaTask
226
235
 
227
236
  | Area | Legacy handler labels (reference only; non-exhaustive) |
228
237
  | --- | --- |
229
- | **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`, `rename-budget-version`, `restore-budget-version`, `update-budget-status` (`ESTIMATE_ACCEPTED` requires win proof, auto-marks `PITCH` or `LOST` projects `WON`, and auto-creates an Asana Event Ops section from Prompt 5 tasks when enabled; Prompt 5 skips quantity-zero items, combines matching non-GO-internal suppliers, keeps each GO internal item separate, removes generated work that repeats another line item or group, and schedules applicable subtasks relative to the project start date; `--projectStatusOnCommercialRejection PITCH\|LOST` is required when rejecting the only accepted/closed budget on a commercial project), `mark-budget-won` (`<budgetId>` + proof file path; `PITCH` or `LOST` projects become `WON` automatically), `create-budget` / `update-budget` (`--payload`), `delete-budget`, `create-budget-approval` (also sends approval request emails), `create-estimate`, `send-estimate-to-contact-person` (same contact-person email workflow as web), `add-budget-items`, `update-budget-item`, `remove-budget-item`, `reorder-budget-items`, `update-budget-item-supplier`, `mark-budget-items-not-utilized`, `restore-budget-item`, `create-placeholder-bill` (admin recovery), `create-budget-category`, `update-budget-category`, `delete-budget-category`, `update-budget-commission`, `delete-budget-commission`, `update-budget-discount`, `delete-budget-discount` (`--payload` where noted), `upload-budget-attachment` (`<budgetId>` + local file path; uses `attachment.requestBudgetAttachmentUpload` + PUT + `attachment.confirmBudgetAttachment`) |
230
- | **Bills / claims** | `list-bills` (`--isClaimable false` for bills, `--isClaimable true` for claims, omit for both), `list-claims` (claims only), `validate-bill-selection` (`--payload`; runs the same supplier, line-item, and quotation-coverage checks as the web flow; `alreadyPaid` never bypasses them), `stage-bill-attachment` (securely uploads invoice/payment-proof files before creation and returns attachment JSON; ownership and one-hour expiry are enforced by a server-side staged-upload record rather than encoded in the object key), `cleanup-staged-bill-attachments`, `create-bill` (`--payload`; set `isClaimable=false` for a bill, `isClaimable=true` for a claim; supplier bills require positive `extractedAmount` and `amount`, with `amount <= extractedAmount`; an already-paid supplier bill sets `alreadyPaid=true` and requires `paymentReference` plus a staged PDF in `paymentProofAttachments`; admin creation automatically queues QBO finalization while other roles remain pending approval; non-legacy supplier bills from 1 Jul 2026 00:00 SGT require approved quotation coverage even when already paid), `update-bill` (`--payload`), `update-bill-payment-evidence` (`--payload`; replaces the payment reference and payment-proof PDFs for an already-paid bill), `delete-bill`, `create-bill-approval` (also sends approval request emails), `update-bill-status` (`PAID` requires `--paymentReference`; pass `--paymentProof <receipt.pdf>` to stage and submit a PDF up to 20MB atomically, or omit it only when BB already has payment proof; moving to `PAID` runs the server's paid-bill notification workflow), `patch-bill-payment` (PAID bills: `--paymentTrackingUrl`, `--paymentReference`, `--quickbooksBillId`, `--paymentDate` ISO; clearing a paid bill's QuickBooks link is not allowed), `patch-bill-invoice-number`, `get-bill-attachments`, `upload-bill-attachment` (`<billId>` + local path), `get-bill-details` |
238
+ | **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`, `rename-budget-version`, `restore-budget-version`, `update-budget-status` (`ESTIMATE_ACCEPTED` requires win proof, auto-marks `PITCH` or `LOST` projects `WON`, and auto-creates an Asana Event Ops section from Prompt 5 tasks when enabled; Prompt 5 skips quantity-zero items, combines matching non-GO-internal suppliers, keeps each GO internal item separate, removes generated work that repeats another line item or group, and schedules applicable subtasks relative to the project start date; `--projectStatusOnCommercialRejection PITCH\|LOST` is required when rejecting the only accepted/closed budget on a commercial project), `mark-budget-won` (`<budgetId>` + proof file path; `PITCH` or `LOST` projects become `WON` automatically), `create-budget` / `update-budget` (`--payload`; cloned unavailable lines require `unavailableItemReviewAcknowledged: true`), `delete-budget`, `create-budget-approval` (also sends approval request emails), `create-estimate`, `send-estimate-to-contact-person` (same contact-person email workflow as web), `add-budget-items`, `update-budget-item`, `replace-budget-item`, `approve-unavailable-item-exception` (Lead/Admin), `remove-budget-item`, `reorder-budget-items`, `update-budget-item-supplier`, `mark-budget-items-not-utilized`, `restore-budget-item`, `create-placeholder-bill` (admin recovery), `create-budget-category`, `update-budget-category`, `delete-budget-category`, `update-budget-commission`, `delete-budget-commission`, `update-budget-discount`, `delete-budget-discount` (`--payload` where noted), `upload-budget-attachment` (`<budgetId>` + local file path; uses `attachment.requestBudgetAttachmentUpload` + PUT + `attachment.confirmBudgetAttachment`) |
239
+ | **Bills / claims** | `list-bills` (`--isClaimable false` for bills, `--isClaimable true` for claims, omit for both), `list-claims` (claims only), `validate-bill-selection` (`--payload`; validates the project and supplier separately, then checks approved quotation links for each selected budget line item independent of the bill supplier; `alreadyPaid` never bypasses the checks), `stage-bill-attachment` (securely uploads invoice/payment-proof files before creation and returns attachment JSON; ownership and one-hour expiry are enforced by a server-side staged-upload record rather than encoded in the object key), `cleanup-staged-bill-attachments`, `create-bill` (`--payload`; set `isClaimable=false` for a bill, `isClaimable=true` for a claim; supplier bills require positive `extractedAmount` and `amount`, with `amount <= extractedAmount`; an already-paid supplier bill sets `alreadyPaid=true` and requires `paymentReference` plus a staged PDF in `paymentProofAttachments`; admin creation automatically queues QBO finalization while other roles remain pending approval; non-legacy supplier bills from 1 Jul 2026 00:00 SGT require approved quotation coverage linked to every selected line item, independent of the bill supplier, even when already paid), `update-bill` (`--payload`), `update-bill-payment-evidence` (`--payload`; replaces the payment reference and payment-proof PDFs for an already-paid bill), `delete-bill`, `create-bill-approval` (also sends approval request emails), `update-bill-status` (`PAID` requires `--paymentReference`; pass `--paymentProof <receipt.pdf>` to stage and submit a PDF up to 20MB atomically, or omit it only when BB already has payment proof; moving to `PAID` runs the server's paid-bill notification workflow), `patch-bill-payment` (PAID bills: `--paymentTrackingUrl`, `--paymentReference`, `--quickbooksBillId`, `--paymentDate` ISO; clearing a paid bill's QuickBooks link is not allowed), `patch-bill-invoice-number`, `get-bill-attachments`, `upload-bill-attachment` (`<billId>` + local path), `get-bill-details` |
231
240
  | **Quotations** | `list-quotations` (supports project, budget, supplier, status, requester, and text-search filters), `get-quotation`, `upload-quotation-attachment` (`<projectId>` + PDF/JPEG/PNG path up to 20MB; returns attachment JSON for payload use), `cleanup-staged-quotation-attachments`, `create-quotation` (`--payload` for `quotation.createDraft`; include `amountWithoutGst` and `gstAmount`), `update-quotation` (`--payload`; updates a `DRAFT` or `REJECTED` quotation), `delete-quotation`, `submit-quotation`, `approve-quotation`, `reject-quotation`, `download-quotation-pdf` (`original`, `staff`, or `final`) |
232
241
  | **Customer invoices** | `check-customer-invoice-readiness`, `list-eligible-customer-invoice-budgets`, `list-customer-invoices` (global/project/budget filters plus summary metrics), `get-customer-invoice`, `get-customer-invoice-email-context`, `create-customer-invoice`, `discard-customer-invoice`, `delete-customer-invoice`, `void-customer-invoice`, `approve-customer-invoice` (admin), `reject-customer-invoice` (admin), `send-customer-invoice-to-contact-person`, `download-customer-invoice-pdf` (non-admins cannot download while approval is pending), `sync-customer-invoice` |
233
242
  | **Approvals** | `list-approvals` / `get-pending-approvals` (`--type budget\|supplier\|bill\|quotation\|customer_invoice\|all`), `approve-bill` / `reject-bill` (send reply email), `approve-budget` / `reject-budget` (send reply email), `approve-supplier` / `reject-supplier` (send reply email), `approve-quotation` / `reject-quotation`, `approve-customer-invoice` / `reject-customer-invoice` |
234
243
  | **Companies & projects** | `list-companies`, `get-company`, `create-company`, `update-company` (`--payload`), `delete-company`, `list-projects`, `get-project` (its budget overview returns `totalRevenue`, calculated only from Estimate Accepted and Estimate Closed budgets), `get-project-hub-status`, `setup-project-hub`, `sync-project-hub-commercial-documents`, `create-project` (required: `--name`, `--companyId`, `--contactPersonId`, `--insideSalesId`, `--businessDevelopmentId`, `--venue`, `--startDate` as ISO datetime for project/window start; when `--asanaTaskId` is omitted, the CLI searches open Asana lead tasks in the Deals project, prompts for one of the top five matches, and resolves Slack channel fields from the selected deal card; optional `--asanaSearch`, `--pax`, `--endDate`, `--description`, `--projectManagerId`; always requests QBO project import like the web app), `update-project` (`--payload` with `dateRange.from` / `dateRange.to` for the project window; optional `projectManagerId` and `requestQboAccountantNotification` in JSON), `delete-project`, `check-project-reconciliation <id>` (runs the web app's live checks without changing status), `reconcile-project <id>` (reruns checks transactionally and marks an eligible project `RECONCILED`), `complete-project <id>` (reruns validation, marks a reconciled project `COMPLETED`, and queues QuickBooks placeholder cleanup), `import-qbo-project`, `update-project-status` (`<id>` `<status>`: `PITCH` \| `WON` \| `COMPLETED` \| `RECONCILED` \| `LOST`; close-out follows `WON` → `RECONCILED` → `COMPLETED`, and reconciliation requires every accepted/closed budget line to have an Approved/Paid bill or claim or be explicitly Not Utilized; marking `COMPLETED` queues deletion of every remaining project placeholder bill from QuickBooks; for `PITCH` → `WON` also pass a Budget Builder user `--projectManagerId` or `--projectManagerEmail`; when marking `WON` without an accepted/closed budget or proof, pass `--wonOverrideReason`) |
235
244
  | **Contacts** | `list-contacts`, `create-contact-person` (`--payload`), `update-contact-person` (`--payload`) |
236
- | **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` (admin), `list-items`, `create-item` (`--payload`), `update-item` (`--payload`), `archive-items` (`--ids` CSV, max 100; Budget Builder only; users can archive their own items, leads/admins any item; blocked by Draft/Pending Approval/Approved budgets), `delete-item` (admin; archives in Budget Builder and makes the item inactive in QuickBooks), `get-item`, `list-item-categories`, `create-item-category` / `update-item-category` / `delete-item-categories` (admin; `--ids` CSV for delete) |
245
+ | **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` (admin), `list-items` (`--includeQuickBooksDeactivationPending true` discovers interrupted QuickBooks deactivations for Admin retry), `create-item` (`--payload`), `update-item` (`--payload`), `archive-items` (`--ids` CSV, max 100; Budget Builder-only; open estimates retain unavailable snapshot lines for resolution), `delete-item` (admin; archives in Budget Builder and makes the item inactive in QuickBooks; blocked while an open estimate references it), `get-item`, `list-item-categories`, `create-item-category` / `update-item-category` / `delete-item-categories` (admin; `--ids` CSV for delete) |
237
246
  | **Dashboard & users** | `whoami` (current API-key owner identity and role), `list-users`, `create-user` (admin; provisions an API-only service identity with no Google sign-in; `--email`, optional `--name`, optional `--role` defaulting to `USER`), `create-api-key` (`--name`; defaults to the caller; admin-only `--userId` for another user; raw key shown once), `list-api-keys` (defaults to the caller; admin-only `--userId` for another user), `revoke-api-key` (key ID; admin-only `--userId` for another user), `get-user-performance`, `get-dashboard`, `get-monthly-metrics`, `get-system-overview`, `get-estimate-performance`, `get-financial-overview` (performance/dashboard commands are admin-only) |
238
247
  | **Errors (admin)** | `get-recent-errors`, `get-error-metrics` |
239
- | **Automation (admin)** | `list-integration-operations`, `retry-integration-operation` |
248
+ | **Automation (admin)** | `list-integration-operations`, `retry-integration-operation` (Resend resolution requires `--confirmExternalStateReconciled true` plus `--outboundEmailResolution ACCEPTED\|NOT_ACCEPTED_RETRY`; include `--providerMessageId` when accepted) |
240
249
  | **Historical / benchmarks** | `get-approved-budgets`, `get-budget-category-benchmarks`, `get-item-pricing-history`, `get-supplier-pricing-history` |
241
250
 
242
251
  The CLI intentionally wraps low-level upload-request/confirm procedures into file-based commands and omits browser-only helpers such as navigation counts, combobox/facet data, recent-page bookkeeping, and live UI subscriptions. Operational workflows for bills, claims, quotations, customer invoices, project reconciliation/completion, and integration recovery are available directly.
@@ -970,6 +970,101 @@
970
970
  "effects": ["state-change", "delete"],
971
971
  "legacyTarget": "remove-budget-item"
972
972
  },
973
+ {
974
+ "path": ["budget", "item", "replace"],
975
+ "legacyAliases": ["replace-budget-item", "replace_budget_item"],
976
+ "summary": "Replace Budget Item.",
977
+ "globalOptions": [
978
+ {
979
+ "name": "--help",
980
+ "description": "Show help for this command."
981
+ },
982
+ {
983
+ "name": "--quiet",
984
+ "description": "Suppress non-error diagnostics."
985
+ },
986
+ {
987
+ "name": "--debug",
988
+ "description": "Emit sanitized diagnostic traces."
989
+ },
990
+ {
991
+ "name": "--api-url",
992
+ "description": "Override the Budget Builder API base URL."
993
+ },
994
+ {
995
+ "name": "--allow-state-change",
996
+ "description": "Allow a Budget Builder state change in non-interactive use."
997
+ },
998
+ {
999
+ "name": "--allow-email",
1000
+ "description": "Allow sending email in non-interactive use."
1001
+ },
1002
+ {
1003
+ "name": "--allow-external-write",
1004
+ "description": "Allow writes to external systems in non-interactive use."
1005
+ },
1006
+ {
1007
+ "name": "--allow-delete",
1008
+ "description": "Allow deleting data in non-interactive use."
1009
+ },
1010
+ {
1011
+ "name": "--allow-financial-write",
1012
+ "description": "Allow financial-record changes in non-interactive use."
1013
+ }
1014
+ ],
1015
+ "argumentMode": "legacy-passthrough",
1016
+ "effects": ["state-change", "financial-write"],
1017
+ "legacyTarget": "replace-budget-item"
1018
+ },
1019
+ {
1020
+ "path": ["budget", "item", "exception", "approve"],
1021
+ "legacyAliases": [
1022
+ "approve-unavailable-item-exception",
1023
+ "approve_unavailable_item_exception"
1024
+ ],
1025
+ "summary": "Approve Unavailable Item Exception.",
1026
+ "globalOptions": [
1027
+ {
1028
+ "name": "--help",
1029
+ "description": "Show help for this command."
1030
+ },
1031
+ {
1032
+ "name": "--quiet",
1033
+ "description": "Suppress non-error diagnostics."
1034
+ },
1035
+ {
1036
+ "name": "--debug",
1037
+ "description": "Emit sanitized diagnostic traces."
1038
+ },
1039
+ {
1040
+ "name": "--api-url",
1041
+ "description": "Override the Budget Builder API base URL."
1042
+ },
1043
+ {
1044
+ "name": "--allow-state-change",
1045
+ "description": "Allow a Budget Builder state change in non-interactive use."
1046
+ },
1047
+ {
1048
+ "name": "--allow-email",
1049
+ "description": "Allow sending email in non-interactive use."
1050
+ },
1051
+ {
1052
+ "name": "--allow-external-write",
1053
+ "description": "Allow writes to external systems in non-interactive use."
1054
+ },
1055
+ {
1056
+ "name": "--allow-delete",
1057
+ "description": "Allow deleting data in non-interactive use."
1058
+ },
1059
+ {
1060
+ "name": "--allow-financial-write",
1061
+ "description": "Allow financial-record changes in non-interactive use."
1062
+ }
1063
+ ],
1064
+ "argumentMode": "legacy-passthrough",
1065
+ "effects": ["state-change"],
1066
+ "legacyTarget": "approve-unavailable-item-exception"
1067
+ },
973
1068
  {
974
1069
  "path": ["budget", "item", "reorder"],
975
1070
  "legacyAliases": ["reorder-budget-items", "reorder_budget_items"],
@@ -3403,7 +3498,7 @@
3403
3498
  "send-customer-invoice-to-contact-person",
3404
3499
  "send_customer_invoice_to_contact_person"
3405
3500
  ],
3406
- "summary": "Send Customer Invoice To Contact Person.",
3501
+ "summary": "Send a customer invoice and update the project billing email.",
3407
3502
  "globalOptions": [
3408
3503
  {
3409
3504
  "name": "--help",
@@ -4190,6 +4285,76 @@
4190
4285
  "effects": [],
4191
4286
  "legacyTarget": "list-projects"
4192
4287
  },
4288
+ {
4289
+ "path": ["project", "hub", "list"],
4290
+ "legacyAliases": ["list-project-hubs", "list_project_hubs"],
4291
+ "summary": "List project hubs.",
4292
+ "globalOptions": [
4293
+ {
4294
+ "name": "--help",
4295
+ "description": "Show help for this command."
4296
+ },
4297
+ {
4298
+ "name": "--quiet",
4299
+ "description": "Suppress non-error diagnostics."
4300
+ },
4301
+ {
4302
+ "name": "--debug",
4303
+ "description": "Emit sanitized diagnostic traces."
4304
+ },
4305
+ {
4306
+ "name": "--api-url",
4307
+ "description": "Override the Budget Builder API base URL."
4308
+ },
4309
+ {
4310
+ "name": "--allow-state-change",
4311
+ "description": "Allow a Budget Builder state change in non-interactive use."
4312
+ },
4313
+ {
4314
+ "name": "--allow-email",
4315
+ "description": "Allow sending email in non-interactive use."
4316
+ },
4317
+ {
4318
+ "name": "--allow-external-write",
4319
+ "description": "Allow writes to external systems in non-interactive use."
4320
+ },
4321
+ {
4322
+ "name": "--allow-delete",
4323
+ "description": "Allow deleting data in non-interactive use."
4324
+ },
4325
+ {
4326
+ "name": "--allow-financial-write",
4327
+ "description": "Allow financial-record changes in non-interactive use."
4328
+ },
4329
+ {
4330
+ "name": "--q",
4331
+ "description": "Search project and company names."
4332
+ },
4333
+ {
4334
+ "name": "--status",
4335
+ "description": "Filter by comma-separated project statuses."
4336
+ },
4337
+ {
4338
+ "name": "--page",
4339
+ "description": "Select a results page."
4340
+ },
4341
+ {
4342
+ "name": "--perPage",
4343
+ "description": "Set the results page size."
4344
+ },
4345
+ {
4346
+ "name": "--sortBy",
4347
+ "description": "Sort by startDate, name, companyName, or status."
4348
+ },
4349
+ {
4350
+ "name": "--sortDir",
4351
+ "description": "Sort in asc or desc order."
4352
+ }
4353
+ ],
4354
+ "argumentMode": "legacy-passthrough",
4355
+ "effects": [],
4356
+ "legacyTarget": "list-project-hubs"
4357
+ },
4193
4358
  {
4194
4359
  "path": ["project", "get"],
4195
4360
  "legacyAliases": ["get-project", "get_project"],
@@ -184,6 +184,22 @@ Legacy aliases: `remove-budget-item`, `remove_budget_item`.
184
184
 
185
185
  Effects: state-change, delete.
186
186
 
187
+ ## `bb budget item replace`
188
+
189
+ Replace Budget Item.
190
+
191
+ Legacy aliases: `replace-budget-item`, `replace_budget_item`.
192
+
193
+ Effects: state-change, financial-write.
194
+
195
+ ## `bb budget item exception approve`
196
+
197
+ Approve Unavailable Item Exception.
198
+
199
+ Legacy aliases: `approve-unavailable-item-exception`, `approve_unavailable_item_exception`.
200
+
201
+ Effects: state-change.
202
+
187
203
  ## `bb budget item reorder`
188
204
 
189
205
  Reorder Budget Items.
@@ -602,7 +618,7 @@ Effects: state-change, email, external-write, financial-write.
602
618
 
603
619
  ## `bb customer-invoice send`
604
620
 
605
- Send Customer Invoice To Contact Person.
621
+ Send a customer invoice and update the project billing email.
606
622
 
607
623
  Legacy aliases: `send-customer-invoice-to-contact-person`, `send_customer_invoice_to_contact_person`.
608
624
 
@@ -736,6 +752,14 @@ Legacy aliases: `list-projects`, `list_projects`.
736
752
 
737
753
  Effects: none.
738
754
 
755
+ ## `bb project hub list`
756
+
757
+ List project hubs.
758
+
759
+ Legacy aliases: `list-project-hubs`, `list_project_hubs`.
760
+
761
+ Effects: none.
762
+
739
763
  ## `bb project get`
740
764
 
741
765
  Get project.
package/dist/commands.js CHANGED
@@ -1037,6 +1037,16 @@ export async function removeBudgetItem(budgetItemId) {
1037
1037
  });
1038
1038
  out(result);
1039
1039
  }
1040
+ export const replaceBudgetItem = async (input) => {
1041
+ await confirmLockedBudgetChangeByBudgetItemId(input.budgetItemId, "Replace unavailable budget item");
1042
+ const result = await api.budgetItem.replaceBudgetItem.mutate(input);
1043
+ out(result);
1044
+ };
1045
+ export const approveUnavailableItemException = async (input) => {
1046
+ await confirmLockedBudgetChangeByBudgetItemId(input.budgetItemId, "Approve unavailable item exception");
1047
+ const result = await api.budgetItem.approveUnavailableItemException.mutate(input);
1048
+ out(result);
1049
+ };
1040
1050
  export async function getBudgetCategories() {
1041
1051
  const categories = await api.budget.getBudgetCategories.query();
1042
1052
  out(categories);
@@ -1091,7 +1101,7 @@ export async function createEstimate(budgetId, preflight) {
1091
1101
  const result = await api.quickbooks.createEstimate.mutate({ budgetId });
1092
1102
  out(result);
1093
1103
  }
1094
- export const describeEstimateEmailConfirmation = (input) => `to ${input.to}; CC ${input.cc.length > 0 ? input.cc.join(", ") : "none"}; reply-to ${input.replyTo}; subject "${input.subject}"; attach the QBO estimate PDF, terms and conditions, and Budget Builder attachments, then mark the estimate sent`;
1104
+ export const describeEstimateEmailConfirmation = (input) => `to ${input.to}; CC ${input.cc.length > 0 ? input.cc.join(", ") : "none"}; reply-to ${input.replyTo}; subject "${input.subject}"; queue durable worker delivery with the QBO estimate PDF, terms and conditions, and Budget Builder attachments; mark the estimate sent after provider acceptance`;
1095
1105
  export const sendEstimateToContactPersonFromPayload = async (raw) => {
1096
1106
  const input = parseSendEstimateToContactPersonPayload(raw);
1097
1107
  await assertSensitiveWorkflowConfirmed({
@@ -1425,7 +1435,7 @@ export const syncCustomerInvoice = async (invoiceId) => {
1425
1435
  });
1426
1436
  out(result);
1427
1437
  };
1428
- export const describeCustomerInvoiceEmailConfirmation = (input) => `emails the QBO invoice PDF to ${input.to}; requested CC ${input.cc.length > 0 ? input.cc.join(", ") : "none"}; reply-to ${input.replyTo}; subject "${input.subject}"; server-required admin, creator, business-development, and inside-sales CC recipients are added; success marks the invoice SENT; do not retry blindly after an ambiguous delivery failure`;
1438
+ export const describeCustomerInvoiceEmailConfirmation = (input) => `emails the QBO invoice PDF to ${input.to} and saves that address as the project's billing email; requested CC ${input.cc.length > 0 ? input.cc.join(", ") : "none"}; reply-to ${input.replyTo}; subject "${input.subject}"; server-required admin, creator, business-development, and inside-sales CC recipients are added; success marks the invoice SENT; do not retry blindly after an ambiguous delivery failure`;
1429
1439
  export const sendCustomerInvoiceToContactPersonFromPayload = async (raw) => {
1430
1440
  const input = parseSendCustomerInvoiceToContactPersonPayload(raw);
1431
1441
  await assertSensitiveWorkflowConfirmed({
@@ -1907,6 +1917,14 @@ export async function listProjects(opts) {
1907
1917
  });
1908
1918
  out(result);
1909
1919
  }
1920
+ export const listProjectHubs = async (opts) => {
1921
+ const result = await api.project.getAllProjectHubs.query({
1922
+ ...opts,
1923
+ page: opts.page ?? 1,
1924
+ perPage: opts.perPage ?? 20,
1925
+ });
1926
+ out(result);
1927
+ };
1910
1928
  export async function getProject(id) {
1911
1929
  const [project, budgetsOverview, billsOverview] = await Promise.all([
1912
1930
  api.project.getProjectDetailsById.query({ id }),
@@ -2015,17 +2033,25 @@ export const listIntegrationOperations = async (input) => {
2015
2033
  const result = await api.integration.listOperations.query(input);
2016
2034
  out(result);
2017
2035
  };
2018
- export const retryIntegrationOperation = async (operationId, confirmExternalStateReconciled) => {
2036
+ export const retryIntegrationOperation = async (operationId, confirmExternalStateReconciled, outboundEmailResolution, providerMessageId) => {
2019
2037
  await assertSensitiveWorkflowConfirmed({
2020
- action: "Retry integration operation",
2038
+ action: outboundEmailResolution === "ACCEPTED"
2039
+ ? "Reconcile integration operation as completed"
2040
+ : "Retry integration operation",
2021
2041
  entity: `operation ${operationId}`,
2022
- details: confirmExternalStateReconciled
2023
- ? "after confirming the external state has been reconciled"
2024
- : undefined,
2042
+ details: outboundEmailResolution === "ACCEPTED"
2043
+ ? `mark provider-accepted email sent with message ID ${providerMessageId ?? "<missing>"}`
2044
+ : outboundEmailResolution === "NOT_ACCEPTED_RETRY"
2045
+ ? "rotate the Resend idempotency key and retry provider delivery"
2046
+ : confirmExternalStateReconciled
2047
+ ? "after confirming the external state has been reconciled"
2048
+ : undefined,
2025
2049
  });
2026
2050
  const result = await api.integration.retryOperation.mutate({
2027
2051
  operationId,
2028
2052
  confirmExternalStateReconciled,
2053
+ outboundEmailResolution,
2054
+ providerMessageId,
2029
2055
  });
2030
2056
  out(result);
2031
2057
  };
@@ -2081,6 +2107,7 @@ export async function listContacts(companyId) {
2081
2107
  // --- Items ---
2082
2108
  export async function listItems(opts) {
2083
2109
  const result = await api.item.getItems.query({
2110
+ includeQuickBooksDeactivationPending: opts.includeQuickBooksDeactivationPending,
2084
2111
  page: opts.page ?? 1,
2085
2112
  perPage: opts.perPage ?? 20,
2086
2113
  name: opts.name,
package/dist/index.js CHANGED
@@ -5,9 +5,9 @@ import { resolve } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { BUDGET_BUILDER_API_BASE_URL, configureBudgetBuilderApiBaseUrl, DEFAULT_BUDGET_BUILDER_API_BASE_URL, requireApiKey, } from "./api-client.js";
7
7
  import { consumeCliQuietFlags, logCliAction, sanitizeFlagsForTrace, setCliQuiet, shouldLogCliActions, } from "./cli-trace.js";
8
- import { addBudgetItems, approveBill, approveBudget, approveCustomerInvoice, approveQuotation, approveSupplier, archiveItemsByIds, checkCustomerInvoiceReadiness, checkProjectReconciliation, cleanupStagedBillAttachments, cleanupStagedQuotationAttachments, completeProject, createApiKeyForUser, createBillApproval, createBillFromPayload, createBudgetApproval, createBudgetCategory, createBudgetFromPayload, createCompany, createContactPersonFromPayload, createCustomerInvoice, createEstimate, createInboundSource, createItemCategory, createItemFromPayload, createPlaceholderBillForBudgetItem, createProject, createQuotationFromPayload, createSupplierCertification, createSupplierFromPayload, createSupplierPaymentMethod, createSupplierRoleOption, createSupplierTagOption, createUser, deleteBillById, deleteBudgetById, deleteBudgetCategory, deleteBudgetCommission, deleteBudgetDiscount, deleteCompanyById, deleteCustomerInvoice, deleteItemById, deleteItemCategoriesByIds, deleteProjectById, deleteQuotationById, deleteSuppliersByIds, discardCreatingCustomerInvoice, downloadCustomerInvoicePdf, downloadQuotationPdf, getApprovedBudgets, getBillAttachments, getBillDetails, getBudget, getBudgetCategories, getBudgetCategoryBenchmarks, getBudgetDetails, getBudgetItemsOnly, getBudgetVersions, getCompany, getCustomerInvoice, getCustomerInvoiceEmailContext, getDashboard, getErrorMetrics, getEstimatePerformance, getFinancialOverview, getInboundSource, getInboundSubmission, getItem, getItemPricingHistory, getMonthlyMetrics, getProject, getProjectHubStatus, getQuotationDetails, getRecentErrors, getSupplierAnalytics, getSupplierDetails, getSupplierPricingHistory, getSystemOverview, getUserPerformance, importQuickBooksProjectId, listApiKeysForUser, listApprovals, listBills, listBudgets, listCompanies, listContacts, listCustomerInvoices, listEligibleCustomerInvoiceBudgets, listInboundSources, listInboundSubmissions, listIntegrationOperations, listItemCategories, listItems, listProjects, listQuotations, listSuppliers, listUsers, markBudgetWonWithProof, patchBillInvoiceNumber, patchBillPayment, reactivateSuppliersByIds, reconcileProject, rejectBill, rejectBudget, rejectCustomerInvoice, rejectQuotation, rejectSupplier, removeBudgetItem, renameBudgetVersion, reorderBudgetItemsCli, restoreBudgetVersion, retryIntegrationOperation, revokeApiKeyForUser, saveInboundAutomationConfig, saveInboundMappingVersion, sendCustomerInvoiceToContactPersonFromPayload, sendEstimateToContactPersonFromPayload, setBudgetItemsNotUtilized, setupProjectHub, stageBillAttachmentsFromPaths, submitQuotation, syncCustomerInvoice, syncProjectHubCommercialDocuments, updateBillFromPayload, updateBillPaymentEvidenceFromPayload, updateBillStatus, updateBudgetCategory, updateBudgetCommissionFromPayload, updateBudgetDiscountFromPayload, updateBudgetFromPayload, updateBudgetItem, updateBudgetItemSupplierCli, updateBudgetStatus, updateCompanyFromPayload, updateContactPersonFromPayload, updateInboundProcessing, updateInboundSource, updateItemCategory, updateItemFromPayload, updateProjectFromPayload, updateProjectStatus, updateQuotationFromPayload, updateSupplierFromPayload, uploadBillAttachmentFromPath, uploadBillAttachmentsFromPaths, uploadBillDocumentsFromPaths, uploadBudgetAttachmentFromPath, uploadQuotationAttachmentFromPath, validateBillSelectionFromPayload, voidCustomerInvoice, whoAmI, } from "./commands.js";
8
+ import { addBudgetItems, approveBill, approveBudget, approveCustomerInvoice, approveQuotation, approveSupplier, approveUnavailableItemException, archiveItemsByIds, checkCustomerInvoiceReadiness, checkProjectReconciliation, cleanupStagedBillAttachments, cleanupStagedQuotationAttachments, completeProject, createApiKeyForUser, createBillApproval, createBillFromPayload, createBudgetApproval, createBudgetCategory, createBudgetFromPayload, createCompany, createContactPersonFromPayload, createCustomerInvoice, createEstimate, createInboundSource, createItemCategory, createItemFromPayload, createPlaceholderBillForBudgetItem, createProject, createQuotationFromPayload, createSupplierCertification, createSupplierFromPayload, createSupplierPaymentMethod, createSupplierRoleOption, createSupplierTagOption, createUser, deleteBillById, deleteBudgetById, deleteBudgetCategory, deleteBudgetCommission, deleteBudgetDiscount, deleteCompanyById, deleteCustomerInvoice, deleteItemById, deleteItemCategoriesByIds, deleteProjectById, deleteQuotationById, deleteSuppliersByIds, discardCreatingCustomerInvoice, downloadCustomerInvoicePdf, downloadQuotationPdf, getApprovedBudgets, getBillAttachments, getBillDetails, getBudget, getBudgetCategories, getBudgetCategoryBenchmarks, getBudgetDetails, getBudgetItemsOnly, getBudgetVersions, getCompany, getCustomerInvoice, getCustomerInvoiceEmailContext, getDashboard, getErrorMetrics, getEstimatePerformance, getFinancialOverview, getInboundSource, getInboundSubmission, getItem, getItemPricingHistory, getMonthlyMetrics, getProject, getProjectHubStatus, getQuotationDetails, getRecentErrors, getSupplierAnalytics, getSupplierDetails, getSupplierPricingHistory, getSystemOverview, getUserPerformance, importQuickBooksProjectId, listApiKeysForUser, listApprovals, listBills, listBudgets, listCompanies, listContacts, listCustomerInvoices, listEligibleCustomerInvoiceBudgets, listInboundSources, listInboundSubmissions, listIntegrationOperations, listItemCategories, listItems, listProjectHubs, listProjects, listQuotations, listSuppliers, listUsers, markBudgetWonWithProof, patchBillInvoiceNumber, patchBillPayment, reactivateSuppliersByIds, reconcileProject, rejectBill, rejectBudget, rejectCustomerInvoice, rejectQuotation, rejectSupplier, removeBudgetItem, renameBudgetVersion, reorderBudgetItemsCli, replaceBudgetItem, restoreBudgetVersion, retryIntegrationOperation, revokeApiKeyForUser, saveInboundAutomationConfig, saveInboundMappingVersion, sendCustomerInvoiceToContactPersonFromPayload, sendEstimateToContactPersonFromPayload, setBudgetItemsNotUtilized, setupProjectHub, stageBillAttachmentsFromPaths, submitQuotation, syncCustomerInvoice, syncProjectHubCommercialDocuments, updateBillFromPayload, updateBillPaymentEvidenceFromPayload, updateBillStatus, updateBudgetCategory, updateBudgetCommissionFromPayload, updateBudgetDiscountFromPayload, updateBudgetFromPayload, updateBudgetItem, updateBudgetItemSupplierCli, updateBudgetStatus, updateCompanyFromPayload, updateContactPersonFromPayload, updateInboundProcessing, updateInboundSource, updateItemCategory, updateItemFromPayload, updateProjectFromPayload, updateProjectStatus, updateQuotationFromPayload, updateSupplierFromPayload, uploadBillAttachmentFromPath, uploadBillAttachmentsFromPaths, uploadBillDocumentsFromPaths, uploadBudgetAttachmentFromPath, uploadQuotationAttachmentFromPath, validateBillSelectionFromPayload, voidCustomerInvoice, whoAmI, } from "./commands.js";
9
9
  import { getFlag, parseArgs } from "./parse-args.js";
10
- import { billStatusesForUpdateHelp, budgetStatusesForHelp, isBudgetStatusUpdate, parseApprovalTypeFlag, parseBillStatusForUpdate, parseCommaSeparatedBillStatuses, parseCommaSeparatedBudgetStatuses, parseCommaSeparatedCustomerInvoiceStatuses, parseCommaSeparatedIds, parseCommaSeparatedIntegrationOperationStatuses, parseCommaSeparatedQuotationStatuses, parseCommaSeparatedSupplierStatuses, parseOptionalBillListSortBy, parseOptionalBillListSortDir, parseOptionalDashboardRole, parseOptionalDeals, parseOptionalErrorSeverity, parseOptionalErrorStatus, parseOptionalExtendedProjectStatus, parseOptionalFinancialRole, parseOptionalSupplierAnalyticsTimeFrame, parseOptionalTimeFrame, parseProjectStatusForUpdate, parseUserRole, projectStatusesForHelp, userRolesForHelp, } from "./parse-cli-enums.js";
10
+ import { billStatusesForUpdateHelp, budgetStatusesForHelp, isBudgetStatusUpdate, parseApprovalTypeFlag, parseBillStatusForUpdate, parseCommaSeparatedBillStatuses, parseCommaSeparatedBudgetStatuses, parseCommaSeparatedCustomerInvoiceStatuses, parseCommaSeparatedIds, parseCommaSeparatedIntegrationOperationStatuses, parseCommaSeparatedProjectStatuses, parseCommaSeparatedQuotationStatuses, parseCommaSeparatedSupplierStatuses, parseOptionalBillListSortBy, parseOptionalBillListSortDir, parseOptionalDashboardRole, parseOptionalDeals, parseOptionalErrorSeverity, parseOptionalErrorStatus, parseOptionalExtendedProjectStatus, parseOptionalFinancialRole, parseOptionalProjectHubSortBy, parseOptionalSupplierAnalyticsTimeFrame, parseOptionalTimeFrame, parseProjectStatusForUpdate, parseUserRole, projectStatusesForHelp, userRolesForHelp, } from "./parse-cli-enums.js";
11
11
  import { parseJsonFlag, parseOptionalNumber as parseOptNum, } from "./parse-json-flag.js";
12
12
  import { createCompletionScript, createHumanHelp, resolveCommand, } from "./registry/index.js";
13
13
  import { CliRuntimeError, clearActiveCliSession, confirmCurrentCommand, createProcessRuntime, emitCommandError, emitCommandResult, setActiveCliSession, } from "./runtime/index.js";
@@ -155,6 +155,9 @@ AI agents must inspect the entity first, summarize the exact entity, target
155
155
  state, and side effects, then wait for explicit user confirmation before running
156
156
  the sensitive command.
157
157
 
158
+ Budget mutations return a conflict while an estimate email is queued,
159
+ processing, or awaiting provider reconciliation.
160
+
158
161
  Budgets
159
162
  list-budgets [--projectId] [--name] [--status CSV] [--createdBy] [--dateFrom] [--dateTo] [--sortBy] [--sortDir] [--page] [--perPage] [--summary]
160
163
  get-budget <id> Budget + line items
@@ -170,7 +173,7 @@ Budgets
170
173
  ESTIMATE_REJECTED requires --projectStatusOnCommercialRejection when rejecting the only accepted/closed budget on a commercial project.
171
174
  status: ${budgetStatusesForHelp.join(", ")}
172
175
  mark-budget-won <budgetId> <filePath> [--projectManagerId <id>] [--invoiceSettings <json>] Upload signed quote/PO proof and set status to ESTIMATE_ACCEPTED; first acceptance requires invoice settings when customer invoicing is enabled
173
- create-budget --payload '<json>' (budget.createBudget; Asana deal card is on the project)
176
+ create-budget --payload '<json>' (budget.createBudget; when sourceBudgetId contains unavailable items, set unavailableItemReviewAcknowledged=true after explicit review)
174
177
  update-budget --payload '<json>' (budget.updateBudget; must include id)
175
178
  delete-budget <budgetId>
176
179
  create-budget-approval <budgetId> (also sends approval request emails)
@@ -179,6 +182,8 @@ Budgets
179
182
  add-budget-items --budgetId --items '[{"itemId":"…","quantity":1,"markup":30},…]'
180
183
  update-budget-item --id <budgetItemId> [--description plain-text] [--note plain-text] [--quantity] [--markup] [--cost] [--unitPrice] [--isFreeOfCharge true|false] [--gstInclusive true|false] [--gstOutOfScope true|false]
181
184
  remove-budget-item <budgetItemId>
185
+ replace-budget-item --budgetItemId <id> --replacementItemId <activeCatalogueItemId>
186
+ approve-unavailable-item-exception --budgetItemId <id> --reason <text> Lead/Admin only; reason must be 5-1000 characters.
182
187
  reorder-budget-items <budgetId> --itemIds <csv>
183
188
  update-budget-item-supplier --budgetItemId --supplierId
184
189
  create-budget-category --name <text>
@@ -197,10 +202,10 @@ Bills
197
202
  list-bills [--projectId] [--budgetId] [--status CSV] [--search <text>] [--isClaimable true|false] [--createdByIds <csv>] [--sortBy createdAt|amount|status] [--sortDir asc|desc] [--page] [--pageSize]
198
203
  list-claims same flags as list-bills; only reimbursable claims (ignores --isClaimable)
199
204
  isClaimable differentiates the shared bill/claim records: false = bill, true = claim.
200
- Non-legacy supplier bills from 1 Jul 2026 00:00 SGT require approved quotation coverage for every selected line item, including already-paid bills.
205
+ Non-legacy supplier bills from 1 Jul 2026 00:00 SGT require approved quotation coverage linked to every selected line item, independent of the bill supplier, including already-paid bills.
201
206
  create-bill-approval <billId> (also sends approval request emails)
202
207
  create-bill --payload '<json>' (bill.create; payload.isClaimable false = bill, true = claim)
203
- validate-bill-selection --payload '<json>' Check projectId, supplierId, and budgetItemIds before creation; alreadyPaid never bypasses quotation checks.
208
+ validate-bill-selection --payload '<json>' Check project and supplier validity separately, then verify approved quotation links for each budgetItemId; alreadyPaid never bypasses quotation checks.
204
209
  stage-bill-attachment <projectId> <filePath...> [--file <path>] [--files <csv>] Upload files before create-bill; returns attachment JSON for attachments/paymentProofAttachments
205
210
  cleanup-staged-bill-attachments <projectId> --keys <csv> Delete unattached staged bill/claim uploads.
206
211
  update-bill --payload '<json>' (bill.update; must include id)
@@ -268,6 +273,8 @@ Companies & projects
268
273
  delete-company <id>
269
274
  list-projects [--companyId] [--name] [--status] [--active true|false] [--page] [--perPage]
270
275
  Omit --active to include both active and inactive projects.
276
+ list-project-hubs [--q] [--status CSV] [--page] [--perPage] [--sortBy startDate|name|companyName|status] [--sortDir asc|desc]
277
+ List active projects that have a Project Hub integration.
271
278
  get-project <id>
272
279
  Budget overview totalRevenue includes Estimate Accepted and Estimate Closed budgets only.
273
280
  create-project --name --companyId --contactPersonId --insideSalesId --businessDevelopmentId --venue --startDate <ISO> [--projectManagerId <userId>] [--asanaTaskId] [--asanaSearch <text>] [--slackChannelId --slackChannelUrl --slackChannelName] [--pax] [--endDate <ISO>] [--description]
@@ -300,11 +307,11 @@ Suppliers & items
300
307
  create-supplier-tag --name (supplier.createSupplierTag)
301
308
  create-item --payload '<json>' (item.createItem)
302
309
  update-item --payload '<json>' (item.updateItem; must include id)
303
- archive-items --ids <csv> (item.archiveItems; Budget Builder only; max 100; users can archive their own items, leads/admins any item; blocked by Draft/Pending Approval/Approved budgets)
304
- delete-item <id> (item.deleteItem; admin; archives in Budget Builder and makes the item inactive in QuickBooks)
310
+ archive-items --ids <csv> (item.archiveItems; Budget Builder only; max 100; users can archive their own items, leads/admins any item; open estimates retain snapshot lines and require resolution before send/acceptance)
311
+ delete-item <id> (item.deleteItem; admin; archives in Budget Builder and makes the item inactive in QuickBooks; blocked while used by an open estimate)
305
312
  get-supplier-details <supplierId>
306
313
  get-supplier-analytics [--name] [--page] [--perPage] [--timeFrame ALL|LAST_YEAR|…] (admin)
307
- list-items [--name] [--page] [--perPage]
314
+ list-items [--name] [--page] [--perPage] [--includeQuickBooksDeactivationPending true]
308
315
  get-item <id>
309
316
  list-item-categories [--page] [--perPage]
310
317
  create-item-category --name (admin)
@@ -331,7 +338,7 @@ Errors
331
338
 
332
339
  Automation (admin)
333
340
  list-integration-operations [--search] [--destination <provider,...>] [--status <PENDING|PROCESSING|FAILED|COMPLETED,...>] [--page] [--perPage]
334
- retry-integration-operation <operationId> [--confirmExternalStateReconciled true|false]
341
+ retry-integration-operation <operationId> [--confirmExternalStateReconciled true|false] [--outboundEmailResolution ACCEPTED|NOT_ACCEPTED_RETRY] [--providerMessageId <id>]
335
342
 
336
343
  Historical / benchmarks
337
344
  get-approved-budgets [--projectId] [--limit] [--minRevenue] [--maxRevenue]
@@ -840,6 +847,30 @@ export async function runCli(argv = process.argv, runtime = createProcessRuntime
840
847
  await removeBudgetItem(id);
841
848
  break;
842
849
  }
850
+ case "replace-budget-item": {
851
+ const budgetItemId = getFlag(flags, "budgetItemId");
852
+ const replacementItemId = getFlag(flags, "replacementItemId");
853
+ if (!budgetItemId || !replacementItemId) {
854
+ throw new Error("replace-budget-item requires --budgetItemId and --replacementItemId");
855
+ }
856
+ await replaceBudgetItem({
857
+ budgetItemId: String(budgetItemId),
858
+ replacementItemId: String(replacementItemId),
859
+ });
860
+ break;
861
+ }
862
+ case "approve-unavailable-item-exception": {
863
+ const budgetItemId = getFlag(flags, "budgetItemId");
864
+ const reason = getFlag(flags, "reason");
865
+ if (!budgetItemId || !reason) {
866
+ throw new Error("approve-unavailable-item-exception requires --budgetItemId and --reason");
867
+ }
868
+ await approveUnavailableItemException({
869
+ budgetItemId: String(budgetItemId),
870
+ reason: String(reason),
871
+ });
872
+ break;
873
+ }
843
874
  case "reorder-budget-items": {
844
875
  const budgetId = positional[0];
845
876
  const itemIdsRaw = getFlag(flags, "itemIds");
@@ -1569,6 +1600,17 @@ export async function runCli(argv = process.argv, runtime = createProcessRuntime
1569
1600
  });
1570
1601
  break;
1571
1602
  }
1603
+ case "list-project-hubs": {
1604
+ await listProjectHubs({
1605
+ q: getFlag(flags, "q"),
1606
+ statuses: parseCommaSeparatedProjectStatuses(getFlag(flags, "status")),
1607
+ page: parsePositiveIntFlag(getFlag(flags, "page"), "--page"),
1608
+ perPage: parsePositiveIntFlag(getFlag(flags, "perPage"), "--perPage"),
1609
+ sortBy: parseOptionalProjectHubSortBy(getFlag(flags, "sortBy")),
1610
+ sortDir: parseOptionalBillListSortDir(getFlag(flags, "sortDir")),
1611
+ });
1612
+ break;
1613
+ }
1572
1614
  case "get-project": {
1573
1615
  const id = positional[0];
1574
1616
  if (!id)
@@ -1824,6 +1866,9 @@ export async function runCli(argv = process.argv, runtime = createProcessRuntime
1824
1866
  }
1825
1867
  case "list-items": {
1826
1868
  await listItems({
1869
+ includeQuickBooksDeactivationPending: flags.includeQuickBooksDeactivationPending === true
1870
+ ? true
1871
+ : parseOptionalBoolFlag(flags, "includeQuickBooksDeactivationPending"),
1827
1872
  name: getFlag(flags, "name"),
1828
1873
  page: parsePositiveIntFlag(getFlag(flags, "page"), "--page"),
1829
1874
  perPage: parsePositiveIntFlag(getFlag(flags, "perPage"), "--perPage"),
@@ -2008,8 +2053,14 @@ export async function runCli(argv = process.argv, runtime = createProcessRuntime
2008
2053
  if (!operationId) {
2009
2054
  throw new Error("retry-integration-operation requires <operationId>");
2010
2055
  }
2056
+ const outboundEmailResolution = getFlag(flags, "outboundEmailResolution");
2057
+ if (outboundEmailResolution &&
2058
+ outboundEmailResolution !== "ACCEPTED" &&
2059
+ outboundEmailResolution !== "NOT_ACCEPTED_RETRY") {
2060
+ throw new Error("--outboundEmailResolution must be ACCEPTED or NOT_ACCEPTED_RETRY");
2061
+ }
2011
2062
  await retryIntegrationOperation(operationId, parseOptionalBoolFlag(flags, "confirmExternalStateReconciled") ??
2012
- false);
2063
+ false, outboundEmailResolution, getFlag(flags, "providerMessageId"));
2013
2064
  break;
2014
2065
  }
2015
2066
  case "list-inbound-sources": {
@@ -78,6 +78,36 @@ export function parseOptionalExtendedProjectStatus(raw) {
78
78
  }
79
79
  return raw;
80
80
  }
81
+ export function parseCommaSeparatedProjectStatuses(raw) {
82
+ if (raw === undefined)
83
+ return undefined;
84
+ const statuses = raw
85
+ .split(",")
86
+ .map((value) => value.trim().toUpperCase())
87
+ .filter(Boolean);
88
+ if (statuses.length === 0)
89
+ return undefined;
90
+ for (const status of statuses) {
91
+ if (!PROJECT_STATUS_VALUES.has(status)) {
92
+ throw new Error(`Invalid project status "${status}". Use one of: ${[...PROJECT_STATUS_VALUES].join(", ")}.`);
93
+ }
94
+ }
95
+ return statuses;
96
+ }
97
+ const PROJECT_HUB_SORT_FIELDS = new Set([
98
+ "startDate",
99
+ "name",
100
+ "companyName",
101
+ "status",
102
+ ]);
103
+ export function parseOptionalProjectHubSortBy(raw) {
104
+ if (raw === undefined || raw === "")
105
+ return undefined;
106
+ if (!PROJECT_HUB_SORT_FIELDS.has(raw)) {
107
+ throw new Error(`Invalid --sortBy "${raw}". Use one of: ${[...PROJECT_HUB_SORT_FIELDS].join(", ")}.`);
108
+ }
109
+ return raw;
110
+ }
81
111
  export function parseOptionalDashboardRole(raw) {
82
112
  if (raw === undefined || raw === "")
83
113
  return undefined;
@@ -76,6 +76,10 @@ export const parseSendCustomerInvoiceToContactPersonPayload = (raw) => {
76
76
  /** Matches budget.createBudget — optional pipedriveDealId; Asana deal is project.asanaTaskId. */
77
77
  export function parseCreateBudgetPayload(raw) {
78
78
  const o = requireObject(raw, "create-budget payload");
79
+ if (o.unavailableItemReviewAcknowledged !== undefined &&
80
+ typeof o.unavailableItemReviewAcknowledged !== "boolean") {
81
+ throw new Error("create-budget payload.unavailableItemReviewAcknowledged must be a boolean when set.");
82
+ }
79
83
  return {
80
84
  name: requiredString(o.name, "create-budget payload.name"),
81
85
  budget: requiredString(o.budget, "create-budget payload.budget"),
@@ -83,6 +87,9 @@ export function parseCreateBudgetPayload(raw) {
83
87
  categoryId: requiredString(o.categoryId, "create-budget payload.categoryId"),
84
88
  projectId: requiredString(o.projectId, "create-budget payload.projectId"),
85
89
  sourceBudgetId: optionalString(o.sourceBudgetId),
90
+ ...(o.unavailableItemReviewAcknowledged !== undefined && {
91
+ unavailableItemReviewAcknowledged: o.unavailableItemReviewAcknowledged,
92
+ }),
86
93
  pipedriveDealId: optionalString(o.pipedriveDealId),
87
94
  };
88
95
  }
@@ -51,6 +51,7 @@ const mutationPrefixes = [
51
51
  "reconcile-",
52
52
  "reject-",
53
53
  "remove-",
54
+ "replace-",
54
55
  "rename-",
55
56
  "reorder-",
56
57
  "restore-",
@@ -149,6 +150,7 @@ const financialWriteTargets = new Set([
149
150
  "reject-customer-invoice",
150
151
  "restore-budget-item",
151
152
  "restore-budget-version",
153
+ "replace-budget-item",
152
154
  "sync-customer-invoice",
153
155
  "update-bill",
154
156
  "update-bill-payment-evidence",
@@ -195,6 +197,9 @@ const summaryFor = (legacyTarget) => {
195
197
  if (legacyTarget === "download-customer-invoice-pdf") {
196
198
  return "Download a customer invoice PDF when permitted.";
197
199
  }
200
+ if (legacyTarget === "send-customer-invoice-to-contact-person") {
201
+ return "Send a customer invoice and update the project billing email.";
202
+ }
198
203
  if (legacyTarget.startsWith("list-")) {
199
204
  return `List ${legacyTarget.slice("list-".length).replaceAll("-", " ")}.`;
200
205
  }
@@ -209,11 +214,11 @@ const aliasesFor = (legacyTarget, aliases) => [
209
214
  alias.replaceAll("-", "_"),
210
215
  ])),
211
216
  ];
212
- const legacyCommand = (legacyTarget, path, aliases = []) => ({
217
+ const legacyCommand = (legacyTarget, path, aliases = [], commandOptions = []) => ({
213
218
  path,
214
219
  legacyAliases: aliasesFor(legacyTarget, aliases),
215
220
  summary: summaryFor(legacyTarget),
216
- options: globalCommandOptions,
221
+ options: [...globalCommandOptions, ...commandOptions],
217
222
  effects: effectsFor(legacyTarget),
218
223
  legacyTarget,
219
224
  });
@@ -239,6 +244,13 @@ const registry = [
239
244
  legacyCommand("add-budget-items", ["budget", "item", "add"]),
240
245
  legacyCommand("update-budget-item", ["budget", "item", "update"]),
241
246
  legacyCommand("remove-budget-item", ["budget", "item", "remove"]),
247
+ legacyCommand("replace-budget-item", ["budget", "item", "replace"]),
248
+ legacyCommand("approve-unavailable-item-exception", [
249
+ "budget",
250
+ "item",
251
+ "exception",
252
+ "approve",
253
+ ]),
242
254
  legacyCommand("reorder-budget-items", ["budget", "item", "reorder"]),
243
255
  legacyCommand("update-budget-item-supplier", [
244
256
  "budget",
@@ -367,6 +379,23 @@ const registry = [
367
379
  legacyCommand("update-company", ["company", "update"]),
368
380
  legacyCommand("delete-company", ["company", "delete"]),
369
381
  legacyCommand("list-projects", ["project", "list"]),
382
+ legacyCommand("list-project-hubs", ["project", "hub", "list"], [], [
383
+ {
384
+ name: "--q",
385
+ description: "Search project and company names.",
386
+ },
387
+ {
388
+ name: "--status",
389
+ description: "Filter by comma-separated project statuses.",
390
+ },
391
+ { name: "--page", description: "Select a results page." },
392
+ { name: "--perPage", description: "Set the results page size." },
393
+ {
394
+ name: "--sortBy",
395
+ description: "Sort by startDate, name, companyName, or status.",
396
+ },
397
+ { name: "--sortDir", description: "Sort in asc or desc order." },
398
+ ]),
370
399
  legacyCommand("get-project", ["project", "get"]),
371
400
  legacyCommand("get-project-hub-status", ["project", "hub", "status"]),
372
401
  legacyCommand("setup-project-hub", ["project", "hub", "setup"]),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@go-labs-sg/bb",
3
- "version": "2.16.0",
3
+ "version": "2.18.0",
4
4
  "description": "Budget Builder CLI for AI agents — manage budgets, bills, claims, quotations, and customer invoices with explicit workflow previews for sensitive changes.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -53,7 +53,7 @@ Do not treat Inside Sales as Lead or Admin. Budget and supplier decisions still
53
53
 
54
54
  ### `LEAD`
55
55
 
56
- Apply the base resource rules, plus Lead-stage budget and supplier workflows. Lead-created suppliers are auto-approved, and Leads can handle budget/supplier approvals that the API assigns to them.
56
+ Apply the base resource rules, plus Lead-stage budget and supplier workflows. Lead-created suppliers are auto-approved, Leads can handle budget/supplier approvals that the API assigns to them, and Leads may approve an audited exception for an unavailable catalogue line after reviewing the exact estimate impact and reason.
57
57
 
58
58
  Do not use Lead credentials for Admin-only dashboards, user/service-identity administration, errors, integration retries, final bill payment, or admin customer-invoice actions.
59
59
 
@@ -76,6 +76,7 @@ Admin credentials may perform global and final-control operations, including:
76
76
  - Cross-user `bb user api-key create`, `bb user api-key list`, and `bb user api-key revoke` with `--userId`.
77
77
  - Global dashboard/performance, error-log, and integration-operation commands.
78
78
  - Admin-only supplier/category recovery and archive operations.
79
+ - Approving audited unavailable-item exceptions after confirming that replacement or removal is not appropriate.
79
80
  - Final bill approval/payment and Admin customer-invoice decisions/payment.
80
81
 
81
82
  Admin is not a force flag. State transitions, evidence, consistency checks, external-operation locks, and interactive confirmation rules still apply.
@@ -86,9 +87,11 @@ This table highlights the role-sensitive command groups an agent is most likely
86
87
 
87
88
  | Commands or action | Required identity condition |
88
89
  | --- | --- |
90
+ | `bb project hub list` | Any active authenticated role; the command is read-only and returns only active projects with a Project Hub integration. |
89
91
  | `bb auth whoami`; own `bb user api-key create`, `bb user api-key list`, and `bb user api-key revoke` | Any active role; omit `--userId` to act as the current user. |
90
92
  | Cross-user API-key commands; `bb user create` | `ADMIN`. |
91
93
  | Budget/project mutations, estimates, and budget attachments | `ADMIN`, budget creator, or project assignment as Business Development, Inside Sales, or Project Manager, depending on the endpoint. |
94
+ | `bb budget item exception approve` | `LEAD` or `ADMIN`; requires an unavailable line and a specific audited reason. |
92
95
  | `bb project hub status`, `bb project hub setup`, and `bb project hub sync` | `ADMIN`, project creator, or project assignment as Business Development, Inside Sales, or Project Manager. Setup and sync also enforce Project Hub lifecycle/readiness checks. |
93
96
  | Edit/delete a bill or quotation | Usually creator or `ADMIN`, with a supported status. |
94
97
  | `bb budget approve`, `bb budget reject`, `bb supplier approve`, and `bb supplier reject` | Current user must be the pending approver and currently have role `LEAD` or `ADMIN`. |