@go-labs-sg/bb 1.3.0 → 1.6.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
@@ -50,7 +50,7 @@ Global options and flags use `--key=value` or `--key value` (see `bb help`).
50
50
 
51
51
  **Authoritative command list:** run `bb help` — it includes every command, positional args, and flags. MCP exposes a subset of the same tRPC surface; the CLI additionally includes a few procedures mainly used by the web UI (e.g. `reorder-budget-items`, `update-budget-item-supplier`). You can also use MCP-style `snake_case` (e.g. `bb list_bills`); it is normalized to kebab-case.
52
52
 
53
- **Mutations with `--payload`:** Commands such as `create-budget`, `create-bill`, `update-supplier`, etc. take a single JSON object (`--payload '<json>'`) matching the corresponding tRPC procedure input. Use ISO strings for date/datetime fields; the CLI coerces them where needed. The API still validates the full shape.
53
+ **Mutations with `--payload`:** Commands such as `create-budget`, `create-bill`, `update-supplier`, etc. take a single JSON object (`--payload '<json>'`) matching the corresponding tRPC procedure input. Use ISO strings for date/datetime fields; the CLI coerces them where needed. The API still validates the full shape. For **`update-project`**, the project window is `dateRange.from` and `dateRange.to` (optional end); there are no separate event-date fields on the project payload. **`create-budget` / `update-budget`** do not accept `asanaTaskId`; configure the deal card on the project (`update-project` / project settings).
54
54
 
55
55
  ### Command overview
56
56
 
@@ -59,7 +59,7 @@ Global options and flags use `--key=value` or `--key value` (see `bb help`).
59
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` (also sends approval request emails), `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
60
  | **Bills** | `list-bills`, `list-claims`, `create-bill` (`--payload`), `update-bill` (`--payload`), `delete-bill`, `create-bill-approval` (also sends approval request emails), `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` (send reply email), `approve-budget` / `reject-budget` (send reply email), `approve-supplier` / `reject-supplier` (send reply email) |
62
- | **Companies & projects** | `list-companies`, `get-company`, `create-company`, `update-company` (`--payload`), `delete-company`, `list-projects`, `get-project`, `create-project` (`--asanaTaskId`, `--slackChannelId`, `--slackChannelUrl`, `--slackChannelName`, …; optional `--requestQboAccountantNotification false` to skip QBO accountant emails), `update-project` (`--payload`, optional `requestQboAccountantNotification` in JSON), `delete-project`, `update-project-status` (`<id>` `<status>`: `PITCH` \| `EVENT` \| `COMPLETED` \| `LOST`) |
62
+ | **Companies & projects** | `list-companies`, `get-company`, `create-company`, `update-company` (`--payload`), `delete-company`, `list-projects`, `get-project`, `create-project` (required: `--name`, `--companyId`, `--contactPersonId`, `--insideSalesId`, `--businessDevelopmentId`, `--venue`, `--pax`, `--asanaTaskId`, `--slackChannelId`, `--slackChannelUrl`, `--slackChannelName`, `--startDate` as ISO datetime for project/window start; optional `--endDate`; optional `--description`; optional `--requestQboAccountantNotification false` to skip QBO accountant emails), `update-project` (`--payload` with `dateRange.from` / `dateRange.to` for the project window; optional `requestQboAccountantNotification` in JSON), `delete-project`, `update-project-status` (`<id>` `<status>`: `PITCH` \| `WON` \| `COMPLETED` \| `LOST`; for `PITCH` → `WON` also pass `--projectManagerId` or `--projectManagerName`) |
63
63
  | **Contacts** | `list-contacts`, `create-contact-person` (`--payload`), `update-contact-person` (`--payload`) |
64
64
  | **Suppliers & items** | `list-suppliers`, `create-supplier` (`--payload`; when supplier status is `PENDING_APPROVAL`, also runs `supplier.createSupplierApproval` and `email.sendSupplierApprovalRequestEmail`), `update-supplier` (`--payload`; when supplier status is `PENDING_APPROVAL`, also runs `supplier.createSupplierApproval` and `email.sendSupplierApprovalRequestEmail`), `delete-suppliers` (`--ids` CSV; admin), `create-certification` / `create-payment-method` / `create-supplier-role` / `create-supplier-tag` (`--name`), `get-supplier-details`, `get-supplier-analytics`, `list-items`, `create-item` (`--payload`), `update-item` (`--payload`), `delete-item`, `get-item`, `list-item-categories`, `create-item-category`, `update-item-category`, `delete-item-categories` (`--ids` CSV; admin) |
65
65
  | **Dashboard & users** | `list-users`, `get-user-performance`, `get-dashboard`, `get-monthly-metrics`, `get-system-overview`, `get-estimate-performance`, `get-financial-overview` |
package/dist/commands.js CHANGED
@@ -14,6 +14,11 @@ function isPendingRequest(n) {
14
14
  const out = (data) => {
15
15
  console.log(JSON.stringify(data, null, 2));
16
16
  };
17
+ const combineDateAndTime = (day, time) => {
18
+ const d = new Date(day);
19
+ d.setHours(time.getHours(), time.getMinutes(), time.getSeconds(), 0);
20
+ return d;
21
+ };
17
22
  /** Extension-based MIME, like browser `File.type` when the OS provides it */
18
23
  const contentTypeHeaderForFileName = (fileName) => {
19
24
  const ct = contentType(fileName);
@@ -44,12 +49,81 @@ export async function getBudget(id) {
44
49
  out({ budget, items });
45
50
  }
46
51
  export async function updateBudgetStatus(budgetId, status) {
52
+ if (status === "ESTIMATE_ACCEPTED") {
53
+ throw new Error("Use mark-budget-won <budgetId> <filePath> so proof upload is included before setting ESTIMATE_ACCEPTED.");
54
+ }
47
55
  const result = await api.budget.updateBudgetStatus.mutate({
48
56
  id: budgetId,
49
57
  status,
50
58
  });
51
59
  out(result);
52
60
  }
61
+ const assertBudgetActionReadiness = async ({ budgetId, requireQuickbooksProject, actionLabel, }) => {
62
+ const budget = await api.budget.getBudget.query({ id: budgetId });
63
+ const hasNoItems = budget.budgetItems.length === 0;
64
+ if (hasNoItems) {
65
+ throw new Error(`Cannot ${actionLabel}: there are no line items in this budget.`);
66
+ }
67
+ const hasZeroCostItems = budget.budgetItems.some((item) => Number(item.cost ?? 0) === 0);
68
+ if (hasZeroCostItems) {
69
+ throw new Error(`Cannot ${actionLabel}: all line items must have a cost greater than zero.`);
70
+ }
71
+ const inactiveSuppliers = budget.budgetItems
72
+ .filter((item) => !item.supplier.active)
73
+ .map((item) => item.supplier.name);
74
+ const uniqueInactiveSuppliers = [...new Set(inactiveSuppliers)];
75
+ if (uniqueInactiveSuppliers.length > 0) {
76
+ throw new Error(`Cannot ${actionLabel}: inactive suppliers found (${uniqueInactiveSuppliers.join(", ")}).`);
77
+ }
78
+ if (requireQuickbooksProject && !budget.project.quickbooksProjectId) {
79
+ throw new Error("Cannot create estimate: project is not linked to QuickBooks yet.");
80
+ }
81
+ };
82
+ const runBudgetPreflight = async (budgetId, preflight) => {
83
+ if (!preflight)
84
+ return;
85
+ if (preflight.company) {
86
+ await api.company.updateCompany.mutate(preflight.company);
87
+ }
88
+ if (preflight.contactPerson) {
89
+ await api.contactPerson.updateContactPerson.mutate(preflight.contactPerson);
90
+ }
91
+ if (preflight.eventDetails) {
92
+ const budget = await api.budget.getBudget.query({ id: budgetId });
93
+ const project = await api.project.getProjectById.query({
94
+ id: budget.projectId,
95
+ });
96
+ const day = new Date(preflight.eventDetails.date);
97
+ const start = new Date(preflight.eventDetails.startTime);
98
+ const end = new Date(preflight.eventDetails.endTime);
99
+ const payload = {
100
+ id: project.id,
101
+ name: project.name,
102
+ description: project.description ?? undefined,
103
+ dateRange: {
104
+ from: combineDateAndTime(day, start),
105
+ to: combineDateAndTime(day, end),
106
+ },
107
+ companyId: project.companyId,
108
+ contactPersonId: project.contactPersonId,
109
+ asanaTaskId: project.asanaTaskId ?? "",
110
+ insideSalesId: project.insideSalesId ?? "",
111
+ businessDevelopmentId: project.businessDevelopmentId ?? "",
112
+ venue: project.venue ?? "",
113
+ pax: String(project.pax),
114
+ ...(project.slackChannelId &&
115
+ project.slackChannelUrl &&
116
+ project.slackChannelName
117
+ ? {
118
+ slackChannelId: project.slackChannelId,
119
+ slackChannelUrl: project.slackChannelUrl,
120
+ slackChannelName: project.slackChannelName,
121
+ }
122
+ : {}),
123
+ };
124
+ await api.project.updateProject.mutate(payload);
125
+ }
126
+ };
53
127
  export async function listBills(opts) {
54
128
  const input = {
55
129
  projectId: opts.projectId,
@@ -264,6 +338,38 @@ export async function uploadBudgetAttachmentFromPath(budgetId, filePath) {
264
338
  });
265
339
  out(confirmed);
266
340
  }
341
+ export async function uploadBudgetWinProofFromPath(budgetId, filePath) {
342
+ const buf = await readFile(filePath);
343
+ const fileName = basename(filePath);
344
+ const size = buf.byteLength;
345
+ const { uploadUrl, key } = await api.attachment.requestBudgetWinProofUpload.mutate({
346
+ budgetId,
347
+ fileName,
348
+ size,
349
+ });
350
+ const res = await fetch(uploadUrl, {
351
+ method: "PUT",
352
+ body: buf,
353
+ headers: { "Content-Type": contentTypeHeaderForFileName(fileName) },
354
+ });
355
+ if (!res.ok) {
356
+ throw new Error(`S3 upload failed: HTTP ${res.status} ${(await res.text()).slice(0, 500)}`);
357
+ }
358
+ await api.attachment.confirmBudgetWinProofAttachment.mutate({
359
+ budgetId,
360
+ key,
361
+ name: fileName,
362
+ size,
363
+ });
364
+ const result = await api.budget.updateBudgetStatus.mutate({
365
+ id: budgetId,
366
+ status: "ESTIMATE_ACCEPTED",
367
+ });
368
+ out(result);
369
+ }
370
+ export async function markBudgetWonWithProof(budgetId, filePath) {
371
+ await uploadBudgetWinProofFromPath(budgetId, filePath);
372
+ }
267
373
  export async function getBillDetails(id) {
268
374
  const result = await api.bill.getById.query({ id });
269
375
  out(result);
@@ -297,7 +403,13 @@ export async function getBudgetVersions(budgetId) {
297
403
  });
298
404
  out(versions);
299
405
  }
300
- export async function createBudgetApproval(budgetId) {
406
+ export async function createBudgetApproval(budgetId, preflight) {
407
+ await runBudgetPreflight(budgetId, preflight);
408
+ await assertBudgetActionReadiness({
409
+ budgetId,
410
+ requireQuickbooksProject: false,
411
+ actionLabel: "request approval",
412
+ });
301
413
  const result = await api.budget.createBudgetApproval.mutate({ budgetId });
302
414
  const approvalIds = (result.results ?? []).map((item) => ({ id: item.id }));
303
415
  let email;
@@ -322,6 +434,16 @@ export async function createBudgetApproval(budgetId) {
322
434
  email,
323
435
  });
324
436
  }
437
+ export async function createEstimate(budgetId, preflight) {
438
+ await runBudgetPreflight(budgetId, preflight);
439
+ await assertBudgetActionReadiness({
440
+ budgetId,
441
+ requireQuickbooksProject: true,
442
+ actionLabel: "create estimate",
443
+ });
444
+ const result = await api.quickbooks.createEstimate.mutate({ budgetId });
445
+ out(result);
446
+ }
325
447
  export async function getBudgetDetails(budgetId) {
326
448
  const detail = await api.budget.getBudgetDetail.query({ id: budgetId });
327
449
  out(detail);
@@ -762,6 +884,10 @@ export async function createProject(opts) {
762
884
  description: opts.description,
763
885
  companyId: opts.companyId,
764
886
  contactPersonId: opts.contactPersonId,
887
+ insideSalesId: opts.insideSalesId,
888
+ businessDevelopmentId: opts.businessDevelopmentId,
889
+ venue: opts.venue,
890
+ pax: opts.pax,
765
891
  asanaTaskId: opts.asanaTaskId,
766
892
  slackChannelId: opts.slackChannelId,
767
893
  slackChannelUrl: opts.slackChannelUrl,
@@ -776,8 +902,15 @@ export async function createProject(opts) {
776
902
  });
777
903
  out(result);
778
904
  }
779
- export async function updateProjectStatus(id, status) {
780
- const result = await api.project.updateProjectStatus.mutate({ id, status });
905
+ export async function updateProjectStatus(id, status, opts) {
906
+ const result = await api.project.updateProjectStatus.mutate({
907
+ id,
908
+ status,
909
+ ...(opts?.projectManagerId && { projectManagerId: opts.projectManagerId }),
910
+ ...(opts?.projectManagerName && {
911
+ projectManagerName: opts.projectManagerName,
912
+ }),
913
+ });
781
914
  out(result);
782
915
  }
783
916
  // --- Contacts ---
@@ -915,7 +1048,10 @@ export async function getApprovedBudgets(opts) {
915
1048
  const revenue = calc.totalSellingAfterDiscount ?? 0;
916
1049
  const cost = calc.totalCostWithoutGst ?? 0;
917
1050
  const gp = calc.gpPercentage ?? 0;
918
- const date = b.date instanceof Date ? b.date.toISOString() : String(b.date);
1051
+ const startDate = b.project.startDate;
1052
+ const date = startDate instanceof Date
1053
+ ? startDate.toISOString()
1054
+ : String(startDate ?? "");
919
1055
  const createdAt = b.createdAt instanceof Date
920
1056
  ? b.createdAt.toISOString()
921
1057
  : String(b.createdAt);
@@ -925,7 +1061,7 @@ export async function getApprovedBudgets(opts) {
925
1061
  categoryId: b.categoryId,
926
1062
  company: b.project?.company?.name,
927
1063
  date,
928
- pax: b.pax,
1064
+ pax: b.project.pax,
929
1065
  revenue: Number(revenue.toFixed(2)),
930
1066
  cost: Number(cost.toFixed(2)),
931
1067
  gpPercentage: Number(gp.toFixed(2)),
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import "./load-env.js";
3
3
  import { requireApiKey } from "./api-client.js";
4
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
+ import { addBudgetItems, approveBill, approveBudget, approveSupplier, createBillApproval, createBillFromPayload, createBudgetApproval, createBudgetCategory, createBudgetFromPayload, createCompany, createContactPersonFromPayload, createEstimate, 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, markBudgetWonWithProof, 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";
6
6
  import { getFlag, parseArgs } from "./parse-args.js";
7
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";
8
8
  import { parseJsonFlag, parseOptionalNumber as parseOptNum, } from "./parse-json-flag.js";
@@ -76,10 +76,12 @@ Budgets
76
76
  get-budget-versions <budgetId>
77
77
  update-budget-status <id> <status>
78
78
  status: ${budgetStatusesForHelp.join(", ")}
79
- create-budget --payload '<json>' (budget.createBudget; ISO dates for date/startTime/endTime)
79
+ mark-budget-won <budgetId> <filePath> Upload signed quote/PO proof and set status to ESTIMATE_ACCEPTED
80
+ create-budget --payload '<json>' (budget.createBudget; Asana deal card is on the project)
80
81
  update-budget --payload '<json>' (budget.updateBudget; must include id)
81
82
  delete-budget <budgetId>
82
83
  create-budget-approval <budgetId> (also sends approval request emails)
84
+ create-estimate <budgetId> (creates QuickBooks estimate; supports preflight updates)
83
85
  add-budget-items --budgetId --items '[{"itemId":"…","quantity":1,"markup":30},…]'
84
86
  update-budget-item --id <budgetItemId> [--description] [--note] [--quantity] [--markup] [--cost] [--unitPrice] [--isFreeOfCharge true|false] [--gstInclusive true|false]
85
87
  remove-budget-item <budgetItemId>
@@ -125,10 +127,11 @@ Companies & projects
125
127
  delete-company <id>
126
128
  list-projects [--companyId] [--name] [--status] [--page] [--perPage]
127
129
  get-project <id>
128
- create-project --name --companyId --contactPersonId --asanaTaskId --slackChannelId --slackChannelUrl --slackChannelName --startDate <ISO> [--endDate] [--description] [--requestQboAccountantNotification false]
130
+ create-project --name --companyId --contactPersonId --insideSalesId --businessDevelopmentId --venue --pax --asanaTaskId --slackChannelId --slackChannelUrl --slackChannelName --startDate <ISO> [--endDate <ISO>] [--description] [--requestQboAccountantNotification false]
129
131
  update-project --payload '<json>' (project.updateProject; optional requestQboAccountantNotification; default notify like web)
130
132
  delete-project <id>
131
- update-project-status <id> <status>
133
+ update-project-status <id> <status> [--projectManagerId <id>] [--projectManagerName <name>]
134
+ Required when moving PITCH → WON: supply --projectManagerId or --projectManagerName.
132
135
  status: ${projectStatusesForHelp.join(", ")}
133
136
 
134
137
  Contacts
@@ -248,6 +251,14 @@ async function main() {
248
251
  await uploadBudgetAttachmentFromPath(budgetId, filePath);
249
252
  break;
250
253
  }
254
+ case "mark-budget-won": {
255
+ const [budgetId, filePath] = positional;
256
+ if (!budgetId || !filePath) {
257
+ throw new Error("mark-budget-won requires <budgetId> <filePath>");
258
+ }
259
+ await markBudgetWonWithProof(budgetId, filePath);
260
+ break;
261
+ }
251
262
  case "get-budget-items": {
252
263
  const id = positional[0];
253
264
  if (!id)
@@ -288,7 +299,40 @@ async function main() {
288
299
  const id = positional[0];
289
300
  if (!id)
290
301
  throw new Error("create-budget-approval requires <budgetId>");
291
- await createBudgetApproval(id);
302
+ const companyPayloadRaw = getFlag(flags, "companyPayload");
303
+ const contactPayloadRaw = getFlag(flags, "contactPayload");
304
+ const eventPayloadRaw = getFlag(flags, "eventPayload");
305
+ await createBudgetApproval(id, {
306
+ company: companyPayloadRaw !== undefined
307
+ ? parseJsonFlag(String(companyPayloadRaw), "--companyPayload")
308
+ : undefined,
309
+ contactPerson: contactPayloadRaw !== undefined
310
+ ? parseJsonFlag(String(contactPayloadRaw), "--contactPayload")
311
+ : undefined,
312
+ eventDetails: eventPayloadRaw !== undefined
313
+ ? parseJsonFlag(String(eventPayloadRaw), "--eventPayload")
314
+ : undefined,
315
+ });
316
+ break;
317
+ }
318
+ case "create-estimate": {
319
+ const id = positional[0];
320
+ if (!id)
321
+ throw new Error("create-estimate requires <budgetId>");
322
+ const companyPayloadRaw = getFlag(flags, "companyPayload");
323
+ const contactPayloadRaw = getFlag(flags, "contactPayload");
324
+ const eventPayloadRaw = getFlag(flags, "eventPayload");
325
+ await createEstimate(id, {
326
+ company: companyPayloadRaw !== undefined
327
+ ? parseJsonFlag(String(companyPayloadRaw), "--companyPayload")
328
+ : undefined,
329
+ contactPerson: contactPayloadRaw !== undefined
330
+ ? parseJsonFlag(String(contactPayloadRaw), "--contactPayload")
331
+ : undefined,
332
+ eventDetails: eventPayloadRaw !== undefined
333
+ ? parseJsonFlag(String(eventPayloadRaw), "--eventPayload")
334
+ : undefined,
335
+ });
292
336
  break;
293
337
  }
294
338
  case "create-budget": {
@@ -691,6 +735,10 @@ async function main() {
691
735
  const name = getFlag(flags, "name");
692
736
  const companyId = getFlag(flags, "companyId");
693
737
  const contactPersonId = getFlag(flags, "contactPersonId");
738
+ const insideSalesId = getFlag(flags, "insideSalesId");
739
+ const businessDevelopmentId = getFlag(flags, "businessDevelopmentId");
740
+ const venue = getFlag(flags, "venue");
741
+ const pax = getFlag(flags, "pax");
694
742
  const asanaTaskId = getFlag(flags, "asanaTaskId");
695
743
  const slackChannelId = getFlag(flags, "slackChannelId");
696
744
  const slackChannelUrl = getFlag(flags, "slackChannelUrl");
@@ -699,17 +747,25 @@ async function main() {
699
747
  if (!name ||
700
748
  !companyId ||
701
749
  !contactPersonId ||
750
+ !insideSalesId ||
751
+ !businessDevelopmentId ||
752
+ !venue ||
753
+ !pax ||
702
754
  !asanaTaskId ||
703
755
  !slackChannelId ||
704
756
  !slackChannelUrl ||
705
757
  !slackChannelName ||
706
758
  !startDate) {
707
- throw new Error("create-project requires --name --companyId --contactPersonId --asanaTaskId --slackChannelId --slackChannelUrl --slackChannelName --startDate (ISO)");
759
+ throw new Error("create-project requires --name --companyId --contactPersonId --insideSalesId --businessDevelopmentId --venue --pax --asanaTaskId --slackChannelId --slackChannelUrl --slackChannelName --startDate (ISO/datetime flags as appropriate); optional --endDate");
708
760
  }
709
761
  await createProject({
710
762
  name,
711
763
  companyId,
712
764
  contactPersonId,
765
+ insideSalesId,
766
+ businessDevelopmentId,
767
+ venue,
768
+ pax,
713
769
  asanaTaskId,
714
770
  slackChannelId,
715
771
  slackChannelUrl,
@@ -742,7 +798,12 @@ async function main() {
742
798
  if (!id || !statusRaw) {
743
799
  throw new Error("update-project-status requires <id> <status>");
744
800
  }
745
- await updateProjectStatus(id, parseProjectStatusForUpdate(statusRaw));
801
+ const pmIdFlag = getFlag(flags, "projectManagerId");
802
+ const pmNameFlag = getFlag(flags, "projectManagerName");
803
+ await updateProjectStatus(id, parseProjectStatusForUpdate(statusRaw), {
804
+ ...(pmIdFlag && { projectManagerId: String(pmIdFlag) }),
805
+ ...(pmNameFlag && { projectManagerName: String(pmNameFlag) }),
806
+ });
746
807
  break;
747
808
  }
748
809
  case "list-contacts": {
@@ -33,23 +33,15 @@ function requiredString(value, fieldLabel) {
33
33
  }
34
34
  return t;
35
35
  }
36
- /** Matches budget.createBudget — dates may be ISO strings in JSON. */
36
+ /** Matches budget.createBudget — optional pipedriveDealId; Asana deal is project.asanaTaskId. */
37
37
  export function parseCreateBudgetPayload(raw) {
38
38
  const o = requireObject(raw, "create-budget payload");
39
39
  return {
40
40
  name: requiredString(o.name, "create-budget payload.name"),
41
- date: toDate(o.date, "date"),
42
- startTime: toDate(o.startTime, "startTime"),
43
- endTime: toDate(o.endTime, "endTime"),
44
- pax: requiredString(o.pax, "create-budget payload.pax"),
45
- venue: requiredString(o.venue, "create-budget payload.venue"),
46
41
  budget: requiredString(o.budget, "create-budget payload.budget"),
47
42
  paymentTerm: requiredString(o.paymentTerm, "create-budget payload.paymentTerm"),
48
43
  categoryId: requiredString(o.categoryId, "create-budget payload.categoryId"),
49
44
  projectId: requiredString(o.projectId, "create-budget payload.projectId"),
50
- insideSalesId: requiredString(o.insideSalesId, "create-budget payload.insideSalesId"),
51
- businessDevelopmentId: requiredString(o.businessDevelopmentId, "create-budget payload.businessDevelopmentId"),
52
- asanaTaskId: requiredString(o.asanaTaskId, "create-budget payload.asanaTaskId"),
53
45
  sourceBudgetId: optionalString(o.sourceBudgetId),
54
46
  pipedriveDealId: optionalString(o.pipedriveDealId),
55
47
  };
@@ -200,6 +192,18 @@ export function parseUpdateProjectPayload(raw) {
200
192
  else if (notification !== undefined && notification !== null) {
201
193
  throw new Error("update-project payload.requestQboAccountantNotification must be a boolean when set.");
202
194
  }
195
+ const slackChannelId = optionalString(o.slackChannelId);
196
+ const slackChannelUrl = optionalString(o.slackChannelUrl);
197
+ const slackChannelName = optionalString(o.slackChannelName);
198
+ const slackPatch = slackChannelId !== undefined &&
199
+ slackChannelUrl !== undefined &&
200
+ slackChannelName !== undefined
201
+ ? {
202
+ slackChannelId,
203
+ slackChannelUrl,
204
+ slackChannelName,
205
+ }
206
+ : {};
203
207
  return {
204
208
  id: requiredString(o.id, "update-project payload.id"),
205
209
  name: requiredString(o.name, "update-project payload.name"),
@@ -207,12 +211,17 @@ export function parseUpdateProjectPayload(raw) {
207
211
  companyId: requiredString(o.companyId, "update-project payload.companyId"),
208
212
  contactPersonId: requiredString(o.contactPersonId, "update-project payload.contactPersonId"),
209
213
  asanaTaskId: requiredString(o.asanaTaskId, "update-project payload.asanaTaskId"),
214
+ insideSalesId: requiredString(o.insideSalesId, "update-project payload.insideSalesId"),
215
+ businessDevelopmentId: requiredString(o.businessDevelopmentId, "update-project payload.businessDevelopmentId"),
216
+ venue: requiredString(o.venue, "update-project payload.venue"),
217
+ pax: requiredString(o.pax, "update-project payload.pax"),
210
218
  dateRange: {
211
219
  from: toDate(range.from, "dateRange.from"),
212
220
  to: range.to === undefined || range.to === null
213
221
  ? undefined
214
222
  : toDate(range.to, "dateRange.to"),
215
223
  },
224
+ ...slackPatch,
216
225
  ...(requestQboAccountantNotification !== undefined && {
217
226
  requestQboAccountantNotification,
218
227
  }),
@@ -8,7 +8,7 @@ export const EstimationMode = {
8
8
  };
9
9
  export const ProjectStatus = {
10
10
  PITCH: "PITCH",
11
- EVENT: "EVENT",
11
+ WON: "WON",
12
12
  COMPLETED: "COMPLETED",
13
13
  LOST: "LOST",
14
14
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@go-labs-sg/bb",
3
- "version": "1.3.0",
3
+ "version": "1.6.0",
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",
@@ -21,7 +21,7 @@
21
21
  "postpack": "node scripts/restore-package-json.js"
22
22
  },
23
23
  "dependencies": {
24
- "@trpc/client": "^11.16.0",
24
+ "@trpc/client": "^11.17.0",
25
25
  "dotenv": "^17.4.2",
26
26
  "mime-types": "^3.0.2",
27
27
  "superjson": "^2.2.6"