@go-labs-sg/bb 2.28.0 → 2.30.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
@@ -230,6 +230,10 @@ commands return a conflict until delivery becomes `SENT` or `FAILED`.
230
230
 
231
231
  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.
232
232
 
233
+ **Draft-only bills:** Use `bb bill create --draft-only --payload '<bill-json>' --allow-state-change --allow-financial-write` when the user authorizes a draft only. The equivalent payload field is `draftOnly: true`. This creates `DRAFT` for every role, without auto-checking, auto-approval, approval requests, emails, or QuickBooks writes. It cannot be combined with `alreadyPaid: true`; conflicting flag and payload values are rejected. Draft-only creation uses the dedicated `bill.createDraft` API route and fails on older servers without falling back to ordinary creation. Deploy backend support before releasing the updated CLI. Reconciled projects must be moved to Won separately before draft-only creation, because reopening a project can trigger external integrations. Completed projects retain their existing creation restriction.
234
+
235
+ Bill creation determines its required permissions from the parsed input, for both canonical and legacy commands. Ordinary unpaid creation requires state-change, financial-write, and external-write permissions, but no email permission. Already-paid creation also requires email permission. Without draft-only mode, existing role behavior remains: ordinary users create drafts, Accounting creates checked bills, and Admin creates approved bills and writes to QuickBooks. Ordinary creation can also adjust existing QuickBooks placeholder bills. Explicit drafts defer those adjustments until the later approval/QuickBooks workflow. When later authorized to submit the draft, use `bb bill approval create <billId>` with its state-change and email permissions; Admin-created drafts also enter finance review through this command. The static command catalog lists the union of possible effects.
236
+
233
237
  **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.
234
238
 
235
239
  **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.
@@ -337,3 +341,11 @@ bun run pack:artifact
337
341
  `pack:artifact` copies only the publish allowlist into a temporary staging directory and writes a sanitized publish manifest there. It never edits the source `package.json`, and the staged archive contains no workspace-only development dependencies. The release workflow packs once, validates the resulting archive, smoke-tests that exact tarball under Bun 1.4, then publishes that exact tarball. Types from `@go-labs/budget-builder-api` are compile-time only (`import type`). Prisma enum **values** used at runtime live in `src/prisma-enums.ts` (kept in sync with `packages/budget-builder/db` generated enums) so npm installs do not need `@go-labs/budget-builder-db`.
338
342
 
339
343
  The generated `command-reference.md` and `command-manifest.json` are release artifacts. Regenerate them whenever the command registry changes; the release workflow rejects a tarball that does not include both files.
344
+
345
+ ### Customer invoice lifecycle filters
346
+
347
+ `bb customer-invoice list --status SENT` filters by the displayed invoice lifecycle.
348
+ The list also accepts `PARTIALLY_SENT`, `PARTIALLY_PAID`, `PAID`, and `VOIDED`,
349
+ alongside existing approval states such as `APPROVED` and `PENDING_APPROVAL`.
350
+ `--sortBy status` sorts by lifecycle; `lifecycleStatus` in list/detail output is
351
+ separate from the stored approval/operational `status`.
@@ -2063,7 +2063,7 @@
2063
2063
  {
2064
2064
  "path": ["bill", "create"],
2065
2065
  "legacyAliases": ["create-bill", "create_bill"],
2066
- "summary": "Create a bill or claim with an approved supplier.",
2066
+ "summary": "Create a bill or claim with an approved supplier. --draft-only guarantees DRAFT without email or external writes; otherwise unpaid creation skips email permission but keeps external-write permission.",
2067
2067
  "globalOptions": [
2068
2068
  {
2069
2069
  "name": "--help",
@@ -2104,6 +2104,10 @@
2104
2104
  {
2105
2105
  "name": "--allow-financial-write",
2106
2106
  "description": "Allow financial-record changes in non-interactive use."
2107
+ },
2108
+ {
2109
+ "name": "--draft-only",
2110
+ "description": "Create a draft for every role; no approval, email, or QuickBooks writes. Conflicts with alreadyPaid: true."
2107
2111
  }
2108
2112
  ],
2109
2113
  "argumentMode": "legacy-passthrough",
@@ -347,7 +347,7 @@ Effects: state-change, email.
347
347
 
348
348
  ## `bb bill create`
349
349
 
350
- Create a bill or claim with an approved supplier.
350
+ Create a bill or claim with an approved supplier. --draft-only guarantees DRAFT without email or external writes; otherwise unpaid creation skips email permission but keeps external-write permission.
351
351
 
352
352
  Legacy aliases: `create-bill`, `create_bill`.
353
353
 
package/dist/index.js CHANGED
@@ -19681,6 +19681,14 @@ var ErrorStatus = {
19681
19681
  RESOLVED: "RESOLVED",
19682
19682
  IGNORED: "IGNORED"
19683
19683
  };
19684
+ var CustomerInvoiceBatchStatus = {
19685
+ CREATING: "CREATING",
19686
+ PENDING_APPROVAL: "PENDING_APPROVAL",
19687
+ APPROVED: "APPROVED",
19688
+ REJECTED_VOIDED: "REJECTED_VOIDED",
19689
+ EXPIRED_VOIDED: "EXPIRED_VOIDED",
19690
+ PARTIAL_QBO_FAILURE: "PARTIAL_QBO_FAILURE"
19691
+ };
19684
19692
  var BillStatus = {
19685
19693
  DRAFT: "DRAFT",
19686
19694
  PENDING_APPROVAL: "PENDING_APPROVAL",
@@ -19742,6 +19750,16 @@ var ExtendedErrorStatus = {
19742
19750
  ...ErrorStatus
19743
19751
  };
19744
19752
 
19753
+ // ../utils/src/customer-invoice-lifecycle.ts
19754
+ var customerInvoiceLifecycleStatuses = [
19755
+ ...Object.values(CustomerInvoiceBatchStatus),
19756
+ "PARTIALLY_SENT",
19757
+ "SENT",
19758
+ "PARTIALLY_PAID",
19759
+ "PAID",
19760
+ "VOIDED"
19761
+ ];
19762
+
19745
19763
  // src/prisma-enums.ts
19746
19764
  var ProjectStatus2 = {
19747
19765
  PITCH: "PITCH",
@@ -19757,14 +19775,6 @@ var SupplierQuotationStatus = {
19757
19775
  REJECTED: "REJECTED",
19758
19776
  SUPERSEDED: "SUPERSEDED"
19759
19777
  };
19760
- var CustomerInvoiceBatchStatus = {
19761
- CREATING: "CREATING",
19762
- PENDING_APPROVAL: "PENDING_APPROVAL",
19763
- APPROVED: "APPROVED",
19764
- REJECTED_VOIDED: "REJECTED_VOIDED",
19765
- EXPIRED_VOIDED: "EXPIRED_VOIDED",
19766
- PARTIAL_QBO_FAILURE: "PARTIAL_QBO_FAILURE"
19767
- };
19768
19778
  var BudgetStatus2 = {
19769
19779
  DRAFT: "DRAFT",
19770
19780
  PENDING_APPROVAL: "PENDING_APPROVAL",
@@ -19797,7 +19807,7 @@ var BUDGET_STATUS_VALUES = new Set(Object.values(BudgetStatus2));
19797
19807
  var BILL_STATUS_VALUES = new Set(Object.values(BillStatus2));
19798
19808
  var PROJECT_STATUS_VALUES = new Set(Object.values(ProjectStatus2));
19799
19809
  var SUPPLIER_QUOTATION_STATUS_VALUES = new Set(Object.values(SupplierQuotationStatus));
19800
- var CUSTOMER_INVOICE_BATCH_STATUS_VALUES = new Set(Object.values(CustomerInvoiceBatchStatus));
19810
+ var CUSTOMER_INVOICE_LIFECYCLE_STATUS_VALUES = new Set(customerInvoiceLifecycleStatuses);
19801
19811
  var USER_ROLE_VALUES = new Set(Object.values(UserRole));
19802
19812
  var EXTENDED_PROJECT_STATUS_VALUES = new Set(Object.values(ExtendedProjectStatus));
19803
19813
  var DASHBOARD_ROLE_VALUES = new Set([
@@ -20023,8 +20033,8 @@ function parseCommaSeparatedCustomerInvoiceStatuses(raw) {
20023
20033
  if (parts.length === 0)
20024
20034
  return;
20025
20035
  for (const status of parts) {
20026
- if (!CUSTOMER_INVOICE_BATCH_STATUS_VALUES.has(status)) {
20027
- throw new Error(`Invalid customer invoice status "${status}". Use one of: ${[...CUSTOMER_INVOICE_BATCH_STATUS_VALUES].join(", ")}.`);
20036
+ if (!CUSTOMER_INVOICE_LIFECYCLE_STATUS_VALUES.has(status)) {
20037
+ throw new Error(`Invalid customer invoice status "${status}". Use one of: ${[...CUSTOMER_INVOICE_LIFECYCLE_STATUS_VALUES].join(", ")}.`);
20028
20038
  }
20029
20039
  }
20030
20040
  return parts;
@@ -20249,7 +20259,7 @@ var billAttachment = (x, i, field = "attachments") => {
20249
20259
  size: Number(a.size)
20250
20260
  };
20251
20261
  };
20252
- function parseCreateBillPayload(raw) {
20262
+ function parseCreateBillPayload(raw, draftOnlyFlag) {
20253
20263
  const o = requireObject(raw, "create-bill payload");
20254
20264
  const isClaimable = o.isClaimable;
20255
20265
  if (typeof isClaimable !== "boolean") {
@@ -20261,7 +20271,18 @@ function parseCreateBillPayload(raw) {
20261
20271
  }
20262
20272
  const attachmentsRaw = o.attachments;
20263
20273
  const attachments = Array.isArray(attachmentsRaw) ? attachmentsRaw.map((x, i) => billAttachment(x, i)) : [];
20274
+ const draftOnlyRaw = o.draftOnly;
20275
+ if (draftOnlyRaw !== undefined && typeof draftOnlyRaw !== "boolean") {
20276
+ throw new Error("create-bill payload.draftOnly must be boolean.");
20277
+ }
20278
+ if (draftOnlyFlag !== undefined && draftOnlyRaw !== undefined && draftOnlyFlag !== draftOnlyRaw) {
20279
+ throw new Error("--draft-only conflicts with payload.draftOnly.");
20280
+ }
20281
+ const draftOnly = draftOnlyFlag ?? draftOnlyRaw;
20264
20282
  const alreadyPaidRaw = o.alreadyPaid;
20283
+ if (draftOnly && alreadyPaidRaw === true) {
20284
+ throw new Error("Draft-only creation cannot be combined with alreadyPaid: true.");
20285
+ }
20265
20286
  if (alreadyPaidRaw !== undefined && alreadyPaidRaw !== null && typeof alreadyPaidRaw !== "boolean") {
20266
20287
  throw new Error("create-bill payload.alreadyPaid must be boolean.");
20267
20288
  }
@@ -20305,6 +20326,7 @@ function parseCreateBillPayload(raw) {
20305
20326
  isClaimable,
20306
20327
  attachments,
20307
20328
  alreadyPaid: alreadyPaidRaw ?? undefined,
20329
+ draftOnly,
20308
20330
  paymentReference: optionalString(o.paymentReference),
20309
20331
  paymentProofAttachments,
20310
20332
  extractedSupplierName: optionalString(o.extractedSupplierName),
@@ -20317,8 +20339,12 @@ function parseCreateBillPayload(raw) {
20317
20339
  }
20318
20340
  function parseUpdateBillPayload(raw) {
20319
20341
  const o = requireObject(raw, "update-bill payload");
20342
+ const { draftOnly, ...input2 } = parseCreateBillPayload(raw);
20343
+ if (draftOnly !== undefined) {
20344
+ throw new Error("draftOnly is only supported when creating a bill.");
20345
+ }
20320
20346
  return {
20321
- ...parseCreateBillPayload(raw),
20347
+ ...input2,
20322
20348
  id: requiredString(o.id, "update-bill payload.id")
20323
20349
  };
20324
20350
  }
@@ -22020,9 +22046,24 @@ async function deleteItemCategoriesByIds(ids) {
22020
22046
  const result = await api2.itemCategory.deleteItemCategories.mutate({ ids });
22021
22047
  out(result);
22022
22048
  }
22023
- async function createBillFromPayload(raw) {
22024
- const input2 = parseCreateBillPayload(raw);
22025
- const result = await api2.bill.create.mutate(input2);
22049
+ async function createBillFromPayload(raw, draftOnlyFlag) {
22050
+ const input2 = parseCreateBillPayload(raw, draftOnlyFlag);
22051
+ await confirmCurrentCommand({
22052
+ action: input2.draftOnly ? "bill create (draft only)" : "bill create",
22053
+ target: `project ${input2.projectId}, supplier ${input2.supplierId}`,
22054
+ effects: [
22055
+ "state-change",
22056
+ "financial-write",
22057
+ ...input2.draftOnly ? [] : ["external-write"],
22058
+ ...input2.alreadyPaid ? ["email"] : []
22059
+ ],
22060
+ details: input2.draftOnly ? "Create a DRAFT bill without approval, email, or QuickBooks writes." : "Uses the caller's normal bill creation workflow; Accounting auto-checks and Admin auto-approves unpaid bills."
22061
+ });
22062
+ const result = input2.draftOnly ? await api2.bill.createDraft.mutate({
22063
+ ...input2,
22064
+ draftOnly: true,
22065
+ alreadyPaid: false
22066
+ }) : await api2.bill.create.mutate(input2);
22026
22067
  out(result);
22027
22068
  }
22028
22069
  async function listQuotations(opts) {
@@ -23179,7 +23220,7 @@ var summaryFor = (legacyTarget) => {
23179
23220
  if (legacyTarget === "whoami")
23180
23221
  return "Show the active API-key identity.";
23181
23222
  if (legacyTarget === "create-bill") {
23182
- return "Create a bill or claim with an approved supplier.";
23223
+ return "Create a bill or claim with an approved supplier. --draft-only guarantees DRAFT without email or external writes; otherwise unpaid creation skips email permission but keeps external-write permission.";
23183
23224
  }
23184
23225
  if (legacyTarget === "validate-bill-selection") {
23185
23226
  return "Validate the approved supplier and selected bill line items.";
@@ -23287,7 +23328,12 @@ var registry2 = [
23287
23328
  legacyCommand("list-bills", ["bill", "list"]),
23288
23329
  legacyCommand("list-claims", ["claim", "list"]),
23289
23330
  legacyCommand("create-bill-approval", ["bill", "approval", "create"]),
23290
- legacyCommand("create-bill", ["bill", "create"]),
23331
+ legacyCommand("create-bill", ["bill", "create"], [], [
23332
+ {
23333
+ name: "--draft-only",
23334
+ description: "Create a draft for every role; no approval, email, or QuickBooks writes. Conflicts with alreadyPaid: true."
23335
+ }
23336
+ ]),
23291
23337
  legacyCommand("validate-bill-selection", ["bill", "selection", "validate"]),
23292
23338
  legacyCommand("update-bill", ["bill", "update"]),
23293
23339
  legacyCommand("update-bill-payment-evidence", [
@@ -23833,7 +23879,7 @@ Bills
23833
23879
  isClaimable differentiates the shared bill/claim records: false = bill, true = claim.
23834
23880
  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.
23835
23881
  create-bill-approval <billId> (also queues approval request emails)
23836
- create-bill --payload '<json>' (bill.create; payload.isClaimable false = bill, true = claim; supplier must be APPROVED)
23882
+ create-bill --payload '<json>' [--draft-only] (bill.create; payload.isClaimable false = bill, true = claim; supplier must be APPROVED)
23837
23883
  validate-bill-selection --payload '<json>' Require an APPROVED supplier, then verify approved quotation links for each budgetItemId; alreadyPaid never bypasses quotation checks.
23838
23884
  stage-bill-attachment <projectId> <filePath...> [--file <path>] [--files <csv>] Upload files before create-bill; returns attachment JSON for attachments/paymentProofAttachments
23839
23885
  cleanup-staged-bill-attachments <projectId> --keys <csv> Delete unattached staged bill/claim uploads.
@@ -24318,7 +24364,7 @@ Waiting for approval\u2026
24318
24364
  }
24319
24365
  configureAccessToken(accessToken);
24320
24366
  }
24321
- if (!resolvedCommand.isLegacyAlias && resolvedCommand.command.effects.length > 0) {
24367
+ if (!resolvedCommand.isLegacyAlias && cmd !== "create-bill" && resolvedCommand.command.effects.length > 0) {
24322
24368
  await confirmCurrentCommand({
24323
24369
  action: resolvedCommand.command.path.join(" "),
24324
24370
  target: commandConfirmationTarget(resolvedCommand),
@@ -24685,7 +24731,8 @@ Waiting for approval\u2026
24685
24731
  throw new Error("create-bill requires --payload '<json>'");
24686
24732
  }
24687
24733
  const raw = parseJsonFlag(String(payloadRaw), "--payload");
24688
- await createBillFromPayload(raw);
24734
+ const draftOnly = flags["draft-only"] === true ? true : parseOptionalBoolFlag(flags, "draft-only");
24735
+ await createBillFromPayload(raw, draftOnly);
24689
24736
  break;
24690
24737
  }
24691
24738
  case "validate-bill-selection": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@go-labs-sg/bb",
3
- "version": "2.28.0",
3
+ "version": "2.30.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",
@@ -129,6 +129,8 @@ The following distinctions are especially important:
129
129
  - Project Hub setup and sync are asynchronous external writes. Read `bb project hub status <project-id>` first, obtain explicit approval, and request the mutation once instead of repeatedly queueing it. Setup readiness remains visible through `status`; commercial sync returns an outbox operation ID for Admin diagnostics.
130
130
  - Bill/claim status commands have separate role rules for `CHECKED`, `APPROVED`, and `PAID`.
131
131
  - `bb bill create --payload` uses `isClaimable=false`; `isClaimable=true` creates a claim. Claims intentionally permit some base-user flows that supplier bills do not.
132
+ - For a draft-only bill or claim, use `bb bill create --draft-only --payload` (or `draftOnly: true` in the payload). This forces `DRAFT` for every role and requires only `--allow-state-change --allow-financial-write`. It sends no approval/email and performs no QuickBooks writes. Do not combine it with `alreadyPaid: true`. An older API without `bill.createDraft` rejects the command; never retry with ordinary creation to bypass that error.
133
+ - Ordinary unpaid creation does not require `--allow-email`, but remains role-dependent: Accounting auto-checks and Admin auto-approves. It still requires `--allow-external-write`, including for possible placeholder adjustments. Already-paid creation retains the email gate.
132
134
  - Staged attachments belong to the user in the database. Do not reuse, share, or manually construct staged keys.
133
135
 
134
136
  ## Handling authorization failures