@go-labs-sg/bb 1.2.1 → 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.).
@@ -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;
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@go-labs-sg/bb",
3
- "version": "1.2.1",
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
  },