@go-labs-sg/bb 1.2.0 → 1.2.2

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
@@ -56,8 +56,8 @@ Global options and flags use `--key=value` or `--key value` (see `bb help`).
56
56
 
57
57
  | Area | Commands (non-exhaustive) |
58
58
  | --- | --- |
59
- | **Budgets** | `list-budgets`, `get-budget`, `get-budget-items`, `get-budget-details`, `get-budget-categories`, `get-budget-versions`, `update-budget-status`, `create-budget` / `update-budget` (`--payload`), `delete-budget`, `create-budget-approval`, `add-budget-items`, `update-budget-item`, `remove-budget-item`, `reorder-budget-items`, `update-budget-item-supplier`, `create-budget-category`, `update-budget-category`, `delete-budget-category`, `update-budget-commission`, `update-budget-discount` (`--payload` where noted) |
60
- | **Bills** | `list-bills`, `list-claims`, `create-bill` (`--payload`), `update-bill` (`--payload`), `delete-bill`, `create-bill-approval`, `update-bill-status`, `patch-bill-payment`, `patch-bill-invoice-number`, `get-bill-attachments`, `get-bill-details` |
59
+ | **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`, `update-budget-status`, `create-budget` / `update-budget` (`--payload`), `delete-budget`, `create-budget-approval`, `add-budget-items`, `update-budget-item`, `remove-budget-item`, `reorder-budget-items`, `update-budget-item-supplier`, `create-budget-category`, `update-budget-category`, `delete-budget-category`, `update-budget-commission`, `update-budget-discount` (`--payload` where noted), `upload-budget-attachment` (`<budgetId>` + local file path; uses `attachment.requestBudgetAttachmentUpload` + PUT + `attachment.confirmBudgetAttachment`) |
60
+ | **Bills** | `list-bills`, `list-claims`, `create-bill` (`--payload`), `update-bill` (`--payload`), `delete-bill`, `create-bill-approval`, `update-bill-status`, `patch-bill-payment` (PAID bills: `--paymentTrackingUrl`, `--paymentReference`, `--quickbooksBillId`, `--paymentDate` ISO; clear with `--clearPaymentTrackingUrl` / `--clearPaymentReference` / `--clearQuickbooksBillId` / `--clearPaymentDate` `true`), `patch-bill-invoice-number`, `get-bill-attachments`, `upload-bill-attachment` (`<billId>` + local path), `get-bill-details` |
61
61
  | **Approvals** | `list-approvals` / `get-pending-approvals`, `approve-bill`, `reject-bill`, `approve-budget`, `reject-budget`, `approve-supplier`, `reject-supplier` |
62
62
  | **Companies & projects** | `list-companies`, `get-company`, `create-company`, `update-company` (`--payload`), `delete-company`, `list-projects`, `get-project`, `create-project` (optional `--requestQboAccountantNotification false` to skip QBO accountant emails), `update-project` (`--payload`, optional `requestQboAccountantNotification` in JSON), `delete-project`, `update-project-status` |
63
63
  | **Contacts** | `list-contacts`, `create-contact-person` (`--payload`), `update-contact-person` (`--payload`) |
@@ -70,6 +70,7 @@ Global options and flags use `--key=value` or `--key value` (see `bb help`).
70
70
 
71
71
  - **Stdout:** JSON (pretty-printed)
72
72
  - **Stderr:** Error objects as JSON on failure
73
+ - **Action logs (default on):** Each run logs to **stderr**: (1) JSON lines for command start, then `ok` or `error` with `durationMs` (`--payload` and similar flags are redacted); (2) tRPC lines for every API procedure (path, timing). **Stdout** stays JSON-only for piping (e.g. `| jq`). Suppress with `--quiet`, `-q`, or `BB_CLI_QUIET=1`.
73
74
  - **Exit code:** `0` on success, `1` on error
74
75
 
75
76
  A `.env` file in the **current working directory** is loaded automatically (for `BB_API_KEY`, etc.).
@@ -93,8 +94,9 @@ bun run src/index.ts list-budgets
93
94
 
94
95
  ### Layout
95
96
 
96
- - **`src/filter-enums.ts`** — “extended” filter enums (ALL + Prisma enums), maintained here so the published package does not depend on unpublished workspace packages. Keep in line with `packages/utils/src/filter-enums.ts` when those values change.
97
+ - **`src/prisma-enums.ts`** — Prisma enum string values for runtime; mirror `packages/db/src/generated/prisma/enums.ts` after schema changes.
98
+ - **`src/filter-enums.ts`** — “extended” filter enums (ALL + Prisma enums). Keep in line with `packages/utils/src/filter-enums.ts` when those values change.
97
99
 
98
100
  ### Publish
99
101
 
100
- `prepublishOnly` strips `workspace:` / `catalog:` entries from this package’s `package.json`, runs `tsc`, then `postpack` restores the file — so the npm tarball does not contain monorepo protocol dependencies. Types from `@bb/api` are compile-time only (`import type`). Enum **values** in emitted JS import `@bb/db/enums` (workspace `@bb/db` during monorepo development).
102
+ `prepublishOnly` strips `workspace:` / `catalog:` entries from this package’s `package.json`, runs `tsc`, then `postpack` restores the file — so the npm tarball does not contain monorepo protocol dependencies. Types from `@bb/api` are compile-time only (`import type`). Prisma enum **values** used at runtime live in `src/prisma-enums.ts` (kept in sync with `packages/db` generated enums) so npm installs do not need `@bb/db`.
@@ -1,5 +1,6 @@
1
- import { createTRPCProxyClient, httpBatchLink } from "@trpc/client";
1
+ import { createTRPCProxyClient, httpBatchLink, loggerLink } from "@trpc/client";
2
2
  import SuperJSON from "superjson";
3
+ import { shouldLogCliActions } from "./cli-trace.js";
3
4
  const BASE_URL = "https://budget-builder.getout.events";
4
5
  function getAuthHeader() {
5
6
  const key = process.env.BB_API_KEY;
@@ -9,6 +10,16 @@ function getAuthHeader() {
9
10
  }
10
11
  export const api = createTRPCProxyClient({
11
12
  links: [
13
+ loggerLink({
14
+ enabled: () => shouldLogCliActions(),
15
+ colorMode: "ansi",
16
+ withContext: false,
17
+ // tRPC defaults use console.log for requests; stderr keeps stdout JSON-safe for pipes.
18
+ console: {
19
+ log: (...args) => console.error(...args),
20
+ error: (...args) => console.error(...args),
21
+ },
22
+ }),
12
23
  httpBatchLink({
13
24
  url: `${BASE_URL}/api/trpc`,
14
25
  transformer: SuperJSON,
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Action logging for the CLI (default on).
3
+ * - Lifecycle: `logCliAction` in index (command + timing).
4
+ * - Per API call: tRPC `loggerLink` in api-client when `shouldLogCliActions()`.
5
+ * Logs go to stderr so stdout stays JSON for piping. Use `--quiet` / `-q` / BB_CLI_QUIET=1 to silence.
6
+ */
7
+ let cliQuietSession = false;
8
+ /** Call from main after `consumeCliQuietFlags`. */
9
+ export const setCliQuiet = (quiet) => {
10
+ cliQuietSession = quiet;
11
+ };
12
+ export const isCliQuiet = () => cliQuietSession || process.env.BB_CLI_QUIET === "1";
13
+ export const shouldLogCliActions = () => !isCliQuiet();
14
+ const SENSITIVE_FLAG = new Set([
15
+ "payload",
16
+ "apikey",
17
+ "bb_api_key",
18
+ "token",
19
+ "password",
20
+ "secret",
21
+ "authorization",
22
+ ]);
23
+ export const sanitizeFlagsForTrace = (flags) => {
24
+ const out = {};
25
+ for (const [k, v] of Object.entries(flags)) {
26
+ const lower = k.toLowerCase();
27
+ if (SENSITIVE_FLAG.has(lower) ||
28
+ lower.includes("secret") ||
29
+ lower.includes("password")) {
30
+ out[k] =
31
+ typeof v === "string" ? `[redacted ${v.length} chars]` : "[redacted]";
32
+ }
33
+ else {
34
+ out[k] = v;
35
+ }
36
+ }
37
+ return out;
38
+ };
39
+ export const consumeCliQuietFlags = (flags) => {
40
+ const fromEnv = process.env.BB_CLI_QUIET === "1";
41
+ const fromFlags = flags.quiet === true || flags.q === true;
42
+ delete flags.quiet;
43
+ delete flags.q;
44
+ return fromEnv || fromFlags;
45
+ };
46
+ /** stderr so stdout remains JSON-only for pipes (same intent as “log what the CLI is doing”). */
47
+ export const logCliAction = (record) => {
48
+ if (!shouldLogCliActions())
49
+ return;
50
+ console.error("[bb]", JSON.stringify({ ...record, ts: new Date().toISOString() }));
51
+ };
package/dist/commands.js CHANGED
@@ -1,3 +1,6 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { basename } from "node:path";
3
+ import { contentType } from "mime-types";
1
4
  import { api } from "./api-client.js";
2
5
  import { BudgetRole, Deals, ExtendedApprovalStatus, ExtendedApprovalType, ExtendedBudgetStatus, TimeFrame, } from "./filter-enums.js";
3
6
  import { billStatusesForApi, } from "./parse-cli-enums.js";
@@ -11,6 +14,11 @@ function isPendingRequest(n) {
11
14
  const out = (data) => {
12
15
  console.log(JSON.stringify(data, null, 2));
13
16
  };
17
+ /** Extension-based MIME, like browser `File.type` when the OS provides it */
18
+ const contentTypeHeaderForFileName = (fileName) => {
19
+ const ct = contentType(fileName);
20
+ return typeof ct === "string" ? ct : "application/octet-stream";
21
+ };
14
22
  export async function listBudgets(opts) {
15
23
  const input = {
16
24
  projectId: opts.projectId,
@@ -23,6 +31,7 @@ export async function listBudgets(opts) {
23
31
  sortDir: opts.sortDir,
24
32
  page: opts.page ?? 1,
25
33
  perPage: opts.perPage ?? 20,
34
+ includeDetails: opts.includeDetails ?? true,
26
35
  };
27
36
  const result = await api.budget.getAllBudgets.query(input);
28
37
  out(result);
@@ -141,12 +150,22 @@ export async function updateBillStatus(opts) {
141
150
  out(result);
142
151
  }
143
152
  export async function patchBillPayment(opts) {
144
- const result = await api.bill.patchPayment.mutate({
145
- id: opts.id,
146
- paymentTrackingUrl: opts.paymentTrackingUrl,
147
- paymentReference: opts.paymentReference,
148
- quickbooksBillId: opts.quickbooksBillId,
149
- });
153
+ const input = { id: opts.id };
154
+ if (opts.paymentTrackingUrl !== undefined)
155
+ input.paymentTrackingUrl = opts.paymentTrackingUrl;
156
+ if (opts.paymentReference !== undefined)
157
+ input.paymentReference = opts.paymentReference;
158
+ if (opts.quickbooksBillId !== undefined)
159
+ input.quickbooksBillId = opts.quickbooksBillId;
160
+ if (opts.paymentDate !== undefined) {
161
+ input.paymentDate =
162
+ opts.paymentDate === null
163
+ ? null
164
+ : typeof opts.paymentDate === "string"
165
+ ? new Date(opts.paymentDate)
166
+ : opts.paymentDate;
167
+ }
168
+ const result = await api.bill.patchPayment.mutate(input);
150
169
  out(result);
151
170
  }
152
171
  export async function patchBillInvoiceNumber(id, invoiceNumber) {
@@ -160,6 +179,56 @@ export async function getBillAttachments(id) {
160
179
  const result = await api.bill.getAttachments.query({ id });
161
180
  out(result);
162
181
  }
182
+ export async function uploadBillAttachmentFromPath(billId, filePath) {
183
+ const buf = await readFile(filePath);
184
+ const fileName = basename(filePath);
185
+ const size = buf.byteLength;
186
+ const { uploadUrl, key } = await api.attachment.requestBillAttachmentUpload.mutate({
187
+ billId,
188
+ fileName,
189
+ size,
190
+ });
191
+ const res = await fetch(uploadUrl, {
192
+ method: "PUT",
193
+ body: buf,
194
+ headers: { "Content-Type": contentTypeHeaderForFileName(fileName) },
195
+ });
196
+ if (!res.ok) {
197
+ throw new Error(`S3 upload failed: HTTP ${res.status} ${(await res.text()).slice(0, 500)}`);
198
+ }
199
+ const confirmed = await api.attachment.confirmBillAttachment.mutate({
200
+ billId,
201
+ key,
202
+ name: fileName,
203
+ size,
204
+ });
205
+ out(confirmed);
206
+ }
207
+ export async function uploadBudgetAttachmentFromPath(budgetId, filePath) {
208
+ const buf = await readFile(filePath);
209
+ const fileName = basename(filePath);
210
+ const size = buf.byteLength;
211
+ const { uploadUrl, key } = await api.attachment.requestBudgetAttachmentUpload.mutate({
212
+ budgetId,
213
+ fileName,
214
+ size,
215
+ });
216
+ const res = await fetch(uploadUrl, {
217
+ method: "PUT",
218
+ body: buf,
219
+ headers: { "Content-Type": contentTypeHeaderForFileName(fileName) },
220
+ });
221
+ if (!res.ok) {
222
+ throw new Error(`S3 upload failed: HTTP ${res.status} ${(await res.text()).slice(0, 500)}`);
223
+ }
224
+ const confirmed = await api.attachment.confirmBudgetAttachment.mutate({
225
+ budgetId,
226
+ key,
227
+ name: fileName,
228
+ size,
229
+ });
230
+ out(confirmed);
231
+ }
163
232
  export async function getBillDetails(id) {
164
233
  const result = await api.bill.getById.query({ id });
165
234
  out(result);
@@ -619,6 +688,7 @@ export async function getApprovedBudgets(opts) {
619
688
  ],
620
689
  page: 1,
621
690
  perPage,
691
+ includeDetails: false,
622
692
  });
623
693
  const items = (budgets ?? []).map((b) => {
624
694
  const calc = b;
@@ -1,4 +1,4 @@
1
- import { ApprovalStatus, ApprovalType, BillStatus, BudgetStatus, ErrorSeverity, ErrorStatus, EstimationMode, ProjectStatus, } from "@bb/db/enums";
1
+ import { ApprovalStatus, ApprovalType, BillStatus, BudgetStatus, ErrorSeverity, ErrorStatus, EstimationMode, ProjectStatus, } from "./prisma-enums.js";
2
2
  export const ExtendedApprovalStatus = {
3
3
  ...ApprovalStatus,
4
4
  ALL: "ALL",
package/dist/index.js CHANGED
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import "./load-env.js";
3
3
  import { requireApiKey } from "./api-client.js";
4
- import { addBudgetItems, approveBill, approveBudget, approveSupplier, createBillApproval, createBillFromPayload, createBudgetApproval, createBudgetCategory, createBudgetFromPayload, createCompany, createContactPersonFromPayload, createItemCategory, createItemFromPayload, createProject, createSupplierCertification, createSupplierFromPayload, createSupplierPaymentMethod, createSupplierRoleOption, createSupplierTagOption, deleteBillById, deleteBudgetById, deleteBudgetCategory, deleteCompanyById, deleteItemById, deleteItemCategoriesByIds, deleteProjectById, deleteSuppliersByIds, getApprovedBudgets, getBillAttachments, getBillDetails, getBudget, getBudgetCategories, getBudgetCategoryBenchmarks, getBudgetDetails, getBudgetItemsOnly, getBudgetVersions, getCompany, getDashboard, getErrorMetrics, getEstimatePerformance, getFinancialOverview, getItem, getItemPricingHistory, getMonthlyMetrics, getProject, getRecentErrors, getSupplierAnalytics, getSupplierDetails, getSupplierPricingHistory, getSystemOverview, getUserPerformance, listApprovals, listBills, listBudgets, listCompanies, listContacts, listItemCategories, listItems, listProjects, listSuppliers, listUsers, patchBillInvoiceNumber, patchBillPayment, rejectBill, rejectBudget, rejectSupplier, removeBudgetItem, reorderBudgetItemsCli, updateBillFromPayload, updateBillStatus, updateBudgetCategory, updateBudgetCommissionFromPayload, updateBudgetDiscountFromPayload, updateBudgetFromPayload, updateBudgetItem, updateBudgetItemSupplierCli, updateBudgetStatus, updateCompanyFromPayload, updateContactPersonFromPayload, updateItemCategory, updateItemFromPayload, updateProjectFromPayload, updateProjectStatus, updateSupplierFromPayload, } from "./commands.js";
4
+ import { consumeCliQuietFlags, logCliAction, sanitizeFlagsForTrace, setCliQuiet, shouldLogCliActions, } from "./cli-trace.js";
5
+ import { addBudgetItems, approveBill, approveBudget, approveSupplier, createBillApproval, createBillFromPayload, createBudgetApproval, createBudgetCategory, createBudgetFromPayload, createCompany, createContactPersonFromPayload, createItemCategory, createItemFromPayload, createProject, createSupplierCertification, createSupplierFromPayload, createSupplierPaymentMethod, createSupplierRoleOption, createSupplierTagOption, deleteBillById, deleteBudgetById, deleteBudgetCategory, deleteCompanyById, deleteItemById, deleteItemCategoriesByIds, deleteProjectById, deleteSuppliersByIds, getApprovedBudgets, getBillAttachments, getBillDetails, getBudget, getBudgetCategories, getBudgetCategoryBenchmarks, getBudgetDetails, getBudgetItemsOnly, getBudgetVersions, getCompany, getDashboard, getErrorMetrics, getEstimatePerformance, getFinancialOverview, getItem, getItemPricingHistory, getMonthlyMetrics, getProject, getRecentErrors, getSupplierAnalytics, getSupplierDetails, getSupplierPricingHistory, getSystemOverview, getUserPerformance, listApprovals, listBills, listBudgets, listCompanies, listContacts, listItemCategories, listItems, listProjects, listSuppliers, listUsers, patchBillInvoiceNumber, patchBillPayment, rejectBill, rejectBudget, rejectSupplier, removeBudgetItem, reorderBudgetItemsCli, updateBillFromPayload, updateBillStatus, updateBudgetCategory, updateBudgetCommissionFromPayload, updateBudgetDiscountFromPayload, updateBudgetFromPayload, updateBudgetItem, updateBudgetItemSupplierCli, updateBudgetStatus, updateCompanyFromPayload, updateContactPersonFromPayload, updateItemCategory, updateItemFromPayload, updateProjectFromPayload, updateProjectStatus, updateSupplierFromPayload, uploadBillAttachmentFromPath, uploadBudgetAttachmentFromPath, } from "./commands.js";
5
6
  import { getFlag, parseArgs } from "./parse-args.js";
6
7
  import { billStatusesForUpdateHelp, budgetStatusesForHelp, isBudgetStatusUpdate, parseApprovalTypeFlag, parseBillStatusForUpdate, parseCommaSeparatedBillStatuses, parseCommaSeparatedBudgetStatuses, parseCommaSeparatedIds, parseOptionalBillListSortBy, parseOptionalBillListSortDir, parseOptionalDashboardRole, parseOptionalDeals, parseOptionalErrorSeverity, parseOptionalErrorStatus, parseOptionalExtendedProjectStatus, parseOptionalFinancialRole, parseOptionalSupplierAnalyticsTimeFrame, parseOptionalTimeFrame, parseProjectStatusForUpdate, projectStatusesForHelp, } from "./parse-cli-enums.js";
7
8
  import { parseJsonFlag, parseOptionalNumber as parseOptNum, } from "./parse-json-flag.js";
@@ -62,10 +63,12 @@ bb — Budget Builder CLI (parity with MCP tools)
62
63
 
63
64
  Usage: bb <command> [options] [args]
64
65
 
66
+ Global: --quiet | -q | BB_CLI_QUIET=1 — hide action logs (default: log command + each tRPC call to stderr; stdout JSON unchanged).
67
+
65
68
  Auth: Set BB_API_KEY in the environment (same API key as MCP / Goracle).
66
69
 
67
70
  Budgets
68
- list-budgets [--projectId] [--name] [--status CSV] [--createdBy] [--dateFrom] [--dateTo] [--sortBy] [--sortDir] [--page] [--perPage]
71
+ list-budgets [--projectId] [--name] [--status CSV] [--createdBy] [--dateFrom] [--dateTo] [--sortBy] [--sortDir] [--page] [--perPage] [--summary]
69
72
  get-budget <id> Budget + line items
70
73
  get-budget-items <id> Line items only
71
74
  get-budget-details <id> Full detail (margins, suppliers, etc.)
@@ -87,19 +90,22 @@ Budgets
87
90
  delete-budget-category <id>
88
91
  update-budget-commission --payload '<json>' (budget.updateBudgetCommission)
89
92
  update-budget-discount --payload '<json>' (budget.updateBudgetDiscount; type percentage|amount)
93
+ upload-budget-attachment <budgetId> <filePath> Presigned S3 upload + confirm
90
94
 
91
95
  Bills
92
96
  list-bills [--projectId] [--budgetId] [--status CSV] [--search <text>] [--isClaimable true|false] [--createdByIds <csv>] [--sortBy createdAt|amount|status] [--sortDir asc|desc] [--page] [--pageSize]
93
97
  list-claims same flags as list-bills; only reimbursable claims (ignores --isClaimable)
94
98
  create-bill-approval <billId>
95
- create-bill --payload '<json>' (bill.create; attachments may be [])
99
+ create-bill --payload '<json>' (bill.create; use upload-bill-attachment after create to add files)
96
100
  update-bill --payload '<json>' (bill.update; must include id)
97
101
  delete-bill <billId>
98
102
  update-bill-status <id> <status> [--rejectionReason] [--paymentTrackingUrl] [--paymentReference]
99
103
  status (${billStatusesForUpdateHelp.join(", ")}) — not PENDING_APPROVAL; use create-bill-approval
100
- patch-bill-payment <billId> [--paymentTrackingUrl] [--paymentReference] [--quickbooksBillId]
104
+ patch-bill-payment <billId> [--paymentTrackingUrl] [--paymentReference] [--quickbooksBillId] [--paymentDate <ISO>]
105
+ Clear a field: --clearPaymentTrackingUrl true | --clearPaymentReference true | --clearQuickbooksBillId true | --clearPaymentDate true
101
106
  patch-bill-invoice-number <billId> <invoiceNumber>
102
107
  get-bill-attachments <billId>
108
+ upload-bill-attachment <billId> <filePath> Presigned S3 upload + confirm (same flow as web UI)
103
109
  get-bill-details <billId>
104
110
 
105
111
  Approvals
@@ -188,12 +194,23 @@ function normalizeCliCommand(raw) {
188
194
  }
189
195
  async function main() {
190
196
  const { command, positional, flags } = parseArgs(process.argv);
197
+ setCliQuiet(consumeCliQuietFlags(flags));
191
198
  const cmd = normalizeCliCommand(command ?? "");
192
199
  if (!cmd || cmd === "help" || flags.help === true) {
193
200
  printHelp();
194
201
  process.exit(0);
195
202
  }
196
203
  requireApiKey();
204
+ const t0 = Date.now();
205
+ if (shouldLogCliActions()) {
206
+ logCliAction({
207
+ event: "cli.command",
208
+ phase: "start",
209
+ command: cmd,
210
+ positional,
211
+ flags: sanitizeFlagsForTrace(flags),
212
+ });
213
+ }
197
214
  try {
198
215
  switch (cmd) {
199
216
  case "list-budgets": {
@@ -210,6 +227,9 @@ async function main() {
210
227
  sortDir: getFlag(flags, "sortDir"),
211
228
  page: parsePositiveIntFlag(getFlag(flags, "page"), "--page"),
212
229
  perPage: parsePositiveIntFlag(getFlag(flags, "perPage"), "--perPage"),
230
+ includeDetails: flags.summary === true
231
+ ? false
232
+ : parseOptionalBoolFlag(flags, "includeDetails") ?? true,
213
233
  });
214
234
  break;
215
235
  }
@@ -220,6 +240,14 @@ async function main() {
220
240
  await getBudget(id);
221
241
  break;
222
242
  }
243
+ case "upload-budget-attachment": {
244
+ const [budgetId, filePath] = positional;
245
+ if (!budgetId || !filePath) {
246
+ throw new Error("upload-budget-attachment requires <budgetId> <filePath>");
247
+ }
248
+ await uploadBudgetAttachmentFromPath(budgetId, filePath);
249
+ break;
250
+ }
223
251
  case "get-budget-items": {
224
252
  const id = positional[0];
225
253
  if (!id)
@@ -450,11 +478,60 @@ async function main() {
450
478
  const id = positional[0];
451
479
  if (!id)
452
480
  throw new Error("patch-bill-payment requires <billId>");
481
+ const paymentTrackingUrl = (() => {
482
+ const clear = parseOptionalBoolFlag(flags, "clearPaymentTrackingUrl");
483
+ const raw = getFlag(flags, "paymentTrackingUrl");
484
+ if (clear === true && raw !== undefined) {
485
+ throw new Error("Use either --paymentTrackingUrl or --clearPaymentTrackingUrl true, not both");
486
+ }
487
+ if (clear === true)
488
+ return null;
489
+ if (raw !== undefined)
490
+ return String(raw);
491
+ return undefined;
492
+ })();
493
+ const paymentReference = (() => {
494
+ const clear = parseOptionalBoolFlag(flags, "clearPaymentReference");
495
+ const raw = getFlag(flags, "paymentReference");
496
+ if (clear === true && raw !== undefined) {
497
+ throw new Error("Use either --paymentReference or --clearPaymentReference true, not both");
498
+ }
499
+ if (clear === true)
500
+ return null;
501
+ if (raw !== undefined)
502
+ return String(raw);
503
+ return undefined;
504
+ })();
505
+ const quickbooksBillId = (() => {
506
+ const clear = parseOptionalBoolFlag(flags, "clearQuickbooksBillId");
507
+ const raw = getFlag(flags, "quickbooksBillId");
508
+ if (clear === true && raw !== undefined) {
509
+ throw new Error("Use either --quickbooksBillId or --clearQuickbooksBillId true, not both");
510
+ }
511
+ if (clear === true)
512
+ return null;
513
+ if (raw !== undefined)
514
+ return String(raw);
515
+ return undefined;
516
+ })();
517
+ const paymentDate = (() => {
518
+ const clear = parseOptionalBoolFlag(flags, "clearPaymentDate");
519
+ const raw = getFlag(flags, "paymentDate");
520
+ if (clear === true && raw !== undefined) {
521
+ throw new Error("Use either --paymentDate or --clearPaymentDate true, not both");
522
+ }
523
+ if (clear === true)
524
+ return null;
525
+ if (raw !== undefined)
526
+ return String(raw);
527
+ return undefined;
528
+ })();
453
529
  await patchBillPayment({
454
530
  id,
455
- paymentTrackingUrl: getFlag(flags, "paymentTrackingUrl"),
456
- paymentReference: getFlag(flags, "paymentReference"),
457
- quickbooksBillId: getFlag(flags, "quickbooksBillId"),
531
+ paymentTrackingUrl,
532
+ paymentReference,
533
+ quickbooksBillId,
534
+ paymentDate,
458
535
  });
459
536
  break;
460
537
  }
@@ -473,6 +550,14 @@ async function main() {
473
550
  await getBillAttachments(id);
474
551
  break;
475
552
  }
553
+ case "upload-bill-attachment": {
554
+ const [billId, filePath] = positional;
555
+ if (!billId || !filePath) {
556
+ throw new Error("upload-bill-attachment requires <billId> <filePath>");
557
+ }
558
+ await uploadBillAttachmentFromPath(billId, filePath);
559
+ break;
560
+ }
476
561
  case "get-bill-details": {
477
562
  const id = positional[0];
478
563
  if (!id)
@@ -914,9 +999,26 @@ async function main() {
914
999
  throw new Error(`Unknown command: ${cmd}. Run 'bb help'.`);
915
1000
  }
916
1001
  }
1002
+ if (shouldLogCliActions()) {
1003
+ logCliAction({
1004
+ event: "cli.command",
1005
+ phase: "ok",
1006
+ command: cmd,
1007
+ durationMs: Date.now() - t0,
1008
+ });
1009
+ }
917
1010
  }
918
1011
  catch (err) {
919
1012
  const message = err instanceof Error ? err.message : String(err);
1013
+ if (shouldLogCliActions()) {
1014
+ logCliAction({
1015
+ event: "cli.command",
1016
+ phase: "error",
1017
+ command: cmd,
1018
+ durationMs: Date.now() - t0,
1019
+ error: message,
1020
+ });
1021
+ }
920
1022
  console.error(JSON.stringify({ error: message }));
921
1023
  process.exit(1);
922
1024
  }
@@ -35,6 +35,9 @@ export function parseArgs(argv) {
35
35
  }
36
36
  }
37
37
  }
38
+ else if (arg === "-q") {
39
+ flags.q = true;
40
+ }
38
41
  else {
39
42
  positional.push(arg);
40
43
  }
@@ -1,5 +1,5 @@
1
- import { BillStatus, BudgetStatus, ProjectStatus } from "@bb/db/enums";
2
1
  import { BudgetRole, Deals, ExtendedBillStatus, ExtendedErrorSeverity, ExtendedErrorStatus, ExtendedProjectStatus, TimeFrame, } from "./filter-enums.js";
2
+ import { BillStatus, BudgetStatus, ProjectStatus } from "./prisma-enums.js";
3
3
  const BUDGET_STATUS_VALUES = new Set(Object.values(BudgetStatus));
4
4
  const BILL_STATUS_VALUES = new Set(Object.values(BillStatus));
5
5
  const PROJECT_STATUS_VALUES = new Set(Object.values(ProjectStatus));
@@ -209,7 +209,7 @@ export function parseApprovalTypeFlag(raw) {
209
209
  }
210
210
  return u;
211
211
  }
212
- /** For help text / error messages (single import site for `@bb/db` enums in the CLI). */
212
+ /** For help text / error messages (values from `./prisma-enums.js`, synced with Prisma). */
213
213
  export const budgetStatusesForHelp = Object.values(BudgetStatus);
214
214
  export const billStatusesForHelp = Object.values(BillStatus);
215
215
  export const billStatusesForUpdateHelp = Object.values(BillStatus).filter((s) => s !== BillStatus.PENDING_APPROVAL);
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Runtime Prisma enum values the CLI needs. Mirrors
3
+ * `packages/db/src/generated/prisma/enums.ts` — keep in sync when the schema changes.
4
+ */
5
+ export const EstimationMode = {
6
+ MICRO: "MICRO",
7
+ MANPOWER_SPECIFIED: "MANPOWER_SPECIFIED",
8
+ };
9
+ export const ProjectStatus = {
10
+ NOT_STARTED: "NOT_STARTED",
11
+ IN_PROGRESS: "IN_PROGRESS",
12
+ COMPLETED: "COMPLETED",
13
+ CANCELLED: "CANCELLED",
14
+ };
15
+ export const BudgetStatus = {
16
+ DRAFT: "DRAFT",
17
+ PENDING_APPROVAL: "PENDING_APPROVAL",
18
+ APPROVED: "APPROVED",
19
+ REJECTED: "REJECTED",
20
+ ESTIMATE_CREATED: "ESTIMATE_CREATED",
21
+ ESTIMATE_SENT: "ESTIMATE_SENT",
22
+ ESTIMATE_ACCEPTED: "ESTIMATE_ACCEPTED",
23
+ ESTIMATE_REJECTED: "ESTIMATE_REJECTED",
24
+ ESTIMATE_CLOSED: "ESTIMATE_CLOSED",
25
+ };
26
+ export const ApprovalStatus = {
27
+ PENDING_APPROVAL: "PENDING_APPROVAL",
28
+ APPROVED: "APPROVED",
29
+ REJECTED: "REJECTED",
30
+ };
31
+ export const ApprovalType = {
32
+ BUDGET: "BUDGET",
33
+ SUPPLIER: "SUPPLIER",
34
+ BILL: "BILL",
35
+ };
36
+ export const ErrorSeverity = {
37
+ LOW: "LOW",
38
+ MEDIUM: "MEDIUM",
39
+ HIGH: "HIGH",
40
+ CRITICAL: "CRITICAL",
41
+ };
42
+ export const ErrorStatus = {
43
+ UNRESOLVED: "UNRESOLVED",
44
+ INVESTIGATING: "INVESTIGATING",
45
+ RESOLVED: "RESOLVED",
46
+ IGNORED: "IGNORED",
47
+ };
48
+ export const BillStatus = {
49
+ DRAFT: "DRAFT",
50
+ PENDING_APPROVAL: "PENDING_APPROVAL",
51
+ CHECKED: "CHECKED",
52
+ APPROVED: "APPROVED",
53
+ PAID: "PAID",
54
+ REJECTED: "REJECTED",
55
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@go-labs-sg/bb",
3
- "version": "1.2.0",
3
+ "version": "1.2.2",
4
4
  "description": "Budget Builder CLI — list budgets, bills, approvals, suppliers; approve bills; change status. For AI agents (e.g. Chuck/OpenClaw).",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -23,9 +23,12 @@
23
23
  "dependencies": {
24
24
  "@trpc/client": "^11.16.0",
25
25
  "dotenv": "^17.2.4",
26
+ "mime-types": "^3.0.2",
26
27
  "superjson": "^2.2.6"
27
28
  },
28
- "devDependencies": {},
29
+ "devDependencies": {
30
+ "@types/mime-types": "^3.0.1"
31
+ },
29
32
  "engines": {
30
33
  "node": ">=18"
31
34
  },
@@ -1,5 +0,0 @@
1
- import type { AppRouter } from "@bb/api";
2
- import { type CreateTRPCProxyClient } from "@trpc/client";
3
- export declare const api: CreateTRPCProxyClient<AppRouter>;
4
- export declare function requireApiKey(): string;
5
- //# sourceMappingURL=api-client.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"api-client.d.ts","sourceRoot":"","sources":["../src/api-client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACzC,OAAO,EAGN,KAAK,qBAAqB,EAC1B,MAAM,cAAc,CAAC;AAetB,eAAO,MAAM,GAAG,EAAE,qBAAqB,CAAC,SAAS,CAgB/C,CAAC;AAEH,wBAAgB,aAAa,IAAI,MAAM,CAOtC"}
@@ -1,28 +0,0 @@
1
- import type { BudgetStatus } from "@bb/db/enums";
2
- import type { ExtendedBudgetStatus } from "@bb/utils/filter-enums";
3
- export declare function listBudgets(opts: {
4
- projectId?: string;
5
- name?: string;
6
- statuses?: ExtendedBudgetStatus[];
7
- page?: number;
8
- perPage?: number;
9
- }): Promise<void>;
10
- export declare function getBudget(id: string): Promise<void>;
11
- export declare function updateBudgetStatus(budgetId: string, status: BudgetStatus): Promise<void>;
12
- export declare function listBills(opts: {
13
- projectId?: string;
14
- budgetId?: string;
15
- status?: string;
16
- page?: number;
17
- pageSize?: number;
18
- }): Promise<void>;
19
- export declare function approveBill(billId: string): Promise<void>;
20
- export declare function listApprovals(opts: {
21
- type?: string;
22
- }): Promise<void>;
23
- export declare function listSuppliers(opts: {
24
- name?: string;
25
- page?: number;
26
- perPage?: number;
27
- }): Promise<void>;
28
- //# sourceMappingURL=commands.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../src/commands.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAWnE,wBAAsB,WAAW,CAAC,IAAI,EAAE;IACvC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,oBAAoB,EAAE,CAAC;IAClC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB,GAAG,OAAO,CAAC,IAAI,CAAC,CAShB;AAED,wBAAsB,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAMzD;AAED,wBAAsB,kBAAkB,CACvC,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,YAAY,GAClB,OAAO,CAAC,IAAI,CAAC,CAMf;AAED,wBAAsB,SAAS,CAAC,IAAI,EAAE;IACrC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB,GAAG,OAAO,CAAC,IAAI,CAAC,CAehB;AAED,wBAAsB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAmB/D;AAED,wBAAsB,aAAa,CAAC,IAAI,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CA6C1E;AAED,wBAAsB,aAAa,CAAC,IAAI,EAAE;IACzC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB,GAAG,OAAO,CAAC,IAAI,CAAC,CAOhB"}
package/dist/index.d.ts DELETED
@@ -1,3 +0,0 @@
1
- #!/usr/bin/env node
2
- import "./load-env.js";
3
- //# sourceMappingURL=index.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,OAAO,eAAe,CAAC"}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=load-env.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"load-env.d.ts","sourceRoot":"","sources":["../src/load-env.ts"],"names":[],"mappings":""}
@@ -1,11 +0,0 @@
1
- /**
2
- * Minimal argv parser for bb CLI.
3
- * Supports: command [positional...] [--key=value] [--key value]
4
- */
5
- export declare function parseArgs(argv: string[]): {
6
- command: string;
7
- positional: string[];
8
- flags: Record<string, string | true>;
9
- };
10
- export declare function getFlag<T extends string | number>(flags: Record<string, string | true>, key: string, parse?: (s: string) => T): T | undefined;
11
- //# sourceMappingURL=parse-args.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"parse-args.d.ts","sourceRoot":"","sources":["../src/parse-args.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG;IAC1C,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC,CAAC;CACrC,CA8BA;AAED,wBAAgB,OAAO,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,EAChD,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC,EACpC,GAAG,EAAE,MAAM,EACX,KAAK,CAAC,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,CAAC,GACtB,CAAC,GAAG,SAAS,CAIf"}