@go-labs-sg/bb 1.4.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);
@@ -85,23 +90,38 @@ const runBudgetPreflight = async (budgetId, preflight) => {
85
90
  }
86
91
  if (preflight.eventDetails) {
87
92
  const budget = await api.budget.getBudget.query({ id: budgetId });
88
- await api.budget.updateBudget.mutate({
89
- id: budget.id,
90
- name: budget.name,
91
- date: new Date(preflight.eventDetails.date),
92
- startTime: new Date(preflight.eventDetails.startTime),
93
- endTime: new Date(preflight.eventDetails.endTime),
94
- pax: String(budget.pax),
95
- venue: budget.venue,
96
- budget: String(budget.budget),
97
- categoryId: budget.categoryId,
98
- paymentTerm: budget.paymentTerm,
99
- insideSalesId: budget.insideSalesId ?? budget.insideSales.id,
100
- businessDevelopmentId: budget.businessDevelopmentId ?? budget.businessDevelopment.id,
101
- pipedriveDealId: budget.pipedriveDealId ?? undefined,
102
- asanaTaskId: budget.asanaTaskId ?? "",
103
- projectId: budget.projectId,
93
+ const project = await api.project.getProjectById.query({
94
+ id: budget.projectId,
104
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);
105
125
  }
106
126
  };
107
127
  export async function listBills(opts) {
@@ -864,6 +884,10 @@ export async function createProject(opts) {
864
884
  description: opts.description,
865
885
  companyId: opts.companyId,
866
886
  contactPersonId: opts.contactPersonId,
887
+ insideSalesId: opts.insideSalesId,
888
+ businessDevelopmentId: opts.businessDevelopmentId,
889
+ venue: opts.venue,
890
+ pax: opts.pax,
867
891
  asanaTaskId: opts.asanaTaskId,
868
892
  slackChannelId: opts.slackChannelId,
869
893
  slackChannelUrl: opts.slackChannelUrl,
@@ -878,8 +902,15 @@ export async function createProject(opts) {
878
902
  });
879
903
  out(result);
880
904
  }
881
- export async function updateProjectStatus(id, status) {
882
- 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
+ });
883
914
  out(result);
884
915
  }
885
916
  // --- Contacts ---
@@ -1017,7 +1048,10 @@ export async function getApprovedBudgets(opts) {
1017
1048
  const revenue = calc.totalSellingAfterDiscount ?? 0;
1018
1049
  const cost = calc.totalCostWithoutGst ?? 0;
1019
1050
  const gp = calc.gpPercentage ?? 0;
1020
- 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 ?? "");
1021
1055
  const createdAt = b.createdAt instanceof Date
1022
1056
  ? b.createdAt.toISOString()
1023
1057
  : String(b.createdAt);
@@ -1027,7 +1061,7 @@ export async function getApprovedBudgets(opts) {
1027
1061
  categoryId: b.categoryId,
1028
1062
  company: b.project?.company?.name,
1029
1063
  date,
1030
- pax: b.pax,
1064
+ pax: b.project.pax,
1031
1065
  revenue: Number(revenue.toFixed(2)),
1032
1066
  cost: Number(cost.toFixed(2)),
1033
1067
  gpPercentage: Number(gp.toFixed(2)),
package/dist/index.js CHANGED
@@ -77,7 +77,7 @@ Budgets
77
77
  update-budget-status <id> <status>
78
78
  status: ${budgetStatusesForHelp.join(", ")}
79
79
  mark-budget-won <budgetId> <filePath> Upload signed quote/PO proof and set status to ESTIMATE_ACCEPTED
80
- create-budget --payload '<json>' (budget.createBudget; ISO dates for date/startTime/endTime)
80
+ create-budget --payload '<json>' (budget.createBudget; Asana deal card is on the project)
81
81
  update-budget --payload '<json>' (budget.updateBudget; must include id)
82
82
  delete-budget <budgetId>
83
83
  create-budget-approval <budgetId> (also sends approval request emails)
@@ -127,10 +127,11 @@ Companies & projects
127
127
  delete-company <id>
128
128
  list-projects [--companyId] [--name] [--status] [--page] [--perPage]
129
129
  get-project <id>
130
- 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]
131
131
  update-project --payload '<json>' (project.updateProject; optional requestQboAccountantNotification; default notify like web)
132
132
  delete-project <id>
133
- 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.
134
135
  status: ${projectStatusesForHelp.join(", ")}
135
136
 
136
137
  Contacts
@@ -734,6 +735,10 @@ async function main() {
734
735
  const name = getFlag(flags, "name");
735
736
  const companyId = getFlag(flags, "companyId");
736
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");
737
742
  const asanaTaskId = getFlag(flags, "asanaTaskId");
738
743
  const slackChannelId = getFlag(flags, "slackChannelId");
739
744
  const slackChannelUrl = getFlag(flags, "slackChannelUrl");
@@ -742,17 +747,25 @@ async function main() {
742
747
  if (!name ||
743
748
  !companyId ||
744
749
  !contactPersonId ||
750
+ !insideSalesId ||
751
+ !businessDevelopmentId ||
752
+ !venue ||
753
+ !pax ||
745
754
  !asanaTaskId ||
746
755
  !slackChannelId ||
747
756
  !slackChannelUrl ||
748
757
  !slackChannelName ||
749
758
  !startDate) {
750
- 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");
751
760
  }
752
761
  await createProject({
753
762
  name,
754
763
  companyId,
755
764
  contactPersonId,
765
+ insideSalesId,
766
+ businessDevelopmentId,
767
+ venue,
768
+ pax,
756
769
  asanaTaskId,
757
770
  slackChannelId,
758
771
  slackChannelUrl,
@@ -785,7 +798,12 @@ async function main() {
785
798
  if (!id || !statusRaw) {
786
799
  throw new Error("update-project-status requires <id> <status>");
787
800
  }
788
- 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
+ });
789
807
  break;
790
808
  }
791
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.4.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"