@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 +12 -0
- package/command-manifest.json +5 -1
- package/command-reference.md +1 -1
- package/dist/index.js +68 -21
- package/package.json +1 -1
- package/role-aware-agent-guide.md +2 -0
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`.
|
package/command-manifest.json
CHANGED
|
@@ -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",
|
package/command-reference.md
CHANGED
|
@@ -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
|
|
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 (!
|
|
20027
|
-
throw new Error(`Invalid customer invoice status "${status}". Use one of: ${[...
|
|
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
|
-
...
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|