@go-labs-sg/bb 2.47.0 → 3.0.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
@@ -24,9 +24,24 @@ For AI agents using this CLI: do not execute mutations until the user explicitly
24
24
 
25
25
  **Registry:** [`@go-labs-sg/bb`](https://www.npmjs.com/package/@go-labs-sg/bb)
26
26
 
27
+ ## Supplier cursor pages and independent totals
28
+
29
+ `bb supplier list` (alias `list-suppliers`) returns rich `items` and `pageInfo` with opaque `nextCursor`, `previousCursor`, and directional availability. Use `--limit 1-100` (default 10), then pass a returned token unchanged with the same filters and sort. Previous navigation uses `--direction backward --cursor <previousCursor>`. Retired `--page`/`--perPage` flags are rejected; numbered/random-access pages are no longer supported.
30
+
31
+ `--sortBy` accepts `name`, `email`, `phone`, `gstRegistered`, `status`, `active`, `createdAt`, or `updatedAt`; `--sortDir` accepts `asc`/`desc`. Alternatively, `--sort '[{"id":"status","desc":false},{"id":"name","desc":false}]'` preserves multiple-column precedence. Do not combine both forms. Missing or changed live status-sort anchors produce a restart error, not an empty success.
32
+
33
+ `bb supplier count` (alias `count-suppliers`) calls `supplier.getSummary` independently: exact total, self-excluded GST/approval/active facets and the existing active-scoped tag facets. It accepts the same filters, not pagination or sort flags. Both commands default to active suppliers; use `--active false` for archived suppliers.
34
+
35
+ ```sh
36
+ bb supplier list --limit 20 --name "ACME" --sortBy name --sortDir asc
37
+ bb supplier count --name "ACME"
38
+ ```
39
+
40
+ Roll out the API and maintained CLI together: `suppliers`/`pageCount` and offset inputs are replaced by `items`/`pageInfo`. The separate supplier analytics endpoint is unchanged.
41
+
27
42
  ## Bill and claim list summaries
28
43
 
29
- `bb bill list` / `bb claim list` (legacy `list-bills` / `list-claims`) use the updated summary-only `bill.getAll`. Lists return summary fields, `isClaimable`, `lineItemCount` and `hasPendingApproval`, plus the existing pagination, counts and metrics envelope. Filters, sorting and the CLI's existing summary output are unchanged. Lists do not return attachment, detailed line-item or approval arrays, full supplier/budget/project/company records, or detail-only payment/integration/verification metadata.
44
+ `bb bill list` / `bb claim list` (legacy `list-bills` / `list-claims`) read one bounded cursor page from summary-only `bill.getAll`: `--limit` (1–100, default 20), `--cursor` and `--direction forward|backward` return `{ items, pageInfo }`; follow `nextCursor`/`previousCursor` with the same filters and sort. `--page`, `--pageSize` and `--perPage` are rejected. Items carry summary fields, `isClaimable`, `lineItemCount` and `hasPendingApproval`. Totals, status facets (each ignoring its own status filter) and optional amount metrics come separately from `bb bill count` / `bb claim count` (`bill.getCounts`, `--includeMetrics true` for total/outstanding/pending-approval), which take filters only. Lists do not return attachment, detailed line-item or approval arrays, full supplier/budget/project/company records, or detail-only payment/integration/verification metadata.
30
45
 
31
46
  **Breaking API migration:** `bill.getList` is removed completely, with no alias or fallback. Direct scripts reading the former rich `bill.getAll` response must adopt summaries and fetch selected bill/claim details with `bb get-bill-details <billId>`. Do not fetch details for every list row unless the task actually needs them. No rich-list flag or automatic detail expansion is added.
32
47
 
@@ -34,9 +49,64 @@ Coordinate the matching API/web deployment and CLI release/installations. MCP is
34
49
 
35
50
  ## Inventory list reads
36
51
 
37
- `bb item list` / `bb list-items` now use the updated page-only `item.getItems` and separate `item.getItemCounts`. The JSON output remains `{ items, pageCount, counts }`; filters and pagination are unchanged. Neither request carries an acting-user cache ID. The server authenticates the CLI session or API-key owner as usual.
52
+ `bb item list` / `bb list-items` returns one cursor page of catalogue items, `{ items, pageInfo }`, newest first. Use `--limit` (1–100, default 20), `--cursor` and `--direction forward|backward`, keeping the same filters and sort; `--sortBy createdAt|updatedAt|name|cost|itemCategory|supplier` with `--sortDir asc|desc` picks the order. `--page` / `--perPage` are rejected. `bb item count` / `bb count-items` takes the same filters and returns `{ total, counts: { category, supplier } }`.
53
+
54
+ ## Error-log pagination
55
+
56
+ `bb error recent list` / `bb get-recent-errors` now return a bounded `{ items, pageInfo }` without running an exact count. Use `--limit 1..100` (default 10), `--cursor '<returned token>'` and `--direction forward|backward`. Pass `nextCursor` forward or `previousCursor` backward, unchanged and with identical `--severity`, `--status` and `--timeFrame` filters. Omit the cursor to restart at the newest row. Legacy `--page` and `--perPage` are rejected rather than silently ignored.
57
+
58
+ `bb error recent count` / `bb count-errors` return independent `{ totalCount }` with the same filters. API/CLI/MCP default to `all_time`; the web retains its `last_30_days` default. MCP tools are `get_recent_errors`, `count_errors`, and independently scoped `get_error_metrics`. All require current administrator authorization.
59
+
60
+ This updates the existing error-list contract in place. Coordinate API, web, CLI and MCP rollout; old numbered-page clients must upgrade. Retain the existing chronological index: CockroachDB secondary indexes already append the primary key, so an additional `ErrorLog(createdAt, id)` index would duplicate its key ordering. Filter-specific indexes require representative plan/workload evidence before rollout. No database changes, deployment, CLI publication or measured RU savings are implied.
61
+
62
+ ## Inbound source pagination
63
+
64
+ `bb inbound source list` / `bb list-inbound-sources` returns `{ sources, pageInfo }` without an exact count. Use `--limit 1..100` (default 25), `--cursor '<token>'`, and `--direction forward|backward`. Pass `nextCursor` forward or `previousCursor` backward without modifying it; omit the cursor to restart. Filters are `--search <name-or-form-id>`, `--processingEnabled true|false`, and `--processingMode ACTIVE|COLLECT_ONLY|SHADOW`.
65
+
66
+ `bb inbound source count` / `bb count-inbound-sources` returns independent `{ totalCount }` using the same filters, not pagination flags. Both commands remain Admin-only and Typeform-scoped. Legacy `--page`/`--perPage` and the numbered `pagination` envelope are removed; coordinate API/web and CLI upgrades. There is no silent offset fallback. Source order remains most recently updated first with an ID tie breaker; edits can move sources between live pages, so this is not a frozen export snapshot. No deployment, publication or measured RU savings are implied.
67
+
68
+ ## Inbound submission pagination
69
+
70
+ `bb inbound submission list [--sourceId <id>] [--limit 25] [--cursor '<token>']`
71
+ (legacy `list-inbound-submissions`) now returns `{ submissions, hasMore, nextCursor }`.
72
+ Pass `nextCursor` back unchanged, with the same source filter, until it is `null`.
73
+ Omit the cursor to start again at the newest submissions. Limits are 1–100.
74
+
75
+ **Breaking pagination migration:** `--page` and `--perPage` are rejected; the
76
+ `pagination.totalCount`/`totalPages` envelope is removed. Request
77
+ `bb inbound submission count [--sourceId <id>]` (legacy `count-inbound-submissions`)
78
+ only when a total is needed. It returns `{ totalCount }`, independently of the list.
79
+ Both commands remain Admin-only. Cursor browsing is not a frozen export snapshot.
80
+
81
+ Deploy the API/web together before publishing the matching CLI. Older CLI clients
82
+ must upgrade before using this changed list; there is no silent offset fallback.
83
+ The web uses previous/next navigation and caches the separate total for reuse
84
+ (60-second freshness, not polling); Refresh rechecks the current page and total.
85
+
38
86
 
39
- The existing `item.getItems` API is updated in place: it now returns an array rather than `{ items, pageCount, counts }`. There is no separate `getItemPage` procedure. Coordinate the API/web deployment and CLI release/installations: old clients expecting the combined response must be upgraded, and new clients require the revised response and separate counts. Direct integrations should fetch counts only when needed. This PR does not publish the CLI or deploy the API.
87
+ ## Company list and count pagination
88
+
89
+ `bb company list` / `bb list-companies` now call `company.getCompanies` and return the complete `{ items, pageInfo }` response. Filters are `--name`, `--createdBy`, `--createdByIds <csv>`, and `--active true|false`. Sorting is `--sortBy name|createdAt` (project-count sorting was removed) and `--sortDir asc|desc`; defaults are `createdAt`, `desc`, and `--limit 20` (limit must be an integer from 1 through 100).
90
+
91
+ Cursors are opaque and scoped to the active account, filters, sort, and page width. Pass `pageInfo.nextCursor` unchanged with `--direction forward`, or `pageInfo.previousCursor` unchanged with `--direction backward`; backward requires `--cursor`. For example:
92
+
93
+ ```bash
94
+ bb company list --limit 20
95
+ bb company list --limit 20 --cursor '<nextCursor-from-the-previous-response>' --direction forward
96
+ bb company list --limit 20 --cursor '<previousCursor-from-the-previous-response>' --direction backward
97
+ ```
98
+
99
+ `bb company count` / `bb count-companies` call `company.getCompanyCounts` separately and return `{ totalCount, counts: { active: { true, false } } }`. Counts accept filters only; list pagination and sorting flags are rejected. Use count when a total is needed rather than walking every list page.
100
+
101
+ This is a breaking replacement for published company list clients that send `page`/`perPage` or expect a numbered-page response. Deploy the matching API before publishing the CLI, upgrade **all old published page clients**, and do not add an unbounded legacy shim or an all-pages loop. Canonical commands and legacy aliases remain available during the migration: `bb company list` / `bb list-companies`, and `bb company count` / `bb count-companies`.
102
+
103
+ ## Project list and count pagination
104
+
105
+ `bb project list` / `bb list-projects` now call `project.getAllProjects` and return the complete `{ items, pageInfo }` response. Filters are `--companyId`, `--name`, legacy single-value `--status <status|ALL>`, optional `--statuses <csv>`, and `--active true|false`. Sorting is `--sortBy name|createdAt|status|companyName` and `--sortDir asc|desc`; defaults are `createdAt`, `desc`, and `--limit 20` (limit must be an integer from 1 through 100).
106
+
107
+ Cursors are opaque and scoped to the active account, filters, sort, and page width. Pass `pageInfo.nextCursor` unchanged with `--direction forward`, or `pageInfo.previousCursor` unchanged with `--direction backward`; backward requires `--cursor`. Legacy `--page` and `--perPage` are rejected rather than silently reinterpreted. For totals, use `bb project count` / `bb count-projects`, which call `project.getProjectCounts` independently and accept filters only; pagination and sorting flags are rejected. Counts return `{ totalCount, counts: { status, active: { true, false } } }`.
108
+
109
+ Deploy the matching API before publishing the CLI. Cursor browsing is not a frozen export snapshot; restart without the cursor when the API reports an expired or mismatched boundary.
40
110
 
41
111
  ## Company discovery and reuse
42
112
 
@@ -77,8 +147,14 @@ If `create-budget` reports an uncertain outcome (including Prisma P2028 or a net
77
147
 
78
148
  A failed `create-supplier` request may likewise have committed. Inspect `bb list-suppliers --name <name>` and matching supplier details before retrying; names alone do not identify one attempt. If supplier creation succeeded but its separate approval request failed or returned no approval IDs, inspect that supplier's approvals by ID and retry only the approval step if needed. Do not create the supplier again. The CLI confirms the combined supplier-and-approval workflow before the first write.
79
149
 
150
+ `bb supplier create` (also `bb create-supplier`) cannot bypass supplier archive permissions. A matching archived name requires an Admin; normal users and Leads receive an authorization error before reactivation or replacement of the approved details. Permission flags do not grant a higher role. Ask an Admin to run `bb supplier reactivate --ids <supplierId>`, then use `bb supplier update` for edits under the normal review policy. Admin same-name recreation retains the existing approval status and creator; a rejected supplier still needs a separate Admin/Lead re-approval. Deploy this shared API guard before publishing the updated CLI help; older CLI versions also use the guarded mutation.
151
+
80
152
  `create-budget-approval` and `create-estimate` accept `--companyPayload`, `--contactPayload`, and `--eventPayload` as JSON objects. The CLI rejects non-object JSON; the API validates required fields.
81
153
 
154
+ Budget approvals apply only to the current pending request: material edits and status changes supersede older pending requests. Refresh the budget and request approval again instead of retrying an old approval ID. `bb budget status update <id> APPROVED` requires a current Lead/Admin role; management permission, resource sharing and CLI permission flags do not grant approval authority. Existing Lead/Admin autoapproval remains available. If `bb quotation submit` or `bb quotation approve` conflicts after a concurrent ORIGINAL, amount or allocation change, refresh and review the current quotation before retrying. Both signing paths claim the captured revision and selection; a failed final approval claim rolls back the assigned decision, signed attachment and email/outbox writes. Deploy the shared API guards before publishing this CLI help. These fixes do not audit or repair historical decisions or signed documents.
155
+
156
+ `bb budget item supplier update --budgetItemId <id> --supplierId <id>` treats an eligible supplier/GST snapshot change as a material financial edit: it supersedes pending budget approvals and applies the standard budget/estimate reset policy. An identical supplier is a no-op. Refresh the budget and request approval again after an actual swap. Quotation signing also claims the exact selected allocation rows, so deleting an allocated budget item (including deletion during a budget restore) conflicts with an in-flight old submission or assigned final approval even if the parent quotation timestamp did not change. Existing capacity, bill-allocation deletion restrictions and attempt-specific cleanup are not bypassed.
157
+
82
158
  `create-project` requires an existing Asana Deals lead task. In non-interactive runs pass `--asanaTaskId`; interactive runs can select a match with `--asanaSearch`. The Slack channel is resolved from the task unless all Slack channel flags are provided. For a new direct client, create the lead task in the website form first.
83
159
 
84
160
  ## Shared catalogue editing
@@ -137,8 +213,11 @@ bunx @go-labs-sg/bb <command>
137
213
  Budget reads are active-only by default. The API enforces the historical-read permission; the CLI does not grant access based on the flag alone.
138
214
 
139
215
  ```bash
140
- # Active budgets only (default)
216
+ # Active budgets only (default); one cursor page of { items, pageInfo }
141
217
  bb list-budgets
218
+ bb list-budgets --limit 20 --cursor <nextCursor> --direction forward
219
+ # Totals plus status and active facets
220
+ bb count-budgets
142
221
  bb get-budget <budget-id>
143
222
 
144
223
  # Include inactive budgets; each form requires an ADMIN identity
@@ -149,6 +228,8 @@ bb get-budget-items <budget-id> --includeInactive
149
228
  bb get-budget-details <budget-id> --includeInactive
150
229
  ```
151
230
 
231
+ `get-budget` and `get-budget-items` return every line item of the budget (`{ items, truncated, totals }`), not a page. `truncated` is true only past the 1,000-line safety cap. `totals` (item count, cost, total cost, total price, actual spending, pending approval) comes from `budget.getBudgetItemTotals`.
232
+
152
233
  `--active false` lists inactive budgets only. `--active all` lists active and inactive budgets. `--includeInactive` allows the three budget/detail/item commands to read inactive history. If the authenticated identity is not an Admin, the API rejects the historical request before reading budget data. For `get-budget`, the CLI requests the budget and its line items concurrently; each API read independently enforces the same active-only default and Admin-only historical opt-in.
153
234
 
154
235
  ## Secure credential storage
@@ -178,6 +259,13 @@ credentials through `Bun.secrets`. It never stores Google credentials. Use
178
259
  `bb auth logout` to revoke the interactive session. For SSH or containers, pass
179
260
  `--no-browser` and open the printed URL on another device.
180
261
 
262
+ The **CLI Sessions** screen uses the same self/admin list-and-revoke flow as the
263
+ other Go Labs apps. Its typed API is `cliAuth.list({ userId? })` and
264
+ `cliAuth.revoke({ sessionId, userId? })`: omitting `userId` targets the authenticated
265
+ user; targeting another user requires a Budget Builder admin. Revocation affects
266
+ only that device's CLI session and tokens, not automation API keys. Existing
267
+ `bb auth login`, `status`, `whoami`, and `logout` commands are unchanged.
268
+
181
269
  Credentials use macOS Keychain, Windows Credential Manager, or Linux Secret Service, not plaintext files.
182
270
  The per-user lock directory contains only random ownership markers used to serialize
183
271
  login, refresh, and logout. macOS/Linux enforce owner-only POSIX permissions there;
@@ -296,7 +384,7 @@ Run `bb version` before agent-driven work to record the installed package versio
296
384
 
297
385
  **Authoritative command list:** run `bb help` or inspect the generated `command-reference.md` / `command-manifest.json`. The catalog includes every canonical route, global option, and effect classification, but does not filter by the current `whoami` role. During the handler migration, command-specific arguments retain the compatibility dispatcher's validation and are explicitly marked `legacy-passthrough` in the manifest; use `bb help --legacy` for their detailed transition reference. Version 2 uses grouped, resource-first commands such as `bb budget list` and `bb bill attachment upload`. Flat commands such as `bb list-budgets` and their legacy `snake_case` aliases remain available during the v2 transition, with a deprecation notice and an equivalent grouped command in help. The CLI calls the shared tRPC API, including procedures also used by the web UI.
298
386
 
299
- **Budget status automation:** Setting a budget to `ESTIMATE_ACCEPTED` requires a confirmed win-proof attachment. `--invoiceSettings '<json>'` remains available when the project needs saved delivery preferences, but invoice creation chooses its amount independently from those settings. If the parent project is `PITCH` or `LOST`, the API marks it `WON` automatically and runs enabled Asana automation. When rejecting the only accepted/closed budget on a commercial project, pass `--projectStatusOnCommercialRejection PITCH|LOST`.
387
+ **Budget status automation:** Setting a budget to `ESTIMATE_ACCEPTED` requires a confirmed win-proof attachment. `--invoiceSettings '<json>'` remains available when the project needs saved delivery preferences, but invoice creation chooses its amount independently from those settings. If the parent project is `PITCH` or `LOST`, the API marks it `WON` automatically, runs enabled Asana automation, and queues one project WON email to the BD, Inside Sales and PM with `ops@getout.sg` in CC (the same email is sent when `update-project-status` moves a `PITCH` or `LOST` project to `WON`). When rejecting the only accepted/closed budget on a commercial project, pass `--projectStatusOnCommercialRejection PITCH|LOST`.
300
388
 
301
389
  ### Typeform inbound administration
302
390
 
@@ -501,7 +589,11 @@ bb budget version list <budgetId> --all
501
589
  - `--all` explicitly collects all visible history through sequential bounded requests. It cannot be combined with `--cursor`; `--limit` controls each request's size. It can still be expensive and retains the complete result in memory. A failed page aborts without emitting a successful partial export.
502
590
  - Pages preserve newest-first `createdAt`/`id` ordering, role visibility, and creator/version-group metadata. They are not a point-in-time database snapshot; versions added after traversal starts require a fresh first-page read, and concurrent changes to visibility can affect traversal.
503
591
 
504
- The backend must expose `budgetVersion.getBudgetVersionsPage`. The legacy unbounded tRPC `getBudgetVersions` remains available but deprecated for external compatibility; unknown external clients are not silently capped. Removing that endpoint requires a separate usage/compatibility review. This change does not publish the CLI, deploy the backend, delete audit snapshots, or establish measured RU savings.
592
+ The canonical backend procedure is `budgetVersion.getBudgetVersions`. It now accepts `{ budgetId, limit?, cursor? }` and returns `{ versions, nextCursor }`, with a default page size of 20 and maximum of 50. The former budgetId-only input remains valid, but returns the first page—not a complete-history array. **This response-shape and completeness change is breaking despite keeping the route name.** Older installed CLI builds and external scripts must consume `versions` and explicitly continue through `nextCursor`; otherwise they may fail or mistake partial history for a complete export. Deprecated MCP builds may also be incompatible, but they must not be updated or used as a rollout gate. Current CLI aliases `get-budget-versions` and `get_budget_versions` remain supported.
593
+
594
+ For direct tRPC clients, call `getBudgetVersions.query({ budgetId, limit: 20, cursor })`, which returns `{ items, pageInfo: { hasNextPage, nextCursor } }` (list standardization); consume `items` and request the next page only when `pageInfo.nextCursor` is non-null. `cursor` is an opaque token bound to the budget and viewer role, not a version ID; a stale or mismatched token returns `BAD_REQUEST` and the export should restart. The CLI output keeps `{ versions, nextCursor }`. The intermediate `getBudgetVersionsPage` procedure is removed; migrate callers of that name to `getBudgetVersions` rather than retrying `NOT_FOUND`. A complete export must explicitly walk every page, abort on a failed page or repeated cursor, and emit success only after reaching the end; `bb budget version list <budgetId> --all` implements that traversal. Do not treat a first page as complete history. Cursors position by `createdAt` + `id`, so a deleted anchor version no longer breaks the next page; traversal is still not a snapshot, so versions written during an export may appear on later pages.
595
+
596
+ Coordinate the backend and maintained client rollout: publish/use this CLI only against a backend with the paginated `getBudgetVersions` contract; an older backend returns an array under that same name. MCP is deprecated and frozen, so do not update or publish it. Existing builds that call `getBudgetVersionsPage` must also migrate before that route is removed, including cached web bundles. The residual requester is not yet attributed: server timing logs prove route usage but do not identify its client or installed version. Reusing the route name bounds its history result; it does not prove complete-history consumers migrated, that a periodic schedule stopped, or that measured RU savings occurred. This source change does not publish clients, deploy the backend, delete audit snapshots or change indexes.
505
597
 
506
598
  ### Link an existing QuickBooks invoice
507
599
 
@@ -535,7 +627,7 @@ For `QBO_IMPORT` invoices, this existing command reads QBO and updates BB balanc
535
627
 
536
628
  The Seatrium example must be previewed against live QBO again before an operator imports it. Previously observed invoice numbers and paid/open balances are not authorization or current-state evidence.
537
629
 
538
- **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.
630
+ **Customer-invoice workflow parity:** `bb customer-invoice list` uses the same global/project cursor-page procedure as the web pages, and `bb customer-invoice count` returns the same totals, facets and metrics; 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.
539
631
 
540
632
  For agent-driven invoice work, use this read-before-write sequence:
541
633
 
@@ -575,17 +667,17 @@ Admin/Lead users can run `bb supplier approve <supplierId>` to directly re-appro
575
667
 
576
668
  | Area | Legacy handler labels (reference only; non-exhaustive) |
577
669
  | --- | --- |
578
- | **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`) |
579
- | **Bills / claims** | `list-bills` (`--isClaimable false` for bills, `--isClaimable true` for claims, omit for both), `list-claims` (claims only), `validate-bill-selection` (`--payload`, plus `--quotationIds q1,q2` or legacy `--quotationId q1`; validates the approved supplier and quotation coverage), `link-bill-quotations` (`<billId> --quotationIds q1,q2`; Admin source-link repair for eligible supplier bills), `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 and provide approved `quotationIds`, or use `--quotation-exempt` for an eligible server-validated exemption; set `isClaimable=true` for a claim without quotations; supplier bills require positive **before-GST** `extractedAmount` (full invoice subtotal) and `amount` (portion billed), with `amount <= extractedAmount`; line and quotation allocation amounts also exclude GST; 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), `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` |
580
- | **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`, `gstAmount`, and `lineAmounts` entries `{ budgetItemId, amount }` for multiple selected items), `update-quotation` (`--payload`; updates a `DRAFT` or `REJECTED` quotation), `delete-quotation`, `submit-quotation`, `approve-quotation`, `reject-quotation`, `download-quotation-pdf` (`original`, `staff`, or `final`) |
581
- | **Customer invoices** | `check-customer-invoice-readiness`, `list-eligible-customer-invoice-budgets`, `preview-customer-invoice-amount`, `list-available-qbo-customer-invoices`, `list-customer-invoices` (global/project/budget filters plus summary metrics; `--sortBy totalInvoiceAmount` sorts provider-confirmed totals including GST, with unknown amounts last), `get-customer-invoice`, `get-customer-invoice-email-context`, `preview-qbo-customer-invoice`, `import-qbo-customer-invoice` (BB-only link with reviewed token), `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` |
670
+ | **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` (forward cursor pages of active categories, name A–Z: `--q` / `--limit` / `--cursor`; returns `{ items, pageInfo }`), `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, adds 5 mandatory supplier subtasks ending in "Get bill and upload to Budget Builder", and sets no subtask due dates; `--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`) |
671
+ | **Bills / claims** | `list-bills` (`--isClaimable false` for bills, `--isClaimable true` for claims, omit for both), `list-claims` (claims only), `validate-bill-selection` (`--payload`, plus `--quotationIds q1,q2` or legacy `--quotationId q1`; validates the approved supplier and quotation coverage), `link-bill-quotations` (`<billId> --quotationIds q1,q2`; Admin source-link repair for eligible supplier bills), `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 and provide approved `quotationIds`, or use `--quotation-exempt` for an eligible server-validated exemption; set `isClaimable=true` for a claim without quotations; supplier bills require positive **before-GST** `extractedAmount` (full invoice subtotal) and `amount` (portion billed), with `amount <= extractedAmount`; line and quotation allocation amounts also exclude GST; 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), `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` (newest-first cursor pages: `--limit` 1–50, `--cursor`; returns `{ items, pageInfo }`), `upload-bill-attachment` (`<billId>` + local path), `get-bill-details` |
672
+ | **Quotations** | `list-quotations` (project, budget, supplier, status, requester and text-search filters; newest-first cursor pages via `--limit` / `--cursor` / `--direction` returning `{ items, pageInfo }`; `--page` / `--perPage` / `--pageSize` are rejected), `count-quotations` (`quotation.getCounts`: filters only, returns `totalCount`, status facets that ignore their own status filter, and `--includeMetrics true` amount metrics), `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`, `gstAmount`, and `lineAmounts` entries `{ budgetItemId, amount }` for multiple selected items), `update-quotation` (`--payload`; updates a `DRAFT` or `REJECTED` quotation), `delete-quotation`, `submit-quotation`, `approve-quotation`, `reject-quotation`, `download-quotation-pdf` (`original`, `staff`, or `final`) |
673
+ | **Customer invoices** | `check-customer-invoice-readiness`, `list-eligible-customer-invoice-budgets`, `preview-customer-invoice-amount`, `list-available-qbo-customer-invoices`, `list-customer-invoices` (global/project/budget filters; cursor pages via `--limit` / `--cursor` / `--direction` returning `{ items, pageInfo }`, `--page` / `--perPage` rejected; `--sortBy totalInvoiceAmount` sorts provider-confirmed totals including GST, with unknown amounts last), `count-customer-invoices` (`customerInvoice.getCounts`: filters only; `totalCount`, lifecycle status facets that ignore their own status filter, and amount metrics), `get-customer-invoice`, `get-customer-invoice-email-context`, `preview-qbo-customer-invoice`, `import-qbo-customer-invoice` (BB-only link with reviewed token), `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` |
582
674
  | **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` |
583
675
  | **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`) |
584
676
  | **Contacts** | `list-contacts`, `create-contact-person` (`--payload`), `update-contact-person` (`--payload`) |
585
- | **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) |
586
- | **Dashboard & users** | `whoami` (current authenticated 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) |
677
+ | **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; cursor pages: `--limit` / `--cursor` / `--direction`, optional `--name`, `--status` CSV, `--gstRegistered true|false`, `--createdBy`, `--timeFrame` (with `--startDate`/`--endDate` for `CUSTOM` only), `--sortBy name|gstRegistered|status|phone|email|updatedAt|createdAt`, `--sortDir`; `--page` / `--perPage` rejected; invalid filters are rejected instead of silently widening results), `count-supplier-analytics` (admin; `{ totalCount }` for the same filters), `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` (cursor pages of active categories: `--limit` / `--cursor` / `--direction`, optional `--name`, `--sortBy createdAt|name|itemCount|createdBy`, `--sortDir`; `--page` / `--perPage` rejected), `count-item-categories` (`{ totalCount }` for the same `--name`), `create-item-category` / `update-item-category` / `delete-item-categories` (admin; `--ids` CSV for delete) |
678
+ | **Dashboard & users** | `whoami` (current authenticated identity and role), `list-users` (forward cursor pages, name A–Z: `--limit` / `--cursor`, optional `--q` matching name or email; `--page` / `--perPage` rejected), `count-users` (`{ totalCount }` for the same `--q`), `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` (newest-first cursor pages: `--limit` 1–100, `--cursor`; returns `{ items, pageInfo }`; 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) |
587
679
  | **Errors (admin)** | `get-recent-errors`, `get-error-metrics` |
588
- | **Automation (admin)** | `list-integration-operations`, `retry-integration-operation` (Resend resolution requires `--confirmExternalStateReconciled true` plus `--outboundEmailResolution ACCEPTED\|NOT_ACCEPTED_RETRY`; include `--providerMessageId` when accepted) |
680
+ | **Automation (admin)** | `list-integration-operations` (cursor pages, newest first: `--limit` / `--cursor` / `--direction` with `--search`, `--destination`, `--status`; `--page` / `--perPage` rejected), `count-integration-operations` (`{ totalCount, filterOptions }` for the same filters), `get-integration-operation <operationId>` (who queued the job, what it is for, saved progress, Retry state), `reconcile-resource-operation <operationId>`, `retry-integration-operation` (QuickBooks jobs with a saved attempt check QuickBooks first and never resend an applied change; Resend resolution requires `--confirmExternalStateReconciled true` plus `--outboundEmailResolution ACCEPTED\|NOT_ACCEPTED_RETRY`; include `--providerMessageId` when accepted) |
589
681
  | **Historical / benchmarks** | `get-approved-budgets`, `get-budget-category-benchmarks`, `get-item-pricing-history`, `get-supplier-pricing-history` |
590
682
 
591
683
  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.
@@ -602,17 +694,22 @@ A `.env` file in the **current working directory** is loaded automatically for a
602
694
 
603
695
  ## Developing in this repo
604
696
 
605
- From the monorepo root (after `bun install`):
697
+ Follow the [development workflow](../../../docs/development-workflow.md#required-cli-coverage). App/API changes affecting `bb` must include CLI contracts, registry/help/docs and verification in the same change; do not update deprecated MCP.
698
+
699
+ From the monorepo root, after isolated worktree setup and client generation:
606
700
 
607
701
  ```bash
608
- export BB_API_KEY=...
609
- bun run packages/budget-builder/cli/src/index.ts budget list
702
+ bun --filter @go-labs-sg/bb typecheck
703
+ bun --filter @go-labs-sg/bb build
704
+ bun run packages/budget-builder/cli/src/index.ts help
610
705
  ```
611
706
 
707
+ Use approved browser authorization or an existing automation API key for commands that access data. Help/build examples do not authorize production reads, mutations, email or financial writes.
708
+
612
709
  Or from `packages/budget-builder/cli`:
613
710
 
614
711
  ```bash
615
- bun run build # emit dist/ via tsc
712
+ bun run build # generate command artifacts, then bundle dist/index.js with Bun
616
713
  bun run typecheck # tsc --noEmit
617
714
  bun run src/index.ts budget list
618
715
  ```
@@ -624,7 +721,7 @@ bun run src/index.ts budget list
624
721
 
625
722
  ### Publish
626
723
 
627
- Build the package first, then create its publishable archive with Bun:
724
+ Only prepare publication when authorized; building or packing is not permission to publish. From `packages/budget-builder/cli`, build the package first, then create its publishable archive with Bun:
628
725
 
629
726
  ```bash
630
727
  bun run build