@go-labs-sg/bb 2.29.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.
@@ -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
@@ -20259,7 +20259,7 @@ var billAttachment = (x, i, field = "attachments") => {
20259
20259
  size: Number(a.size)
20260
20260
  };
20261
20261
  };
20262
- function parseCreateBillPayload(raw) {
20262
+ function parseCreateBillPayload(raw, draftOnlyFlag) {
20263
20263
  const o = requireObject(raw, "create-bill payload");
20264
20264
  const isClaimable = o.isClaimable;
20265
20265
  if (typeof isClaimable !== "boolean") {
@@ -20271,7 +20271,18 @@ function parseCreateBillPayload(raw) {
20271
20271
  }
20272
20272
  const attachmentsRaw = o.attachments;
20273
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;
20274
20282
  const alreadyPaidRaw = o.alreadyPaid;
20283
+ if (draftOnly && alreadyPaidRaw === true) {
20284
+ throw new Error("Draft-only creation cannot be combined with alreadyPaid: true.");
20285
+ }
20275
20286
  if (alreadyPaidRaw !== undefined && alreadyPaidRaw !== null && typeof alreadyPaidRaw !== "boolean") {
20276
20287
  throw new Error("create-bill payload.alreadyPaid must be boolean.");
20277
20288
  }
@@ -20315,6 +20326,7 @@ function parseCreateBillPayload(raw) {
20315
20326
  isClaimable,
20316
20327
  attachments,
20317
20328
  alreadyPaid: alreadyPaidRaw ?? undefined,
20329
+ draftOnly,
20318
20330
  paymentReference: optionalString(o.paymentReference),
20319
20331
  paymentProofAttachments,
20320
20332
  extractedSupplierName: optionalString(o.extractedSupplierName),
@@ -20327,8 +20339,12 @@ function parseCreateBillPayload(raw) {
20327
20339
  }
20328
20340
  function parseUpdateBillPayload(raw) {
20329
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
+ }
20330
20346
  return {
20331
- ...parseCreateBillPayload(raw),
20347
+ ...input2,
20332
20348
  id: requiredString(o.id, "update-bill payload.id")
20333
20349
  };
20334
20350
  }
@@ -22030,9 +22046,24 @@ async function deleteItemCategoriesByIds(ids) {
22030
22046
  const result = await api2.itemCategory.deleteItemCategories.mutate({ ids });
22031
22047
  out(result);
22032
22048
  }
22033
- async function createBillFromPayload(raw) {
22034
- const input2 = parseCreateBillPayload(raw);
22035
- 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);
22036
22067
  out(result);
22037
22068
  }
22038
22069
  async function listQuotations(opts) {
@@ -23189,7 +23220,7 @@ var summaryFor = (legacyTarget) => {
23189
23220
  if (legacyTarget === "whoami")
23190
23221
  return "Show the active API-key identity.";
23191
23222
  if (legacyTarget === "create-bill") {
23192
- 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.";
23193
23224
  }
23194
23225
  if (legacyTarget === "validate-bill-selection") {
23195
23226
  return "Validate the approved supplier and selected bill line items.";
@@ -23297,7 +23328,12 @@ var registry2 = [
23297
23328
  legacyCommand("list-bills", ["bill", "list"]),
23298
23329
  legacyCommand("list-claims", ["claim", "list"]),
23299
23330
  legacyCommand("create-bill-approval", ["bill", "approval", "create"]),
23300
- 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
+ ]),
23301
23337
  legacyCommand("validate-bill-selection", ["bill", "selection", "validate"]),
23302
23338
  legacyCommand("update-bill", ["bill", "update"]),
23303
23339
  legacyCommand("update-bill-payment-evidence", [
@@ -23843,7 +23879,7 @@ Bills
23843
23879
  isClaimable differentiates the shared bill/claim records: false = bill, true = claim.
23844
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.
23845
23881
  create-bill-approval <billId> (also queues approval request emails)
23846
- 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)
23847
23883
  validate-bill-selection --payload '<json>' Require an APPROVED supplier, then verify approved quotation links for each budgetItemId; alreadyPaid never bypasses quotation checks.
23848
23884
  stage-bill-attachment <projectId> <filePath...> [--file <path>] [--files <csv>] Upload files before create-bill; returns attachment JSON for attachments/paymentProofAttachments
23849
23885
  cleanup-staged-bill-attachments <projectId> --keys <csv> Delete unattached staged bill/claim uploads.
@@ -24328,7 +24364,7 @@ Waiting for approval\u2026
24328
24364
  }
24329
24365
  configureAccessToken(accessToken);
24330
24366
  }
24331
- if (!resolvedCommand.isLegacyAlias && resolvedCommand.command.effects.length > 0) {
24367
+ if (!resolvedCommand.isLegacyAlias && cmd !== "create-bill" && resolvedCommand.command.effects.length > 0) {
24332
24368
  await confirmCurrentCommand({
24333
24369
  action: resolvedCommand.command.path.join(" "),
24334
24370
  target: commandConfirmationTarget(resolvedCommand),
@@ -24695,7 +24731,8 @@ Waiting for approval\u2026
24695
24731
  throw new Error("create-bill requires --payload '<json>'");
24696
24732
  }
24697
24733
  const raw = parseJsonFlag(String(payloadRaw), "--payload");
24698
- await createBillFromPayload(raw);
24734
+ const draftOnly = flags["draft-only"] === true ? true : parseOptionalBoolFlag(flags, "draft-only");
24735
+ await createBillFromPayload(raw, draftOnly);
24699
24736
  break;
24700
24737
  }
24701
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.29.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