@go-labs-sg/bb 1.4.0 → 1.6.1

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
@@ -1,6 +1,8 @@
1
1
  # bb
2
2
 
3
- Command-line interface for **Budget Builder**. Call budgets, bills, approvals, suppliers, dashboard, and more from your terminal or from tools that don’t support MCP (e.g. some AI agents). Responses are JSON on stdout.
3
+ Command-line interface for **Budget Builder**. Call budgets, bills, claims, approvals, suppliers, dashboard, and more from your terminal or from tools that don’t support MCP (e.g. some AI agents). Responses are JSON on stdout.
4
+
5
+ For AI agents: bills and claims are stored as the same underlying bill records. Use the `isClaimable` column/flag to distinguish them: `isClaimable=false` means a normal bill, while `isClaimable=true` means a reimbursable claim.
4
6
 
5
7
  **Registry:** [`@go-labs-sg/bb`](https://www.npmjs.com/package/@go-labs-sg/bb)
6
8
 
@@ -50,16 +52,16 @@ Global options and flags use `--key=value` or `--key value` (see `bb help`).
50
52
 
51
53
  **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
54
 
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.
55
+ **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
56
 
55
57
  ### Command overview
56
58
 
57
59
  | Area | Commands (non-exhaustive) |
58
60
  | --- | --- |
59
61
  | **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
- | **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` |
62
+ | **Bills / claims** | `list-bills` (`--isClaimable false` for bills, `--isClaimable true` for claims, omit for both), `list-claims` (claims only), `create-bill` (`--payload`; set `isClaimable=false` for a bill or `isClaimable=true` for a claim), `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
63
  | **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`) |
64
+ | **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
65
  | **Contacts** | `list-contacts`, `create-contact-person` (`--payload`), `update-contact-person` (`--payload`) |
64
66
  | **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
67
  | **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
@@ -59,7 +59,7 @@ function parseDashboardQueryFlags(flags) {
59
59
  }
60
60
  function printHelp() {
61
61
  const help = `
62
- bb — Budget Builder CLI (parity with MCP tools)
62
+ bb — Budget Builder CLI for AI agents (parity with MCP tools)
63
63
 
64
64
  Usage: bb <command> [options] [args]
65
65
 
@@ -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)
@@ -97,8 +97,9 @@ Budgets
97
97
  Bills
98
98
  list-bills [--projectId] [--budgetId] [--status CSV] [--search <text>] [--isClaimable true|false] [--createdByIds <csv>] [--sortBy createdAt|amount|status] [--sortDir asc|desc] [--page] [--pageSize]
99
99
  list-claims same flags as list-bills; only reimbursable claims (ignores --isClaimable)
100
+ isClaimable differentiates the shared bill/claim records: false = bill, true = claim.
100
101
  create-bill-approval <billId> (also sends approval request emails)
101
- create-bill --payload '<json>' (bill.create; use upload-bill-attachment after create to add files)
102
+ create-bill --payload '<json>' (bill.create; payload.isClaimable false = bill, true = claim; use upload-bill-attachment after create to add files)
102
103
  update-bill --payload '<json>' (bill.update; must include id)
103
104
  delete-bill <billId>
104
105
  update-bill-status <id> <status> [--rejectionReason] [--paymentTrackingUrl] [--paymentReference]
@@ -127,10 +128,11 @@ Companies & projects
127
128
  delete-company <id>
128
129
  list-projects [--companyId] [--name] [--status] [--page] [--perPage]
129
130
  get-project <id>
130
- create-project --name --companyId --contactPersonId --asanaTaskId --slackChannelId --slackChannelUrl --slackChannelName --startDate <ISO> [--endDate] [--description] [--requestQboAccountantNotification false]
131
+ create-project --name --companyId --contactPersonId --insideSalesId --businessDevelopmentId --venue --pax --asanaTaskId --slackChannelId --slackChannelUrl --slackChannelName --startDate <ISO> [--endDate <ISO>] [--description] [--requestQboAccountantNotification false]
131
132
  update-project --payload '<json>' (project.updateProject; optional requestQboAccountantNotification; default notify like web)
132
133
  delete-project <id>
133
- update-project-status <id> <status>
134
+ update-project-status <id> <status> [--projectManagerId <id>] [--projectManagerName <name>]
135
+ Required when moving PITCH → WON: supply --projectManagerId or --projectManagerName.
134
136
  status: ${projectStatusesForHelp.join(", ")}
135
137
 
136
138
  Contacts
@@ -734,6 +736,10 @@ async function main() {
734
736
  const name = getFlag(flags, "name");
735
737
  const companyId = getFlag(flags, "companyId");
736
738
  const contactPersonId = getFlag(flags, "contactPersonId");
739
+ const insideSalesId = getFlag(flags, "insideSalesId");
740
+ const businessDevelopmentId = getFlag(flags, "businessDevelopmentId");
741
+ const venue = getFlag(flags, "venue");
742
+ const pax = getFlag(flags, "pax");
737
743
  const asanaTaskId = getFlag(flags, "asanaTaskId");
738
744
  const slackChannelId = getFlag(flags, "slackChannelId");
739
745
  const slackChannelUrl = getFlag(flags, "slackChannelUrl");
@@ -742,17 +748,25 @@ async function main() {
742
748
  if (!name ||
743
749
  !companyId ||
744
750
  !contactPersonId ||
751
+ !insideSalesId ||
752
+ !businessDevelopmentId ||
753
+ !venue ||
754
+ !pax ||
745
755
  !asanaTaskId ||
746
756
  !slackChannelId ||
747
757
  !slackChannelUrl ||
748
758
  !slackChannelName ||
749
759
  !startDate) {
750
- throw new Error("create-project requires --name --companyId --contactPersonId --asanaTaskId --slackChannelId --slackChannelUrl --slackChannelName --startDate (ISO)");
760
+ 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
761
  }
752
762
  await createProject({
753
763
  name,
754
764
  companyId,
755
765
  contactPersonId,
766
+ insideSalesId,
767
+ businessDevelopmentId,
768
+ venue,
769
+ pax,
756
770
  asanaTaskId,
757
771
  slackChannelId,
758
772
  slackChannelUrl,
@@ -785,7 +799,12 @@ async function main() {
785
799
  if (!id || !statusRaw) {
786
800
  throw new Error("update-project-status requires <id> <status>");
787
801
  }
788
- await updateProjectStatus(id, parseProjectStatusForUpdate(statusRaw));
802
+ const pmIdFlag = getFlag(flags, "projectManagerId");
803
+ const pmNameFlag = getFlag(flags, "projectManagerName");
804
+ await updateProjectStatus(id, parseProjectStatusForUpdate(statusRaw), {
805
+ ...(pmIdFlag && { projectManagerId: String(pmIdFlag) }),
806
+ ...(pmNameFlag && { projectManagerName: String(pmNameFlag) }),
807
+ });
789
808
  break;
790
809
  }
791
810
  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,7 +1,7 @@
1
1
  {
2
2
  "name": "@go-labs-sg/bb",
3
- "version": "1.4.0",
4
- "description": "Budget Builder CLI — list budgets, bills, approvals, suppliers; approve bills; change status. For AI agents (e.g. Chuck/OpenClaw).",
3
+ "version": "1.6.1",
4
+ "description": "Budget Builder CLI for AI agents manage budgets, bills, and claims; bill records use isClaimable=false for bills and isClaimable=true for claims.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "bin": {
@@ -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"