@go-labs-sg/bb 2.16.0 → 2.17.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,6 +183,10 @@ 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.
@@ -191,7 +195,7 @@ Every canonical command is classified by effect: `state-change`, `email`, `exter
191
195
 
192
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).
193
197
 
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.
198
+ **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
199
 
196
200
  **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.
197
201
 
@@ -207,11 +211,14 @@ For agent-driven invoice work, use this read-before-write sequence:
207
211
  Project Hub setup and commercial-document reconciliation are durable, asynchronous Budget Builder operations. Read status before requesting either mutation:
208
212
 
209
213
  ```bash
214
+ bb project hub list --q <project-or-company> --status WON,COMPLETED
210
215
  bb project hub status <project-id>
211
216
  bb project hub setup <project-id>
212
217
  bb project hub sync <project-id>
213
218
  ```
214
219
 
220
+ `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.
221
+
215
222
  `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
223
 
217
224
  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.
@@ -236,7 +243,7 @@ Project and budget query payloads keep the Asana identities explicit: `asanaTask
236
243
  | **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) |
237
244
  | **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
245
  | **Errors (admin)** | `get-recent-errors`, `get-error-metrics` |
239
- | **Automation (admin)** | `list-integration-operations`, `retry-integration-operation` |
246
+ | **Automation (admin)** | `list-integration-operations`, `retry-integration-operation` (Resend resolution requires `--confirmExternalStateReconciled true` plus `--outboundEmailResolution ACCEPTED\|NOT_ACCEPTED_RETRY`; include `--providerMessageId` when accepted) |
240
247
  | **Historical / benchmarks** | `get-approved-budgets`, `get-budget-category-benchmarks`, `get-item-pricing-history`, `get-supplier-pricing-history` |
241
248
 
242
249
  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.
@@ -4190,6 +4190,76 @@
4190
4190
  "effects": [],
4191
4191
  "legacyTarget": "list-projects"
4192
4192
  },
4193
+ {
4194
+ "path": ["project", "hub", "list"],
4195
+ "legacyAliases": ["list-project-hubs", "list_project_hubs"],
4196
+ "summary": "List project hubs.",
4197
+ "globalOptions": [
4198
+ {
4199
+ "name": "--help",
4200
+ "description": "Show help for this command."
4201
+ },
4202
+ {
4203
+ "name": "--quiet",
4204
+ "description": "Suppress non-error diagnostics."
4205
+ },
4206
+ {
4207
+ "name": "--debug",
4208
+ "description": "Emit sanitized diagnostic traces."
4209
+ },
4210
+ {
4211
+ "name": "--api-url",
4212
+ "description": "Override the Budget Builder API base URL."
4213
+ },
4214
+ {
4215
+ "name": "--allow-state-change",
4216
+ "description": "Allow a Budget Builder state change in non-interactive use."
4217
+ },
4218
+ {
4219
+ "name": "--allow-email",
4220
+ "description": "Allow sending email in non-interactive use."
4221
+ },
4222
+ {
4223
+ "name": "--allow-external-write",
4224
+ "description": "Allow writes to external systems in non-interactive use."
4225
+ },
4226
+ {
4227
+ "name": "--allow-delete",
4228
+ "description": "Allow deleting data in non-interactive use."
4229
+ },
4230
+ {
4231
+ "name": "--allow-financial-write",
4232
+ "description": "Allow financial-record changes in non-interactive use."
4233
+ },
4234
+ {
4235
+ "name": "--q",
4236
+ "description": "Search project and company names."
4237
+ },
4238
+ {
4239
+ "name": "--status",
4240
+ "description": "Filter by comma-separated project statuses."
4241
+ },
4242
+ {
4243
+ "name": "--page",
4244
+ "description": "Select a results page."
4245
+ },
4246
+ {
4247
+ "name": "--perPage",
4248
+ "description": "Set the results page size."
4249
+ },
4250
+ {
4251
+ "name": "--sortBy",
4252
+ "description": "Sort by startDate, name, companyName, or status."
4253
+ },
4254
+ {
4255
+ "name": "--sortDir",
4256
+ "description": "Sort in asc or desc order."
4257
+ }
4258
+ ],
4259
+ "argumentMode": "legacy-passthrough",
4260
+ "effects": [],
4261
+ "legacyTarget": "list-project-hubs"
4262
+ },
4193
4263
  {
4194
4264
  "path": ["project", "get"],
4195
4265
  "legacyAliases": ["get-project", "get_project"],
@@ -736,6 +736,14 @@ Legacy aliases: `list-projects`, `list_projects`.
736
736
 
737
737
  Effects: none.
738
738
 
739
+ ## `bb project hub list`
740
+
741
+ List project hubs.
742
+
743
+ Legacy aliases: `list-project-hubs`, `list_project_hubs`.
744
+
745
+ Effects: none.
746
+
739
747
  ## `bb project get`
740
748
 
741
749
  Get project.
package/dist/commands.js CHANGED
@@ -1091,7 +1091,7 @@ export async function createEstimate(budgetId, preflight) {
1091
1091
  const result = await api.quickbooks.createEstimate.mutate({ budgetId });
1092
1092
  out(result);
1093
1093
  }
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`;
1094
+ 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
1095
  export const sendEstimateToContactPersonFromPayload = async (raw) => {
1096
1096
  const input = parseSendEstimateToContactPersonPayload(raw);
1097
1097
  await assertSensitiveWorkflowConfirmed({
@@ -1907,6 +1907,14 @@ export async function listProjects(opts) {
1907
1907
  });
1908
1908
  out(result);
1909
1909
  }
1910
+ export const listProjectHubs = async (opts) => {
1911
+ const result = await api.project.getAllProjectHubs.query({
1912
+ ...opts,
1913
+ page: opts.page ?? 1,
1914
+ perPage: opts.perPage ?? 20,
1915
+ });
1916
+ out(result);
1917
+ };
1910
1918
  export async function getProject(id) {
1911
1919
  const [project, budgetsOverview, billsOverview] = await Promise.all([
1912
1920
  api.project.getProjectDetailsById.query({ id }),
@@ -2015,17 +2023,25 @@ export const listIntegrationOperations = async (input) => {
2015
2023
  const result = await api.integration.listOperations.query(input);
2016
2024
  out(result);
2017
2025
  };
2018
- export const retryIntegrationOperation = async (operationId, confirmExternalStateReconciled) => {
2026
+ export const retryIntegrationOperation = async (operationId, confirmExternalStateReconciled, outboundEmailResolution, providerMessageId) => {
2019
2027
  await assertSensitiveWorkflowConfirmed({
2020
- action: "Retry integration operation",
2028
+ action: outboundEmailResolution === "ACCEPTED"
2029
+ ? "Reconcile integration operation as completed"
2030
+ : "Retry integration operation",
2021
2031
  entity: `operation ${operationId}`,
2022
- details: confirmExternalStateReconciled
2023
- ? "after confirming the external state has been reconciled"
2024
- : undefined,
2032
+ details: outboundEmailResolution === "ACCEPTED"
2033
+ ? `mark provider-accepted email sent with message ID ${providerMessageId ?? "<missing>"}`
2034
+ : outboundEmailResolution === "NOT_ACCEPTED_RETRY"
2035
+ ? "rotate the Resend idempotency key and retry provider delivery"
2036
+ : confirmExternalStateReconciled
2037
+ ? "after confirming the external state has been reconciled"
2038
+ : undefined,
2025
2039
  });
2026
2040
  const result = await api.integration.retryOperation.mutate({
2027
2041
  operationId,
2028
2042
  confirmExternalStateReconciled,
2043
+ outboundEmailResolution,
2044
+ providerMessageId,
2029
2045
  });
2030
2046
  out(result);
2031
2047
  };
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, 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, 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
@@ -268,6 +271,8 @@ Companies & projects
268
271
  delete-company <id>
269
272
  list-projects [--companyId] [--name] [--status] [--active true|false] [--page] [--perPage]
270
273
  Omit --active to include both active and inactive projects.
274
+ list-project-hubs [--q] [--status CSV] [--page] [--perPage] [--sortBy startDate|name|companyName|status] [--sortDir asc|desc]
275
+ List active projects that have a Project Hub integration.
271
276
  get-project <id>
272
277
  Budget overview totalRevenue includes Estimate Accepted and Estimate Closed budgets only.
273
278
  create-project --name --companyId --contactPersonId --insideSalesId --businessDevelopmentId --venue --startDate <ISO> [--projectManagerId <userId>] [--asanaTaskId] [--asanaSearch <text>] [--slackChannelId --slackChannelUrl --slackChannelName] [--pax] [--endDate <ISO>] [--description]
@@ -331,7 +336,7 @@ Errors
331
336
 
332
337
  Automation (admin)
333
338
  list-integration-operations [--search] [--destination <provider,...>] [--status <PENDING|PROCESSING|FAILED|COMPLETED,...>] [--page] [--perPage]
334
- retry-integration-operation <operationId> [--confirmExternalStateReconciled true|false]
339
+ retry-integration-operation <operationId> [--confirmExternalStateReconciled true|false] [--outboundEmailResolution ACCEPTED|NOT_ACCEPTED_RETRY] [--providerMessageId <id>]
335
340
 
336
341
  Historical / benchmarks
337
342
  get-approved-budgets [--projectId] [--limit] [--minRevenue] [--maxRevenue]
@@ -1569,6 +1574,17 @@ export async function runCli(argv = process.argv, runtime = createProcessRuntime
1569
1574
  });
1570
1575
  break;
1571
1576
  }
1577
+ case "list-project-hubs": {
1578
+ await listProjectHubs({
1579
+ q: getFlag(flags, "q"),
1580
+ statuses: parseCommaSeparatedProjectStatuses(getFlag(flags, "status")),
1581
+ page: parsePositiveIntFlag(getFlag(flags, "page"), "--page"),
1582
+ perPage: parsePositiveIntFlag(getFlag(flags, "perPage"), "--perPage"),
1583
+ sortBy: parseOptionalProjectHubSortBy(getFlag(flags, "sortBy")),
1584
+ sortDir: parseOptionalBillListSortDir(getFlag(flags, "sortDir")),
1585
+ });
1586
+ break;
1587
+ }
1572
1588
  case "get-project": {
1573
1589
  const id = positional[0];
1574
1590
  if (!id)
@@ -2008,8 +2024,14 @@ export async function runCli(argv = process.argv, runtime = createProcessRuntime
2008
2024
  if (!operationId) {
2009
2025
  throw new Error("retry-integration-operation requires <operationId>");
2010
2026
  }
2027
+ const outboundEmailResolution = getFlag(flags, "outboundEmailResolution");
2028
+ if (outboundEmailResolution &&
2029
+ outboundEmailResolution !== "ACCEPTED" &&
2030
+ outboundEmailResolution !== "NOT_ACCEPTED_RETRY") {
2031
+ throw new Error("--outboundEmailResolution must be ACCEPTED or NOT_ACCEPTED_RETRY");
2032
+ }
2011
2033
  await retryIntegrationOperation(operationId, parseOptionalBoolFlag(flags, "confirmExternalStateReconciled") ??
2012
- false);
2034
+ false, outboundEmailResolution, getFlag(flags, "providerMessageId"));
2013
2035
  break;
2014
2036
  }
2015
2037
  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;
@@ -209,11 +209,11 @@ const aliasesFor = (legacyTarget, aliases) => [
209
209
  alias.replaceAll("-", "_"),
210
210
  ])),
211
211
  ];
212
- const legacyCommand = (legacyTarget, path, aliases = []) => ({
212
+ const legacyCommand = (legacyTarget, path, aliases = [], commandOptions = []) => ({
213
213
  path,
214
214
  legacyAliases: aliasesFor(legacyTarget, aliases),
215
215
  summary: summaryFor(legacyTarget),
216
- options: globalCommandOptions,
216
+ options: [...globalCommandOptions, ...commandOptions],
217
217
  effects: effectsFor(legacyTarget),
218
218
  legacyTarget,
219
219
  });
@@ -367,6 +367,23 @@ const registry = [
367
367
  legacyCommand("update-company", ["company", "update"]),
368
368
  legacyCommand("delete-company", ["company", "delete"]),
369
369
  legacyCommand("list-projects", ["project", "list"]),
370
+ legacyCommand("list-project-hubs", ["project", "hub", "list"], [], [
371
+ {
372
+ name: "--q",
373
+ description: "Search project and company names.",
374
+ },
375
+ {
376
+ name: "--status",
377
+ description: "Filter by comma-separated project statuses.",
378
+ },
379
+ { name: "--page", description: "Select a results page." },
380
+ { name: "--perPage", description: "Set the results page size." },
381
+ {
382
+ name: "--sortBy",
383
+ description: "Sort by startDate, name, companyName, or status.",
384
+ },
385
+ { name: "--sortDir", description: "Sort in asc or desc order." },
386
+ ]),
370
387
  legacyCommand("get-project", ["project", "get"]),
371
388
  legacyCommand("get-project-hub-status", ["project", "hub", "status"]),
372
389
  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.17.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",
@@ -86,6 +86,7 @@ This table highlights the role-sensitive command groups an agent is most likely
86
86
 
87
87
  | Commands or action | Required identity condition |
88
88
  | --- | --- |
89
+ | `bb project hub list` | Any active authenticated role; the command is read-only and returns only active projects with a Project Hub integration. |
89
90
  | `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
91
  | Cross-user API-key commands; `bb user create` | `ADMIN`. |
91
92
  | 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. |