@go-labs-sg/bb 1.15.0 → 1.17.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 +16 -2
- package/dist/api-client.js +11 -1
- package/dist/commands.js +263 -8
- package/dist/index.js +122 -17
- package/dist/parse-cli-enums.js +12 -1
- package/dist/prisma-enums.js +7 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -37,6 +37,20 @@ export BB_API_KEY=<your-key>
|
|
|
37
37
|
|
|
38
38
|
Create or revoke keys in the web app: **Goracle → API Keys** (MCP API keys).
|
|
39
39
|
|
|
40
|
+
Admins can provision API-only service identities and manage their keys from the
|
|
41
|
+
CLI. These identities are not human web accounts: `create-user` does not create
|
|
42
|
+
or link a Google OAuth account, so the resulting user cannot sign in with Google.
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
bb create-user --email agent@example.com --name "Budget Agent" --role USER
|
|
46
|
+
bb create-api-key --userId <user-id> --name "Budget Agent CLI"
|
|
47
|
+
bb list-api-keys --userId <user-id>
|
|
48
|
+
bb revoke-api-key <api-key-id> --userId <user-id>
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The raw key is returned only by `create-api-key`; copy it immediately. Listing
|
|
52
|
+
keys returns metadata only.
|
|
53
|
+
|
|
40
54
|
The CLI talks to the production API: `https://budget-builder.getout.events`.
|
|
41
55
|
|
|
42
56
|
## Usage
|
|
@@ -72,10 +86,10 @@ Global options and flags use `--key=value` or `--key value` (see `bb help`).
|
|
|
72
86
|
| **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; non-legacy supplier bills from 1 Jul 2026 00:00 SGT require approved quotation coverage), `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` |
|
|
73
87
|
| **Quotations** | `list-quotations`, `get-quotation`, `upload-quotation-attachment` (`<projectId>` + local PDF path; returns attachment JSON for payload use), `create-quotation` (`--payload` for `quotation.createDraft`), `submit-quotation`, `approve-quotation`, `reject-quotation`, `download-quotation-pdf` (`original`, `staff`, or `final`) |
|
|
74
88
|
| **Approvals** | `list-approvals` / `get-pending-approvals` (`--type budget\|supplier\|bill\|quotation\|all`), `approve-bill` / `reject-bill` (send reply email), `approve-budget` / `reject-budget` (send reply email), `approve-supplier` / `reject-supplier` (send reply email), `approve-quotation` / `reject-quotation` |
|
|
75
|
-
| **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`, `--
|
|
89
|
+
| **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`, `--startDate` as ISO datetime for project/window start; when `--asanaTaskId` is omitted, the CLI searches open Asana lead tasks in the Deals project, prompts for one of the top five matches, and resolves Slack channel fields from the selected deal card; optional `--asanaSearch`, `--pax`, `--endDate`, `--description`; always requests QBO project import like the web app), `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`; when marking `WON` without an accepted/closed budget or proof, pass `--wonOverrideReason`) |
|
|
76
90
|
| **Contacts** | `list-contacts`, `create-contact-person` (`--payload`), `update-contact-person` (`--payload`) |
|
|
77
91
|
| **Suppliers & items** | `list-suppliers` (defaults to active suppliers, `--perPage 10`, sorted by `createdAt` desc; supports `--name`, `--sortBy` for scalar supplier fields, `--sortDir`, `--createdBy`, `--gstRegistered`, `--status`, `--supplierTags`, `--active false` for archived 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; archives/deactivates related items), `reactivate-suppliers` (`--ids` CSV; admin; reactivates related items), `create-certification` / `create-payment-method` / `create-supplier-role` / `create-supplier-tag` (`--name`), `get-supplier-details` (includes `supplierApprovalSummary` for pending approvers, superseded approvers, and the actual responder/respondedAt metadata), `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) |
|
|
78
|
-
| **Dashboard & users** | `list-users`, `get-user-performance`, `get-dashboard`, `get-monthly-metrics`, `get-system-overview`, `get-estimate-performance`, `get-financial-overview` |
|
|
92
|
+
| **Dashboard & users** | `list-users`, `create-user` (admin; provisions an API-only service identity with no Google sign-in; `--email`, optional `--name`, optional `--role` defaulting to `USER`), `create-api-key` (admin; `--userId`, `--name`; raw key shown once), `list-api-keys` (admin; optional `--userId`), `revoke-api-key` (admin; key ID plus `--userId` for another user's key), `get-user-performance`, `get-dashboard`, `get-monthly-metrics`, `get-system-overview`, `get-estimate-performance`, `get-financial-overview` |
|
|
79
93
|
| **Errors** | `get-recent-errors`, `get-error-metrics` |
|
|
80
94
|
| **Historical / benchmarks** | `get-approved-budgets`, `get-budget-category-benchmarks`, `get-item-pricing-history`, `get-supplier-pricing-history` |
|
|
81
95
|
|
package/dist/api-client.js
CHANGED
|
@@ -8,10 +8,20 @@ function getAuthHeader() {
|
|
|
8
8
|
return undefined;
|
|
9
9
|
return `Bearer ${key.trim()}`;
|
|
10
10
|
}
|
|
11
|
+
const containsRawApiKey = (value, seen = new WeakSet()) => {
|
|
12
|
+
if (typeof value !== "object" || value === null)
|
|
13
|
+
return false;
|
|
14
|
+
if (seen.has(value))
|
|
15
|
+
return false;
|
|
16
|
+
seen.add(value);
|
|
17
|
+
return Object.entries(value).some(([key, nestedValue]) => (key.toLowerCase() === "apikey" && typeof nestedValue === "string") ||
|
|
18
|
+
containsRawApiKey(nestedValue, seen));
|
|
19
|
+
};
|
|
11
20
|
export const api = createTRPCProxyClient({
|
|
12
21
|
links: [
|
|
13
22
|
loggerLink({
|
|
14
|
-
enabled: () => shouldLogCliActions()
|
|
23
|
+
enabled: (opts) => shouldLogCliActions() &&
|
|
24
|
+
!(opts.direction === "down" && containsRawApiKey(opts.result)),
|
|
15
25
|
colorMode: "ansi",
|
|
16
26
|
withContext: false,
|
|
17
27
|
// tRPC defaults use console.log for requests; stderr keeps stdout JSON-safe for pipes.
|
package/dist/commands.js
CHANGED
|
@@ -13,17 +13,200 @@ const BUDGET = "BUDGET";
|
|
|
13
13
|
const BILL = "BILL";
|
|
14
14
|
const SUPPLIER = "SUPPLIER";
|
|
15
15
|
const QUOTATION = "QUOTATION";
|
|
16
|
+
const ASANA_WON_LOST_SECTION_GIDS = new Set([
|
|
17
|
+
"1211678338364908",
|
|
18
|
+
"1211678338364907",
|
|
19
|
+
]);
|
|
20
|
+
const MAX_ASANA_LEAD_CANDIDATES = 5;
|
|
21
|
+
const ASANA_SEARCH_DETAIL_LIMIT = 15;
|
|
16
22
|
function isPendingRequest(n) {
|
|
17
23
|
return n.notificationType === "request" && n.status === "PENDING_APPROVAL";
|
|
18
24
|
}
|
|
19
25
|
const out = (data) => {
|
|
20
26
|
console.log(JSON.stringify(data, null, 2));
|
|
21
27
|
};
|
|
28
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
29
|
+
const stringField = (record, key) => {
|
|
30
|
+
const value = record[key];
|
|
31
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
32
|
+
};
|
|
33
|
+
const asanaSectionsForTask = (task) => {
|
|
34
|
+
if (!isRecord(task) || !Array.isArray(task.memberships))
|
|
35
|
+
return [];
|
|
36
|
+
return task.memberships.flatMap((membership) => {
|
|
37
|
+
if (!isRecord(membership) || !isRecord(membership.section))
|
|
38
|
+
return [];
|
|
39
|
+
return [
|
|
40
|
+
{
|
|
41
|
+
gid: stringField(membership.section, "gid"),
|
|
42
|
+
name: stringField(membership.section, "name"),
|
|
43
|
+
},
|
|
44
|
+
];
|
|
45
|
+
});
|
|
46
|
+
};
|
|
47
|
+
const isWonOrLostAsanaTask = (task) => asanaSectionsForTask(task).some((section) => {
|
|
48
|
+
if (section.gid && ASANA_WON_LOST_SECTION_GIDS.has(section.gid)) {
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
const name = section.name?.trim().toLowerCase();
|
|
52
|
+
return Boolean(name && /\b(won|lost)\b/.test(name));
|
|
53
|
+
});
|
|
54
|
+
const asanaCandidateFromTask = (task, details) => {
|
|
55
|
+
const source = details ?? task;
|
|
56
|
+
const sourceRecord = isRecord(source) ? source : undefined;
|
|
57
|
+
const gid = (sourceRecord ? stringField(sourceRecord, "gid") : undefined) ?? task.gid;
|
|
58
|
+
const name = (sourceRecord ? stringField(sourceRecord, "name") : undefined) ??
|
|
59
|
+
task.name ??
|
|
60
|
+
gid;
|
|
61
|
+
if (!gid || isWonOrLostAsanaTask(source))
|
|
62
|
+
return null;
|
|
63
|
+
return {
|
|
64
|
+
gid,
|
|
65
|
+
name,
|
|
66
|
+
modifiedAt: (sourceRecord ? stringField(sourceRecord, "modified_at") : undefined) ??
|
|
67
|
+
task.modified_at,
|
|
68
|
+
createdAt: (sourceRecord ? stringField(sourceRecord, "created_at") : undefined) ??
|
|
69
|
+
task.created_at,
|
|
70
|
+
sections: asanaSectionsForTask(source),
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
const uniqueSearchQueries = (...queries) => {
|
|
74
|
+
const seen = new Set();
|
|
75
|
+
const unique = [];
|
|
76
|
+
for (const query of queries) {
|
|
77
|
+
const normalized = query?.trim() ?? "";
|
|
78
|
+
if (!normalized)
|
|
79
|
+
continue;
|
|
80
|
+
const key = normalized.toLowerCase();
|
|
81
|
+
if (seen.has(key))
|
|
82
|
+
continue;
|
|
83
|
+
seen.add(key);
|
|
84
|
+
unique.push(normalized);
|
|
85
|
+
}
|
|
86
|
+
return unique;
|
|
87
|
+
};
|
|
88
|
+
const searchOpenAsanaLeadCandidates = async ({ projectName, searchQuery, }) => {
|
|
89
|
+
const queries = uniqueSearchQueries(projectName, searchQuery);
|
|
90
|
+
const candidates = [];
|
|
91
|
+
const seenTaskIds = new Set();
|
|
92
|
+
for (const query of queries) {
|
|
93
|
+
const results = await api.asana.searchTasks.query({ query });
|
|
94
|
+
const tasks = results.data.slice(0, ASANA_SEARCH_DETAIL_LIMIT);
|
|
95
|
+
const detailsByTaskId = new Map();
|
|
96
|
+
await Promise.all(tasks.map(async (task) => {
|
|
97
|
+
if (!task.gid)
|
|
98
|
+
return;
|
|
99
|
+
try {
|
|
100
|
+
const details = await api.asana.getTaskById.query({ id: task.gid });
|
|
101
|
+
detailsByTaskId.set(task.gid, details.data);
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
// Keep the compact search result if detail lookup fails for one task.
|
|
105
|
+
}
|
|
106
|
+
}));
|
|
107
|
+
for (const task of tasks) {
|
|
108
|
+
if (!task.gid || seenTaskIds.has(task.gid))
|
|
109
|
+
continue;
|
|
110
|
+
const candidate = asanaCandidateFromTask(task, detailsByTaskId.get(task.gid));
|
|
111
|
+
if (!candidate)
|
|
112
|
+
continue;
|
|
113
|
+
seenTaskIds.add(candidate.gid);
|
|
114
|
+
candidates.push(candidate);
|
|
115
|
+
if (candidates.length >= MAX_ASANA_LEAD_CANDIDATES)
|
|
116
|
+
return candidates;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return candidates;
|
|
120
|
+
};
|
|
121
|
+
const formatAsanaCandidateMetadata = (candidate) => {
|
|
122
|
+
const sectionText = candidate.sections
|
|
123
|
+
.map((section) => section.name ?? section.gid)
|
|
124
|
+
.filter(Boolean)
|
|
125
|
+
.join(", ") || "stage unavailable";
|
|
126
|
+
const dateText = candidate.modifiedAt
|
|
127
|
+
? `modified ${new Date(candidate.modifiedAt).toISOString()}`
|
|
128
|
+
: candidate.createdAt
|
|
129
|
+
? `created ${new Date(candidate.createdAt).toISOString()}`
|
|
130
|
+
: "date unavailable";
|
|
131
|
+
return `${sectionText}; ${dateText}; ${candidate.gid}`;
|
|
132
|
+
};
|
|
133
|
+
const promptForAsanaLeadCandidate = async (candidates) => {
|
|
134
|
+
if (candidates.length === 0) {
|
|
135
|
+
throw new Error("No open Asana lead tasks were found in the Deals project. Pass --asanaTaskId explicitly or update the Deals project first.");
|
|
136
|
+
}
|
|
137
|
+
if (!process.stdin.isTTY || !process.stderr.isTTY) {
|
|
138
|
+
throw new Error("create-project requires --asanaTaskId in non-interactive runs. Interactive runs can omit it to choose from the top Asana lead task matches.");
|
|
139
|
+
}
|
|
140
|
+
console.error("Select the Asana lead task to link to this project:");
|
|
141
|
+
candidates.forEach((candidate, index) => {
|
|
142
|
+
console.error(`${index + 1}. ${candidate.name} (${formatAsanaCandidateMetadata(candidate)})`);
|
|
143
|
+
});
|
|
144
|
+
const rl = createInterface({
|
|
145
|
+
input: process.stdin,
|
|
146
|
+
output: process.stderr,
|
|
147
|
+
});
|
|
148
|
+
try {
|
|
149
|
+
while (true) {
|
|
150
|
+
const answer = await rl.question(`Choose 1-${candidates.length}, or q to abort: `);
|
|
151
|
+
const trimmed = answer.trim().toLowerCase();
|
|
152
|
+
if (trimmed === "q" || trimmed === "quit") {
|
|
153
|
+
throw new Error("Aborted.");
|
|
154
|
+
}
|
|
155
|
+
const selected = Number.parseInt(trimmed, 10);
|
|
156
|
+
if (Number.isInteger(selected) &&
|
|
157
|
+
selected >= 1 &&
|
|
158
|
+
selected <= candidates.length) {
|
|
159
|
+
const candidate = candidates[selected - 1];
|
|
160
|
+
if (candidate)
|
|
161
|
+
return candidate;
|
|
162
|
+
}
|
|
163
|
+
console.error(`Enter a number from 1 to ${candidates.length}.`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
finally {
|
|
167
|
+
rl.close();
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
const resolveAsanaDealSlackChannel = async (asanaTaskId) => {
|
|
171
|
+
const result = await api.asana.getDealSlackChannel.query({ id: asanaTaskId });
|
|
172
|
+
if (result.status !== "resolved") {
|
|
173
|
+
throw new Error(result.message);
|
|
174
|
+
}
|
|
175
|
+
console.error(`Resolved Slack channel from Asana deal: #${result.channel.slackChannelName} (${result.channel.slackChannelId})`);
|
|
176
|
+
return result.channel;
|
|
177
|
+
};
|
|
178
|
+
const resolveCreateProjectLinks = async (opts) => {
|
|
179
|
+
const asanaTaskId = opts.asanaTaskId?.trim() ||
|
|
180
|
+
(await promptForAsanaLeadCandidate(await searchOpenAsanaLeadCandidates({
|
|
181
|
+
projectName: opts.name,
|
|
182
|
+
searchQuery: opts.asanaSearch,
|
|
183
|
+
}))).gid;
|
|
184
|
+
const hasCompleteManualSlackChannel = Boolean(opts.slackChannelId && opts.slackChannelUrl && opts.slackChannelName);
|
|
185
|
+
if (hasCompleteManualSlackChannel) {
|
|
186
|
+
return {
|
|
187
|
+
asanaTaskId,
|
|
188
|
+
slackChannelId: opts.slackChannelId,
|
|
189
|
+
slackChannelUrl: opts.slackChannelUrl,
|
|
190
|
+
slackChannelName: opts.slackChannelName,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
const slackChannel = await resolveAsanaDealSlackChannel(asanaTaskId);
|
|
194
|
+
return {
|
|
195
|
+
asanaTaskId,
|
|
196
|
+
...slackChannel,
|
|
197
|
+
};
|
|
198
|
+
};
|
|
22
199
|
const combineDateAndTime = (day, time) => {
|
|
23
200
|
const d = new Date(day);
|
|
24
201
|
d.setHours(time.getHours(), time.getMinutes(), time.getSeconds(), 0);
|
|
25
202
|
return d;
|
|
26
203
|
};
|
|
204
|
+
const formatOptionalProjectPaxInput = (pax) => {
|
|
205
|
+
if (typeof pax !== "number" || !Number.isFinite(pax) || pax <= 0) {
|
|
206
|
+
return "";
|
|
207
|
+
}
|
|
208
|
+
return String(pax);
|
|
209
|
+
};
|
|
27
210
|
/** Extension-based MIME, like browser `File.type` when the OS provides it */
|
|
28
211
|
const contentTypeHeaderForFileName = (fileName) => {
|
|
29
212
|
const ct = contentType(fileName);
|
|
@@ -139,6 +322,9 @@ export async function updateBudgetStatus(budgetId, status, opts) {
|
|
|
139
322
|
const result = await api.budget.updateBudgetStatus.mutate({
|
|
140
323
|
id: budgetId,
|
|
141
324
|
status,
|
|
325
|
+
...(opts?.projectManagerId !== undefined && {
|
|
326
|
+
projectManagerId: opts.projectManagerId,
|
|
327
|
+
}),
|
|
142
328
|
...(opts?.markProjectWon !== undefined && {
|
|
143
329
|
markProjectWon: opts.markProjectWon,
|
|
144
330
|
}),
|
|
@@ -200,7 +386,7 @@ const runBudgetPreflight = async (budgetId, preflight) => {
|
|
|
200
386
|
insideSalesId: project.insideSalesId ?? "",
|
|
201
387
|
businessDevelopmentId: project.businessDevelopmentId ?? "",
|
|
202
388
|
venue: project.venue ?? "",
|
|
203
|
-
pax:
|
|
389
|
+
pax: formatOptionalProjectPaxInput(project.pax),
|
|
204
390
|
...(project.slackChannelId &&
|
|
205
391
|
project.slackChannelUrl &&
|
|
206
392
|
project.slackChannelName
|
|
@@ -403,7 +589,11 @@ export async function getBillAttachments(id) {
|
|
|
403
589
|
const result = await api.bill.getAttachments.query({ id });
|
|
404
590
|
out(result);
|
|
405
591
|
}
|
|
406
|
-
export async function uploadBillAttachmentFromPath(billId, filePath) {
|
|
592
|
+
export async function uploadBillAttachmentFromPath(billId, filePath, type = "BILL") {
|
|
593
|
+
const confirmed = await uploadSingleBillAttachmentFromPath(billId, filePath, type);
|
|
594
|
+
out(confirmed);
|
|
595
|
+
}
|
|
596
|
+
const uploadSingleBillAttachmentFromPath = async (billId, filePath, type = "BILL") => {
|
|
407
597
|
const buf = await readFile(filePath);
|
|
408
598
|
const fileName = basename(filePath);
|
|
409
599
|
const size = buf.byteLength;
|
|
@@ -420,13 +610,51 @@ export async function uploadBillAttachmentFromPath(billId, filePath) {
|
|
|
420
610
|
if (!res.ok) {
|
|
421
611
|
throw new Error(`S3 upload failed: HTTP ${res.status} ${(await res.text()).slice(0, 500)}`);
|
|
422
612
|
}
|
|
423
|
-
|
|
613
|
+
return await api.attachment.confirmBillAttachment.mutate({
|
|
424
614
|
billId,
|
|
425
615
|
key,
|
|
426
616
|
name: fileName,
|
|
427
617
|
size,
|
|
618
|
+
type,
|
|
619
|
+
});
|
|
620
|
+
};
|
|
621
|
+
export async function uploadBillAttachmentsFromPaths(billId, filePaths, type = "BILL") {
|
|
622
|
+
if (filePaths.length === 0) {
|
|
623
|
+
throw new Error("At least one bill attachment file path is required");
|
|
624
|
+
}
|
|
625
|
+
const results = [];
|
|
626
|
+
for (const filePath of filePaths) {
|
|
627
|
+
results.push(await uploadSingleBillAttachmentFromPath(billId, filePath, type));
|
|
628
|
+
}
|
|
629
|
+
out({
|
|
630
|
+
success: true,
|
|
631
|
+
billId,
|
|
632
|
+
type,
|
|
633
|
+
count: results.length,
|
|
634
|
+
attachments: results.map((result) => result.attachment),
|
|
635
|
+
results,
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
export async function uploadBillDocumentsFromPaths({ billId, invoicePaths = [], paymentProofPaths = [], }) {
|
|
639
|
+
if (invoicePaths.length === 0 && paymentProofPaths.length === 0) {
|
|
640
|
+
throw new Error("At least one invoice or payment proof file path is required");
|
|
641
|
+
}
|
|
642
|
+
const invoiceResults = [];
|
|
643
|
+
for (const filePath of invoicePaths) {
|
|
644
|
+
invoiceResults.push(await uploadSingleBillAttachmentFromPath(billId, filePath, "BILL"));
|
|
645
|
+
}
|
|
646
|
+
const paymentProofResults = [];
|
|
647
|
+
for (const filePath of paymentProofPaths) {
|
|
648
|
+
paymentProofResults.push(await uploadSingleBillAttachmentFromPath(billId, filePath, "BILL_PAYMENT_PROOF"));
|
|
649
|
+
}
|
|
650
|
+
out({
|
|
651
|
+
success: true,
|
|
652
|
+
billId,
|
|
653
|
+
count: invoiceResults.length + paymentProofResults.length,
|
|
654
|
+
invoiceAttachments: invoiceResults.map((result) => result.attachment),
|
|
655
|
+
paymentProofAttachments: paymentProofResults.map((result) => result.attachment),
|
|
656
|
+
results: [...invoiceResults, ...paymentProofResults],
|
|
428
657
|
});
|
|
429
|
-
out(confirmed);
|
|
430
658
|
}
|
|
431
659
|
export async function uploadQuotationAttachmentFromPath(projectId, filePath) {
|
|
432
660
|
const buf = await readFile(filePath);
|
|
@@ -513,6 +741,9 @@ export async function uploadBudgetWinProofFromPath(budgetId, filePath, opts) {
|
|
|
513
741
|
const result = await api.budget.updateBudgetStatus.mutate({
|
|
514
742
|
id: budgetId,
|
|
515
743
|
status: "ESTIMATE_ACCEPTED",
|
|
744
|
+
...(opts?.projectManagerId !== undefined && {
|
|
745
|
+
projectManagerId: opts.projectManagerId,
|
|
746
|
+
}),
|
|
516
747
|
...(opts?.markProjectWon !== undefined && {
|
|
517
748
|
markProjectWon: opts.markProjectWon,
|
|
518
749
|
}),
|
|
@@ -1220,6 +1451,14 @@ export async function getProject(id) {
|
|
|
1220
1451
|
out({ project, budgetsOverview, billsOverview });
|
|
1221
1452
|
}
|
|
1222
1453
|
export async function createProject(opts) {
|
|
1454
|
+
const links = await resolveCreateProjectLinks({
|
|
1455
|
+
name: opts.name,
|
|
1456
|
+
asanaTaskId: opts.asanaTaskId,
|
|
1457
|
+
asanaSearch: opts.asanaSearch,
|
|
1458
|
+
slackChannelId: opts.slackChannelId,
|
|
1459
|
+
slackChannelUrl: opts.slackChannelUrl,
|
|
1460
|
+
slackChannelName: opts.slackChannelName,
|
|
1461
|
+
});
|
|
1223
1462
|
const result = await api.project.createProject.mutate({
|
|
1224
1463
|
name: opts.name,
|
|
1225
1464
|
description: opts.description,
|
|
@@ -1229,10 +1468,10 @@ export async function createProject(opts) {
|
|
|
1229
1468
|
businessDevelopmentId: opts.businessDevelopmentId,
|
|
1230
1469
|
venue: opts.venue,
|
|
1231
1470
|
pax: opts.pax,
|
|
1232
|
-
asanaTaskId:
|
|
1233
|
-
slackChannelId:
|
|
1234
|
-
slackChannelUrl:
|
|
1235
|
-
slackChannelName:
|
|
1471
|
+
asanaTaskId: links.asanaTaskId,
|
|
1472
|
+
slackChannelId: links.slackChannelId,
|
|
1473
|
+
slackChannelUrl: links.slackChannelUrl,
|
|
1474
|
+
slackChannelName: links.slackChannelName,
|
|
1236
1475
|
dateRange: {
|
|
1237
1476
|
from: new Date(opts.startDate),
|
|
1238
1477
|
to: opts.endDate ? new Date(opts.endDate) : undefined,
|
|
@@ -1298,6 +1537,22 @@ export async function getSupplierAnalytics(opts) {
|
|
|
1298
1537
|
out(result);
|
|
1299
1538
|
}
|
|
1300
1539
|
// --- Dashboard / analytics ---
|
|
1540
|
+
export async function createUser(input) {
|
|
1541
|
+
const user = await api.user.create.mutate(input);
|
|
1542
|
+
out(user);
|
|
1543
|
+
}
|
|
1544
|
+
export async function createApiKeyForUser(input) {
|
|
1545
|
+
const apiKey = await api.mcpApiKey.create.mutate(input);
|
|
1546
|
+
out(apiKey);
|
|
1547
|
+
}
|
|
1548
|
+
export async function listApiKeysForUser(input) {
|
|
1549
|
+
const apiKeys = await api.mcpApiKey.list.query(input);
|
|
1550
|
+
out(apiKeys);
|
|
1551
|
+
}
|
|
1552
|
+
export async function revokeApiKeyForUser(input) {
|
|
1553
|
+
const result = await api.mcpApiKey.revoke.mutate(input);
|
|
1554
|
+
out(result);
|
|
1555
|
+
}
|
|
1301
1556
|
export async function listUsers() {
|
|
1302
1557
|
const users = await api.user.getAllUsers.query();
|
|
1303
1558
|
out(users);
|
package/dist/index.js
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
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, approveQuotation, approveSupplier, createBillApproval, createBillFromPayload, createBudgetApproval, createBudgetCategory, createBudgetFromPayload, createCompany, createContactPersonFromPayload, createEstimate, createItemCategory, createItemFromPayload, createProject, createQuotationFromPayload, createSupplierCertification, createSupplierFromPayload, createSupplierPaymentMethod, createSupplierRoleOption, createSupplierTagOption, deleteBillById, deleteBudgetById, deleteBudgetCategory, deleteCompanyById, deleteItemById, deleteItemCategoriesByIds, deleteProjectById, deleteSuppliersByIds, downloadQuotationPdf, getApprovedBudgets, getBillAttachments, getBillDetails, getBudget, getBudgetCategories, getBudgetCategoryBenchmarks, getBudgetDetails, getBudgetItemsOnly, getBudgetVersions, getCompany, getDashboard, getErrorMetrics, getEstimatePerformance, getFinancialOverview, getItem, getItemPricingHistory, getMonthlyMetrics, getProject, getQuotationDetails, getRecentErrors, getSupplierAnalytics, getSupplierDetails, getSupplierPricingHistory, getSystemOverview, getUserPerformance, listApprovals, listBills, listBudgets, listCompanies, listContacts, listItemCategories, listItems, listProjects, listQuotations, listSuppliers, listUsers, markBudgetWonWithProof, patchBillInvoiceNumber, patchBillPayment, reactivateSuppliersByIds, rejectBill, rejectBudget, rejectQuotation, rejectSupplier, removeBudgetItem, reorderBudgetItemsCli, submitQuotation, updateBillFromPayload, updateBillStatus, updateBudgetCategory, updateBudgetCommissionFromPayload, updateBudgetDiscountFromPayload, updateBudgetFromPayload, updateBudgetItem, updateBudgetItemSupplierCli, updateBudgetStatus, updateCompanyFromPayload, updateContactPersonFromPayload, updateItemCategory, updateItemFromPayload, updateProjectFromPayload, updateProjectStatus, updateSupplierFromPayload, uploadBillAttachmentFromPath, uploadBudgetAttachmentFromPath, uploadQuotationAttachmentFromPath, } from "./commands.js";
|
|
5
|
+
import { addBudgetItems, approveBill, approveBudget, approveQuotation, approveSupplier, createApiKeyForUser, createBillApproval, createBillFromPayload, createBudgetApproval, createBudgetCategory, createBudgetFromPayload, createCompany, createContactPersonFromPayload, createEstimate, createItemCategory, createItemFromPayload, createProject, createQuotationFromPayload, createSupplierCertification, createSupplierFromPayload, createSupplierPaymentMethod, createSupplierRoleOption, createSupplierTagOption, createUser, deleteBillById, deleteBudgetById, deleteBudgetCategory, deleteCompanyById, deleteItemById, deleteItemCategoriesByIds, deleteProjectById, deleteSuppliersByIds, downloadQuotationPdf, getApprovedBudgets, getBillAttachments, getBillDetails, getBudget, getBudgetCategories, getBudgetCategoryBenchmarks, getBudgetDetails, getBudgetItemsOnly, getBudgetVersions, getCompany, getDashboard, getErrorMetrics, getEstimatePerformance, getFinancialOverview, getItem, getItemPricingHistory, getMonthlyMetrics, getProject, getQuotationDetails, getRecentErrors, getSupplierAnalytics, getSupplierDetails, getSupplierPricingHistory, getSystemOverview, getUserPerformance, listApiKeysForUser, listApprovals, listBills, listBudgets, listCompanies, listContacts, listItemCategories, listItems, listProjects, listQuotations, listSuppliers, listUsers, markBudgetWonWithProof, patchBillInvoiceNumber, patchBillPayment, reactivateSuppliersByIds, rejectBill, rejectBudget, rejectQuotation, rejectSupplier, removeBudgetItem, reorderBudgetItemsCli, revokeApiKeyForUser, submitQuotation, updateBillFromPayload, updateBillStatus, updateBudgetCategory, updateBudgetCommissionFromPayload, updateBudgetDiscountFromPayload, updateBudgetFromPayload, updateBudgetItem, updateBudgetItemSupplierCli, updateBudgetStatus, updateCompanyFromPayload, updateContactPersonFromPayload, updateItemCategory, updateItemFromPayload, updateProjectFromPayload, updateProjectStatus, updateSupplierFromPayload, uploadBillAttachmentFromPath, uploadBillAttachmentsFromPaths, uploadBillDocumentsFromPaths, uploadBudgetAttachmentFromPath, uploadQuotationAttachmentFromPath, } from "./commands.js";
|
|
6
6
|
import { getFlag, parseArgs } from "./parse-args.js";
|
|
7
|
-
import { billStatusesForUpdateHelp, budgetStatusesForHelp, isBudgetStatusUpdate, parseApprovalTypeFlag, parseBillStatusForUpdate, parseCommaSeparatedBillStatuses, parseCommaSeparatedBudgetStatuses, parseCommaSeparatedIds, parseCommaSeparatedSupplierStatuses, parseOptionalBillListSortBy, parseOptionalBillListSortDir, parseOptionalDashboardRole, parseOptionalDeals, parseOptionalErrorSeverity, parseOptionalErrorStatus, parseOptionalExtendedProjectStatus, parseOptionalFinancialRole, parseOptionalSupplierAnalyticsTimeFrame, parseOptionalTimeFrame, parseProjectStatusForUpdate, projectStatusesForHelp, } from "./parse-cli-enums.js";
|
|
7
|
+
import { billStatusesForUpdateHelp, budgetStatusesForHelp, isBudgetStatusUpdate, parseApprovalTypeFlag, parseBillStatusForUpdate, parseCommaSeparatedBillStatuses, parseCommaSeparatedBudgetStatuses, parseCommaSeparatedIds, parseCommaSeparatedSupplierStatuses, parseOptionalBillListSortBy, parseOptionalBillListSortDir, parseOptionalDashboardRole, parseOptionalDeals, parseOptionalErrorSeverity, parseOptionalErrorStatus, parseOptionalExtendedProjectStatus, parseOptionalFinancialRole, parseOptionalSupplierAnalyticsTimeFrame, parseOptionalTimeFrame, parseProjectStatusForUpdate, parseUserRole, projectStatusesForHelp, userRolesForHelp, } from "./parse-cli-enums.js";
|
|
8
8
|
import { parseJsonFlag, parseOptionalNumber as parseOptNum, } from "./parse-json-flag.js";
|
|
9
9
|
function parsePositiveIntFlag(value, flagName) {
|
|
10
10
|
if (value === undefined)
|
|
@@ -133,12 +133,12 @@ Budgets
|
|
|
133
133
|
get-budget-details <id> Full detail (margins, suppliers, etc.)
|
|
134
134
|
get-budget-categories
|
|
135
135
|
get-budget-versions <budgetId>
|
|
136
|
-
update-budget-status <id> <status> [--markProjectWon true|false] [--projectStatusOnCommercialRejection PITCH|LOST]
|
|
136
|
+
update-budget-status <id> <status> [--projectManagerId <id>] [--markProjectWon true|false] [--projectStatusOnCommercialRejection PITCH|LOST]
|
|
137
137
|
Budget-changing commands require interactive "yes" confirmation when the current budget is ESTIMATE_CREATED, ESTIMATE_SENT, ESTIMATE_ACCEPTED, ESTIMATE_REJECTED, or ESTIMATE_CLOSED.
|
|
138
|
-
ESTIMATE_ACCEPTED requires win proof, auto-marks PITCH or LOST projects WON, and auto-creates the Asana project/section when enabled.
|
|
138
|
+
ESTIMATE_ACCEPTED requires win proof, auto-marks PITCH or LOST projects WON, requires --projectManagerId when the project has no PM, and auto-creates the Asana project/section when enabled.
|
|
139
139
|
ESTIMATE_REJECTED requires --projectStatusOnCommercialRejection when rejecting the only accepted/closed budget on a commercial project.
|
|
140
140
|
status: ${budgetStatusesForHelp.join(", ")}
|
|
141
|
-
mark-budget-won <budgetId> <filePath> [--markProjectWon true|false] Upload signed quote/PO proof and set status to ESTIMATE_ACCEPTED; PITCH or LOST projects become WON automatically
|
|
141
|
+
mark-budget-won <budgetId> <filePath> [--projectManagerId <id>] [--markProjectWon true|false] Upload signed quote/PO proof and set status to ESTIMATE_ACCEPTED; PITCH or LOST projects become WON automatically and require PM when none is assigned
|
|
142
142
|
create-budget --payload '<json>' (budget.createBudget; Asana deal card is on the project)
|
|
143
143
|
update-budget --payload '<json>' (budget.updateBudget; must include id)
|
|
144
144
|
delete-budget <budgetId>
|
|
@@ -171,7 +171,9 @@ Bills
|
|
|
171
171
|
Clear a field: --clearPaymentTrackingUrl true | --clearPaymentReference true | --clearQuickbooksBillId true | --clearPaymentDate true
|
|
172
172
|
patch-bill-invoice-number <billId> <invoiceNumber>
|
|
173
173
|
get-bill-attachments <billId>
|
|
174
|
-
upload-bill-attachment <billId> <filePath>
|
|
174
|
+
upload-bill-attachment <billId> <filePath...> [--file <path>] [--files <csv>] Attach invoice/supporting files as BILL attachments to any bill status, including PAID bills
|
|
175
|
+
attach-bill-payment-receipt <billId> <filePath...> [--file <path>] [--files <csv>] Attach Wise/payment-proof PDFs as BILL_PAYMENT_PROOF attachments
|
|
176
|
+
attach-bill-documents <billId> [--invoice <path>] [--invoices <csv>] [--paymentProof|--payment-proof|--proof <path>] [--paymentProofs|--payment-proofs|--proofs <csv>] Attach invoice and/or payment proof files with separate BB attachment types
|
|
175
177
|
get-bill-details <billId>
|
|
176
178
|
|
|
177
179
|
Quotations
|
|
@@ -203,7 +205,7 @@ Companies & projects
|
|
|
203
205
|
list-projects [--companyId] [--name] [--status] [--active true|false] [--page] [--perPage]
|
|
204
206
|
Omit --active to include both active and inactive projects.
|
|
205
207
|
get-project <id>
|
|
206
|
-
create-project --name --companyId --contactPersonId --insideSalesId --businessDevelopmentId --venue --asanaTaskId --slackChannelId --slackChannelUrl --slackChannelName
|
|
208
|
+
create-project --name --companyId --contactPersonId --insideSalesId --businessDevelopmentId --venue --startDate <ISO> [--asanaTaskId] [--asanaSearch <text>] [--slackChannelId --slackChannelUrl --slackChannelName] [--pax] [--endDate <ISO>] [--description]
|
|
207
209
|
update-project --payload '<json>' (project.updateProject; optional requestQboAccountantNotification; default notify like web)
|
|
208
210
|
delete-project <id>
|
|
209
211
|
update-project-status <id> <status> [--projectManagerId <id>] [--projectManagerName <name>] [--wonOverrideReason <text>]
|
|
@@ -241,6 +243,10 @@ Suppliers & items
|
|
|
241
243
|
|
|
242
244
|
Dashboard & users
|
|
243
245
|
list-users
|
|
246
|
+
create-user --email <email> [--name <name>] [--role ${userRolesForHelp.join("|")}] (admin; API-only service identity with no Google sign-in)
|
|
247
|
+
create-api-key --userId <userId> --name <label> (admin; raw key is shown once)
|
|
248
|
+
list-api-keys [--userId <userId>] (admin; defaults to the caller)
|
|
249
|
+
revoke-api-key <apiKeyId> [--userId <userId>] (admin; --userId is required for another user's key)
|
|
244
250
|
get-user-performance [--userId]
|
|
245
251
|
get-dashboard [--userId] [--role BD|CREATOR|INSIDE_SALES|ALL] [--deals ALL|SUCCESSFUL|LOST] [--timeFrame] [--startDate] [--endDate]
|
|
246
252
|
get-monthly-metrics [same optional flags as get-dashboard] (dashboard.getMonthlyMetrics)
|
|
@@ -336,6 +342,9 @@ async function main() {
|
|
|
336
342
|
throw new Error("mark-budget-won requires <budgetId> <filePath>");
|
|
337
343
|
}
|
|
338
344
|
await markBudgetWonWithProof(budgetId, filePath, {
|
|
345
|
+
projectManagerId: getFlag(flags, "projectManagerId") !== undefined
|
|
346
|
+
? String(getFlag(flags, "projectManagerId"))
|
|
347
|
+
: undefined,
|
|
339
348
|
markProjectWon: parseOptionalBoolFlag(flags, "markProjectWon"),
|
|
340
349
|
});
|
|
341
350
|
break;
|
|
@@ -374,6 +383,9 @@ async function main() {
|
|
|
374
383
|
throw new Error(`Invalid status. Use one of: ${budgetStatusesForHelp.join(", ")}`);
|
|
375
384
|
}
|
|
376
385
|
await updateBudgetStatus(id, status, {
|
|
386
|
+
projectManagerId: getFlag(flags, "projectManagerId") !== undefined
|
|
387
|
+
? String(getFlag(flags, "projectManagerId"))
|
|
388
|
+
: undefined,
|
|
377
389
|
markProjectWon: parseOptionalBoolFlag(flags, "markProjectWon"),
|
|
378
390
|
projectStatusOnCommercialRejection: parseCommercialRejectionProjectStatus(flags),
|
|
379
391
|
});
|
|
@@ -678,12 +690,71 @@ async function main() {
|
|
|
678
690
|
await getBillAttachments(id);
|
|
679
691
|
break;
|
|
680
692
|
}
|
|
681
|
-
case "upload-bill-attachment":
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
693
|
+
case "upload-bill-attachment":
|
|
694
|
+
case "attach-bill-attachment":
|
|
695
|
+
case "attach-bill-payment-receipt": {
|
|
696
|
+
const [billId, ...positionalFilePaths] = positional;
|
|
697
|
+
const fileFlag = getFlag(flags, "file");
|
|
698
|
+
const filesFlag = getFlag(flags, "files");
|
|
699
|
+
const filePaths = [
|
|
700
|
+
...positionalFilePaths,
|
|
701
|
+
...(fileFlag !== undefined ? [String(fileFlag)] : []),
|
|
702
|
+
...(filesFlag !== undefined
|
|
703
|
+
? String(filesFlag)
|
|
704
|
+
.split(",")
|
|
705
|
+
.map((value) => value.trim())
|
|
706
|
+
.filter(Boolean)
|
|
707
|
+
: []),
|
|
708
|
+
];
|
|
709
|
+
if (!billId || filePaths.length === 0) {
|
|
710
|
+
throw new Error(`${cmd} requires <billId> <filePath...> or --file/--files`);
|
|
711
|
+
}
|
|
712
|
+
const attachmentType = cmd === "attach-bill-payment-receipt" ? "BILL_PAYMENT_PROOF" : "BILL";
|
|
713
|
+
const [firstFilePath] = filePaths;
|
|
714
|
+
if (filePaths.length === 1 && firstFilePath !== undefined) {
|
|
715
|
+
await uploadBillAttachmentFromPath(billId, firstFilePath, attachmentType);
|
|
716
|
+
}
|
|
717
|
+
else {
|
|
718
|
+
await uploadBillAttachmentsFromPaths(billId, filePaths, attachmentType);
|
|
685
719
|
}
|
|
686
|
-
|
|
720
|
+
break;
|
|
721
|
+
}
|
|
722
|
+
case "attach-bill-documents": {
|
|
723
|
+
const billId = positional[0];
|
|
724
|
+
if (!billId)
|
|
725
|
+
throw new Error("attach-bill-documents requires <billId>");
|
|
726
|
+
const getFlagAlias = (...flagNames) => {
|
|
727
|
+
for (const flagName of flagNames) {
|
|
728
|
+
const value = getFlag(flags, flagName);
|
|
729
|
+
if (value !== undefined)
|
|
730
|
+
return value;
|
|
731
|
+
}
|
|
732
|
+
return undefined;
|
|
733
|
+
};
|
|
734
|
+
const parseFilesFlag = (...flagNames) => {
|
|
735
|
+
const raw = getFlagAlias(...flagNames);
|
|
736
|
+
if (raw === undefined)
|
|
737
|
+
return [];
|
|
738
|
+
return String(raw)
|
|
739
|
+
.split(",")
|
|
740
|
+
.map((value) => value.trim())
|
|
741
|
+
.filter(Boolean);
|
|
742
|
+
};
|
|
743
|
+
const invoicePath = getFlagAlias("invoice", "invoice-file");
|
|
744
|
+
const paymentProofPath = getFlagAlias("paymentProof", "payment-proof", "proof", "receipt");
|
|
745
|
+
const invoicePaths = [
|
|
746
|
+
...(invoicePath !== undefined ? [String(invoicePath)] : []),
|
|
747
|
+
...parseFilesFlag("invoices", "invoice-files"),
|
|
748
|
+
];
|
|
749
|
+
const paymentProofPaths = [
|
|
750
|
+
...(paymentProofPath !== undefined ? [String(paymentProofPath)] : []),
|
|
751
|
+
...parseFilesFlag("paymentProofs", "payment-proofs", "proofs", "receipts"),
|
|
752
|
+
];
|
|
753
|
+
await uploadBillDocumentsFromPaths({
|
|
754
|
+
billId,
|
|
755
|
+
invoicePaths,
|
|
756
|
+
paymentProofPaths,
|
|
757
|
+
});
|
|
687
758
|
break;
|
|
688
759
|
}
|
|
689
760
|
case "get-bill-details": {
|
|
@@ -908,6 +979,7 @@ async function main() {
|
|
|
908
979
|
const venue = getFlag(flags, "venue");
|
|
909
980
|
const pax = getFlag(flags, "pax");
|
|
910
981
|
const asanaTaskId = getFlag(flags, "asanaTaskId");
|
|
982
|
+
const asanaSearch = getFlag(flags, "asanaSearch");
|
|
911
983
|
const slackChannelId = getFlag(flags, "slackChannelId");
|
|
912
984
|
const slackChannelUrl = getFlag(flags, "slackChannelUrl");
|
|
913
985
|
const slackChannelName = getFlag(flags, "slackChannelName");
|
|
@@ -921,12 +993,8 @@ async function main() {
|
|
|
921
993
|
!insideSalesId ||
|
|
922
994
|
!businessDevelopmentId ||
|
|
923
995
|
!venue ||
|
|
924
|
-
!asanaTaskId ||
|
|
925
|
-
!slackChannelId ||
|
|
926
|
-
!slackChannelUrl ||
|
|
927
|
-
!slackChannelName ||
|
|
928
996
|
!startDate) {
|
|
929
|
-
throw new Error("create-project requires --name --companyId --contactPersonId --insideSalesId --businessDevelopmentId --venue --
|
|
997
|
+
throw new Error("create-project requires --name --companyId --contactPersonId --insideSalesId --businessDevelopmentId --venue --startDate (ISO/datetime flags as appropriate); optional --asanaTaskId, --asanaSearch, --pax and --endDate");
|
|
930
998
|
}
|
|
931
999
|
await createProject({
|
|
932
1000
|
name,
|
|
@@ -937,6 +1005,7 @@ async function main() {
|
|
|
937
1005
|
venue,
|
|
938
1006
|
pax,
|
|
939
1007
|
asanaTaskId,
|
|
1008
|
+
asanaSearch,
|
|
940
1009
|
slackChannelId,
|
|
941
1010
|
slackChannelUrl,
|
|
942
1011
|
slackChannelName,
|
|
@@ -1167,6 +1236,42 @@ async function main() {
|
|
|
1167
1236
|
await listUsers();
|
|
1168
1237
|
break;
|
|
1169
1238
|
}
|
|
1239
|
+
case "create-user": {
|
|
1240
|
+
const email = getFlag(flags, "email");
|
|
1241
|
+
if (!email) {
|
|
1242
|
+
throw new Error(`create-user requires --email <email> [--name <name>] [--role ${userRolesForHelp.join("|")}]`);
|
|
1243
|
+
}
|
|
1244
|
+
await createUser({
|
|
1245
|
+
email,
|
|
1246
|
+
name: getFlag(flags, "name"),
|
|
1247
|
+
role: parseUserRole(getFlag(flags, "role")),
|
|
1248
|
+
});
|
|
1249
|
+
break;
|
|
1250
|
+
}
|
|
1251
|
+
case "create-api-key": {
|
|
1252
|
+
const userId = getFlag(flags, "userId");
|
|
1253
|
+
const name = getFlag(flags, "name");
|
|
1254
|
+
if (!userId || !name) {
|
|
1255
|
+
throw new Error("create-api-key requires --userId <userId> --name <label>");
|
|
1256
|
+
}
|
|
1257
|
+
await createApiKeyForUser({ userId, name });
|
|
1258
|
+
break;
|
|
1259
|
+
}
|
|
1260
|
+
case "list-api-keys": {
|
|
1261
|
+
await listApiKeysForUser({ userId: getFlag(flags, "userId") });
|
|
1262
|
+
break;
|
|
1263
|
+
}
|
|
1264
|
+
case "revoke-api-key": {
|
|
1265
|
+
const id = positional[0];
|
|
1266
|
+
if (!id) {
|
|
1267
|
+
throw new Error("revoke-api-key requires <apiKeyId> [--userId <userId>]");
|
|
1268
|
+
}
|
|
1269
|
+
await revokeApiKeyForUser({
|
|
1270
|
+
id,
|
|
1271
|
+
userId: getFlag(flags, "userId"),
|
|
1272
|
+
});
|
|
1273
|
+
break;
|
|
1274
|
+
}
|
|
1170
1275
|
case "get-user-performance": {
|
|
1171
1276
|
await getUserPerformance(getFlag(flags, "userId"));
|
|
1172
1277
|
break;
|
package/dist/parse-cli-enums.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { BudgetRole, Deals, ExtendedApprovalStatus, ExtendedBillStatus, ExtendedErrorSeverity, ExtendedErrorStatus, ExtendedProjectStatus, TimeFrame, } from "./filter-enums.js";
|
|
2
|
-
import { BillStatus, BudgetStatus, ProjectStatus } from "./prisma-enums.js";
|
|
2
|
+
import { BillStatus, BudgetStatus, ProjectStatus, UserRole, } from "./prisma-enums.js";
|
|
3
3
|
const BUDGET_STATUS_VALUES = new Set(Object.values(BudgetStatus));
|
|
4
4
|
const BILL_STATUS_VALUES = new Set(Object.values(BillStatus));
|
|
5
5
|
const PROJECT_STATUS_VALUES = new Set(Object.values(ProjectStatus));
|
|
6
|
+
const USER_ROLE_VALUES = new Set(Object.values(UserRole));
|
|
6
7
|
const EXTENDED_PROJECT_STATUS_VALUES = new Set(Object.values(ExtendedProjectStatus));
|
|
7
8
|
const DASHBOARD_ROLE_VALUES = new Set([
|
|
8
9
|
"BD",
|
|
@@ -52,6 +53,15 @@ export function parseProjectStatusForUpdate(raw) {
|
|
|
52
53
|
}
|
|
53
54
|
return raw;
|
|
54
55
|
}
|
|
56
|
+
export function parseUserRole(raw) {
|
|
57
|
+
if (raw === undefined || raw === "")
|
|
58
|
+
return UserRole.USER;
|
|
59
|
+
const normalizedRole = raw.trim().toUpperCase();
|
|
60
|
+
if (!USER_ROLE_VALUES.has(normalizedRole)) {
|
|
61
|
+
throw new Error(`Invalid user role "${raw}". Use one of: ${[...USER_ROLE_VALUES].join(", ")}.`);
|
|
62
|
+
}
|
|
63
|
+
return normalizedRole;
|
|
64
|
+
}
|
|
55
65
|
export function parseOptionalExtendedProjectStatus(raw) {
|
|
56
66
|
if (raw === undefined || raw === "")
|
|
57
67
|
return undefined;
|
|
@@ -233,3 +243,4 @@ export const budgetStatusesForHelp = Object.values(BudgetStatus);
|
|
|
233
243
|
export const billStatusesForHelp = Object.values(BillStatus);
|
|
234
244
|
export const billStatusesForUpdateHelp = Object.values(BillStatus).filter((s) => s !== BillStatus.PENDING_APPROVAL);
|
|
235
245
|
export const projectStatusesForHelp = Object.values(ProjectStatus);
|
|
246
|
+
export const userRolesForHelp = Object.values(UserRole);
|
package/dist/prisma-enums.js
CHANGED
|
@@ -36,6 +36,13 @@ export const ApprovalType = {
|
|
|
36
36
|
CUSTOMER_INVOICE: "CUSTOMER_INVOICE",
|
|
37
37
|
QUOTATION: "QUOTATION",
|
|
38
38
|
};
|
|
39
|
+
export const UserRole = {
|
|
40
|
+
USER: "USER",
|
|
41
|
+
LEAD: "LEAD",
|
|
42
|
+
ADMIN: "ADMIN",
|
|
43
|
+
INSIDE_SALES: "INSIDE_SALES",
|
|
44
|
+
ACCOUNTING_TEAM: "ACCOUNTING_TEAM",
|
|
45
|
+
};
|
|
39
46
|
export const ErrorSeverity = {
|
|
40
47
|
LOW: "LOW",
|
|
41
48
|
MEDIUM: "MEDIUM",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@go-labs-sg/bb",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.17.0",
|
|
4
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",
|