@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 +2 -2
- package/dist/commands.js +54 -20
- package/dist/index.js +23 -5
- package/dist/parse-mutation-payload.js +18 -9
- package/dist/prisma-enums.js +1 -1
- package/package.json +2 -2
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`,
|
|
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.
|
|
89
|
-
id: budget.
|
|
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({
|
|
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
|
|
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;
|
|
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
|
-
|
|
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 —
|
|
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
|
}),
|
package/dist/prisma-enums.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@go-labs-sg/bb",
|
|
3
|
-
"version": "1.
|
|
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.
|
|
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"
|