@go-labs-sg/bb 2.45.1 → 2.47.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 +70 -7
- package/command-manifest.json +12 -0
- package/dist/index.js +402 -40
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,10 +1,22 @@
|
|
|
1
1
|
# bb
|
|
2
2
|
|
|
3
|
-
Command-line interface for **Budget Builder**. Call budgets, bills, claims, approvals, suppliers, dashboard, and more from your terminal or from
|
|
3
|
+
Command-line interface for **Budget Builder**. Call budgets, bills, claims, approvals, suppliers, dashboard, and more from your terminal or from AI agents and automation. The CLI is the maintained integration surface; the former MCP server is deprecated. Responses are JSON on stdout.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## First step: update before use
|
|
6
|
+
|
|
7
|
+
Before starting a task, **update to the latest published release first**, including when an AI agent runs this CLI:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
bun add --global @go-labs-sg/bb@latest
|
|
11
|
+
bb version
|
|
12
|
+
bb --help
|
|
13
|
+
```
|
|
6
14
|
|
|
7
|
-
|
|
15
|
+
Run this sequence once per task, not before every command. Re-read the updated help before choosing commands or flags. If updating fails, stop and report the failure rather than silently using an outdated CLI. If the user explicitly requested a pinned version or a development/staging build, preserve that version instead of replacing it with the public release.
|
|
16
|
+
|
|
17
|
+
New installation? Follow the runtime and installation instructions below to install Bun first, then return to this step.
|
|
18
|
+
|
|
19
|
+
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.
|
|
8
20
|
|
|
9
21
|
For AI agents using this CLI: run `bb auth whoami` before reading or changing Budget Builder data. The CLI displays every command regardless of the authenticated user's role, but the API authorizes each request using the current browser-authorized CLI user or automation API-key owner, resource ownership or project assignment, current workflow state, and approval assignment. See the [role-aware agent guide](./role-aware-agent-guide.md) before choosing commands.
|
|
10
22
|
|
|
@@ -12,6 +24,20 @@ For AI agents using this CLI: do not execute mutations until the user explicitly
|
|
|
12
24
|
|
|
13
25
|
**Registry:** [`@go-labs-sg/bb`](https://www.npmjs.com/package/@go-labs-sg/bb)
|
|
14
26
|
|
|
27
|
+
## Bill and claim list summaries
|
|
28
|
+
|
|
29
|
+
`bb bill list` / `bb claim list` (legacy `list-bills` / `list-claims`) use the updated summary-only `bill.getAll`. Lists return summary fields, `isClaimable`, `lineItemCount` and `hasPendingApproval`, plus the existing pagination, counts and metrics envelope. Filters, sorting and the CLI's existing summary output are unchanged. Lists do not return attachment, detailed line-item or approval arrays, full supplier/budget/project/company records, or detail-only payment/integration/verification metadata.
|
|
30
|
+
|
|
31
|
+
**Breaking API migration:** `bill.getList` is removed completely, with no alias or fallback. Direct scripts reading the former rich `bill.getAll` response must adopt summaries and fetch selected bill/claim details with `bb get-bill-details <billId>`. Do not fetch details for every list row unless the task actually needs them. No rich-list flag or automatic detail expansion is added.
|
|
32
|
+
|
|
33
|
+
Coordinate the matching API/web deployment and CLI release/installations. MCP is deprecated and is not a rollout or acceptance gate. Published CLI builds still calling `getList` receive `NOT_FOUND` after API rollout, so a mixed-revision deployment is not compatible. Update those installations to the matching CLI release. Budget-list options are unchanged. This source change does not publish the CLI or deploy the API.
|
|
34
|
+
|
|
35
|
+
## Inventory list reads
|
|
36
|
+
|
|
37
|
+
`bb item list` / `bb list-items` now use the updated page-only `item.getItems` and separate `item.getItemCounts`. The JSON output remains `{ items, pageCount, counts }`; filters and pagination are unchanged. Neither request carries an acting-user cache ID. The server authenticates the CLI session or API-key owner as usual.
|
|
38
|
+
|
|
39
|
+
The existing `item.getItems` API is updated in place: it now returns an array rather than `{ items, pageCount, counts }`. There is no separate `getItemPage` procedure. Coordinate the API/web deployment and CLI release/installations: old clients expecting the combined response must be upgraded, and new clients require the revised response and separate counts. Direct integrations should fetch counts only when needed. This PR does not publish the CLI or deploy the API.
|
|
40
|
+
|
|
15
41
|
## Company discovery and reuse
|
|
16
42
|
|
|
17
43
|
`bb company search --query "a star" [--page 2]` searches stored BB companies and QuickBooks company customers, including inactive records. Results identify `bb`, `qbo`, or `both`; punctuation/spacing normalization is for suggestions only. Search does not write to either system.
|
|
@@ -63,7 +89,7 @@ See the [supplier review and rollout policy](../api/docs/supplier-edit-review.md
|
|
|
63
89
|
|
|
64
90
|
## Runtime and installation
|
|
65
91
|
|
|
66
|
-
The same CLI package
|
|
92
|
+
The same CLI package targets macOS, Windows, and Linux; authentication depends on the secure backend described below. It requires [Bun](https://bun.com/docs/installation) 1.4.0 or newer and a Budget Builder account with any active user role. Windows users can run the commands below in native PowerShell; Bash and `export PATH=...` are not required.
|
|
67
93
|
|
|
68
94
|
Check whether a compatible Bun version is already available:
|
|
69
95
|
|
|
@@ -125,6 +151,16 @@ bb get-budget-details <budget-id> --includeInactive
|
|
|
125
151
|
|
|
126
152
|
`--active false` lists inactive budgets only. `--active all` lists active and inactive budgets. `--includeInactive` allows the three budget/detail/item commands to read inactive history. If the authenticated identity is not an Admin, the API rejects the historical request before reading budget data. For `get-budget`, the CLI requests the budget and its line items concurrently; each API read independently enforces the same active-only default and Admin-only historical opt-in.
|
|
127
153
|
|
|
154
|
+
## Secure credential storage
|
|
155
|
+
|
|
156
|
+
`GO_LABS_CLI_CREDENTIAL_ACCOUNT` selects the native credential-store account for this CLI. The default is `default`, preserving existing production logins. The downloadable staging launcher sets it to `staging` so a staging login does not replace the production login; keep the same account value for login and subsequent commands. The launcher also sets `GO_LABS_CLI_SKIP_DOTENV=1`, which skips the CLI's explicit current-directory `.env` load while leaving normal production behavior unchanged.
|
|
157
|
+
|
|
158
|
+
Browser sessions use `Bun.secrets`: macOS Keychain, Windows Credential Manager, or Linux Secret Service. Linux requires libsecret, a reachable user D-Bus session, and an unlocked Secret Service collection. SSH and containers must provide those native prerequisites themselves; `--no-browser` only lets approval happen on another device. Denied D-Bus access is a host permissions/session issue, not something the CLI bypasses.
|
|
159
|
+
|
|
160
|
+
Before requesting approval, login verifies write/read/delete capability with a unique non-secret probe. Native operations have a 10-second process deadline and sanitized errors. Issued credentials are read back and verified; failed persistence attempts remote revocation and reports local and remote cleanup separately. There is no plaintext fallback or external credential-helper configuration.
|
|
161
|
+
|
|
162
|
+
See the [shared authentication contract](../../cli-auth/README.md) for recovery and verification limits.
|
|
163
|
+
|
|
128
164
|
## Authentication
|
|
129
165
|
|
|
130
166
|
Interactive users authorize the CLI through Budget Builder's normal Google-backed
|
|
@@ -142,12 +178,12 @@ credentials through `Bun.secrets`. It never stores Google credentials. Use
|
|
|
142
178
|
`bb auth logout` to revoke the interactive session. For SSH or containers, pass
|
|
143
179
|
`--no-browser` and open the printed URL on another device.
|
|
144
180
|
|
|
145
|
-
Credentials use macOS Keychain
|
|
181
|
+
Credentials use macOS Keychain, Windows Credential Manager, or Linux Secret Service, not plaintext files.
|
|
146
182
|
The per-user lock directory contains only random ownership markers used to serialize
|
|
147
183
|
login, refresh, and logout. macOS/Linux enforce owner-only POSIX permissions there;
|
|
148
184
|
Windows uses its native filesystem access controls rather than POSIX mode checks.
|
|
149
185
|
|
|
150
|
-
Budget Builder API keys remain supported for
|
|
186
|
+
Budget Builder API keys remain supported for CI and non-interactive CLI
|
|
151
187
|
automation. When `BB_API_KEY` is present it takes precedence over the interactive
|
|
152
188
|
session for that process. Create and manage automation keys under **API Keys** or
|
|
153
189
|
with the commands below:
|
|
@@ -164,6 +200,16 @@ only. Revoking the key currently stored in `BB_API_KEY` causes that automation
|
|
|
164
200
|
process to fail until the environment credential is replaced or removed so the
|
|
165
201
|
interactive session can be used.
|
|
166
202
|
|
|
203
|
+
`bb user api-key list` and the `mcpApiKey.list` API retain the nullable
|
|
204
|
+
`lastUsedAt` field for compatibility (an ISO timestamp in CLI JSON). Interpret
|
|
205
|
+
only its calendar date in `Asia/Singapore` (UTC+08:00): it records the first
|
|
206
|
+
successful usage touch of that day, **not the most recent request time**.
|
|
207
|
+
Null means no use has been recorded; pre-rollout values may represent any use
|
|
208
|
+
on their recorded day. Do not use the time portion for inactivity timeouts,
|
|
209
|
+
minute-level mutation attribution, or request auditing. Authentication still
|
|
210
|
+
reads the key and current user ban status on every request; revocation does
|
|
211
|
+
not cancel requests already in flight.
|
|
212
|
+
|
|
167
213
|
Admins can provision API-only service identities and manage their keys from the
|
|
168
214
|
CLI. These identities are not human web accounts: `create-user` does not create
|
|
169
215
|
or link a Google OAuth account, so the resulting user cannot sign in with Google.
|
|
@@ -248,7 +294,7 @@ Global options and command flags use `--key=value` or `--key value`. Run `bb hel
|
|
|
248
294
|
|
|
249
295
|
Run `bb version` before agent-driven work to record the installed package version and production API target. This is read-only and does not require authentication.
|
|
250
296
|
|
|
251
|
-
**Authoritative command list:** run `bb help` or inspect the generated `command-reference.md` / `command-manifest.json`. The catalog includes every canonical route, global option, and effect classification, but does not filter by the current `whoami` role. During the handler migration, command-specific arguments retain the compatibility dispatcher's validation and are explicitly marked `legacy-passthrough` in the manifest; use `bb help --legacy` for their detailed transition reference. Version 2 uses grouped, resource-first commands such as `bb budget list` and `bb bill attachment upload`. Flat commands such as `bb list-budgets` and their
|
|
297
|
+
**Authoritative command list:** run `bb help` or inspect the generated `command-reference.md` / `command-manifest.json`. The catalog includes every canonical route, global option, and effect classification, but does not filter by the current `whoami` role. During the handler migration, command-specific arguments retain the compatibility dispatcher's validation and are explicitly marked `legacy-passthrough` in the manifest; use `bb help --legacy` for their detailed transition reference. Version 2 uses grouped, resource-first commands such as `bb budget list` and `bb bill attachment upload`. Flat commands such as `bb list-budgets` and their legacy `snake_case` aliases remain available during the v2 transition, with a deprecation notice and an equivalent grouped command in help. The CLI calls the shared tRPC API, including procedures also used by the web UI.
|
|
252
298
|
|
|
253
299
|
**Budget status automation:** Setting a budget to `ESTIMATE_ACCEPTED` requires a confirmed win-proof attachment. `--invoiceSettings '<json>'` remains available when the project needs saved delivery preferences, but invoice creation chooses its amount independently from those settings. If the parent project is `PITCH` or `LOST`, the API marks it `WON` automatically and runs enabled Asana automation. When rejecting the only accepted/closed budget on a commercial project, pass `--projectStatusOnCommercialRejection PITCH|LOST`.
|
|
254
300
|
|
|
@@ -440,6 +486,23 @@ bb quotation create --payload '{
|
|
|
440
486
|
Use the same fields for `bb quotation update --payload`, adding the existing quotation `"id": "<quotation-id>"`. Read the quotation and its budget before updating so the IDs and current status are confirmed. The example's subtotal is S$1,200, GST is S$0, and total is S$1,200; those values are illustrative only and contain no live resource IDs.
|
|
441
487
|
|
|
442
488
|
|
|
489
|
+
### Budget version history (pagination contract change)
|
|
490
|
+
|
|
491
|
+
`bb budget version list <budgetId>` now returns **one page**, not the full history array. The normal CLI envelope contains `data: { versions, nextCursor }`; legacy aliases `get-budget-versions` and `get_budget_versions` return that page object directly. Update scripts that previously iterated the top-level array to iterate `versions` and explicitly request older pages when needed.
|
|
492
|
+
|
|
493
|
+
```bash
|
|
494
|
+
bb budget version list <budgetId>
|
|
495
|
+
bb budget version list <budgetId> --limit 20 --cursor <nextCursor>
|
|
496
|
+
bb budget version list <budgetId> --all
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
- Default page size: 20; `--limit` accepts integers from 1 to 50.
|
|
500
|
+
- `nextCursor: null` means no older visible versions. Otherwise pass it as `--cursor` for the next page; do not automatically drain history for ordinary browsing.
|
|
501
|
+
- `--all` explicitly collects all visible history through sequential bounded requests. It cannot be combined with `--cursor`; `--limit` controls each request's size. It can still be expensive and retains the complete result in memory. A failed page aborts without emitting a successful partial export.
|
|
502
|
+
- Pages preserve newest-first `createdAt`/`id` ordering, role visibility, and creator/version-group metadata. They are not a point-in-time database snapshot; versions added after traversal starts require a fresh first-page read, and concurrent changes to visibility can affect traversal.
|
|
503
|
+
|
|
504
|
+
The backend must expose `budgetVersion.getBudgetVersionsPage`. The legacy unbounded tRPC `getBudgetVersions` remains available but deprecated for external compatibility; unknown external clients are not silently capped. Removing that endpoint requires a separate usage/compatibility review. This change does not publish the CLI, deploy the backend, delete audit snapshots, or establish measured RU savings.
|
|
505
|
+
|
|
443
506
|
### Link an existing QuickBooks invoice
|
|
444
507
|
|
|
445
508
|
**Release status:** these CLI commands are pending release. The repository package version remains `2.31.0`; that version number alone does not establish that a published CLI contains them. Check `bb help` for `customer-invoice preview-qbo` and `customer-invoice import-qbo` after the normal CLI release. This code delivery does not publish the CLI, deploy the API, or perform a production import. The backend must expose `customerInvoice.previewExistingInvoice` and `customerInvoice.linkExistingInvoice` (implemented in PR #1067).
|
package/command-manifest.json
CHANGED
|
@@ -606,6 +606,18 @@
|
|
|
606
606
|
{
|
|
607
607
|
"name": "--allow-financial-write",
|
|
608
608
|
"description": "Allow financial-record changes in non-interactive use."
|
|
609
|
+
},
|
|
610
|
+
{
|
|
611
|
+
"name": "--limit <1-50>",
|
|
612
|
+
"description": "Versions per page (default 20). Returns versions and nextCursor, not a full-history array."
|
|
613
|
+
},
|
|
614
|
+
{
|
|
615
|
+
"name": "--cursor <versionId>",
|
|
616
|
+
"description": "Load the next page using the previous nextCursor; null means complete."
|
|
617
|
+
},
|
|
618
|
+
{
|
|
619
|
+
"name": "--all",
|
|
620
|
+
"description": "Explicitly fetch all visible history in bounded pages; cannot combine with --cursor. May be expensive."
|
|
609
621
|
}
|
|
610
622
|
],
|
|
611
623
|
"argumentMode": "legacy-passthrough",
|
package/dist/index.js
CHANGED
|
@@ -468,7 +468,9 @@ var require_dist = __commonJS(function(exports, module) {
|
|
|
468
468
|
// src/load-env.ts
|
|
469
469
|
var import_dotenv = __toESM(require_dist(), 1);
|
|
470
470
|
import path from "path";
|
|
471
|
-
|
|
471
|
+
if (process.env.GO_LABS_CLI_SKIP_DOTENV !== "1") {
|
|
472
|
+
import_dotenv.config({ path: path.resolve(process.cwd(), ".env"), quiet: true });
|
|
473
|
+
}
|
|
472
474
|
|
|
473
475
|
// ../../cli-auth/src/client/credential-store.ts
|
|
474
476
|
import { createHash as createHash2, randomUUID } from "crypto";
|
|
@@ -1483,6 +1485,8 @@ function datetime(args) {
|
|
|
1483
1485
|
}
|
|
1484
1486
|
var anyString = /^[\s\S]{0,}$/;
|
|
1485
1487
|
var number = /^-?\d+(?:\.\d+)?$/;
|
|
1488
|
+
var boolean = /^(?:true|false)$/i;
|
|
1489
|
+
var _null = /^null$/i;
|
|
1486
1490
|
var lowercase = /^[^A-Z]*$/;
|
|
1487
1491
|
var uppercase = /^[^a-z]*$/;
|
|
1488
1492
|
|
|
@@ -2364,6 +2368,43 @@ var $ZodNumberFormat = /* @__PURE__ */ $constructor("$ZodNumberFormat", (inst, d
|
|
|
2364
2368
|
$ZodCheckNumberFormat.init(inst, def);
|
|
2365
2369
|
$ZodNumber.init(inst, def);
|
|
2366
2370
|
});
|
|
2371
|
+
var $ZodBoolean = /* @__PURE__ */ $constructor("$ZodBoolean", (inst, def) => {
|
|
2372
|
+
$ZodType.init(inst, def);
|
|
2373
|
+
inst._zod.pattern = boolean;
|
|
2374
|
+
inst._zod.parse = (payload, _ctx) => {
|
|
2375
|
+
if (def.coerce)
|
|
2376
|
+
try {
|
|
2377
|
+
payload.value = Boolean(payload.value);
|
|
2378
|
+
} catch (_) {}
|
|
2379
|
+
const input = payload.value;
|
|
2380
|
+
if (typeof input === "boolean")
|
|
2381
|
+
return payload;
|
|
2382
|
+
payload.issues.push({
|
|
2383
|
+
expected: "boolean",
|
|
2384
|
+
code: "invalid_type",
|
|
2385
|
+
input,
|
|
2386
|
+
inst
|
|
2387
|
+
});
|
|
2388
|
+
return payload;
|
|
2389
|
+
};
|
|
2390
|
+
});
|
|
2391
|
+
var $ZodNull = /* @__PURE__ */ $constructor("$ZodNull", (inst, def) => {
|
|
2392
|
+
$ZodType.init(inst, def);
|
|
2393
|
+
inst._zod.pattern = _null;
|
|
2394
|
+
inst._zod.values = new Set([null]);
|
|
2395
|
+
inst._zod.parse = (payload, _ctx) => {
|
|
2396
|
+
const input = payload.value;
|
|
2397
|
+
if (input === null)
|
|
2398
|
+
return payload;
|
|
2399
|
+
payload.issues.push({
|
|
2400
|
+
expected: "null",
|
|
2401
|
+
code: "invalid_type",
|
|
2402
|
+
input,
|
|
2403
|
+
inst
|
|
2404
|
+
});
|
|
2405
|
+
return payload;
|
|
2406
|
+
};
|
|
2407
|
+
});
|
|
2367
2408
|
var $ZodUnknown = /* @__PURE__ */ $constructor("$ZodUnknown", (inst, def) => {
|
|
2368
2409
|
$ZodType.init(inst, def);
|
|
2369
2410
|
inst._zod.parse = (payload) => payload;
|
|
@@ -3901,6 +3942,18 @@ function _int(Class, params) {
|
|
|
3901
3942
|
...normalizeParams(params)
|
|
3902
3943
|
});
|
|
3903
3944
|
}
|
|
3945
|
+
function _boolean(Class, params) {
|
|
3946
|
+
return new Class({
|
|
3947
|
+
type: "boolean",
|
|
3948
|
+
...normalizeParams(params)
|
|
3949
|
+
});
|
|
3950
|
+
}
|
|
3951
|
+
function _null2(Class, params) {
|
|
3952
|
+
return new Class({
|
|
3953
|
+
type: "null",
|
|
3954
|
+
...normalizeParams(params)
|
|
3955
|
+
});
|
|
3956
|
+
}
|
|
3904
3957
|
function _unknown(Class) {
|
|
3905
3958
|
return new Class({
|
|
3906
3959
|
type: "unknown"
|
|
@@ -4783,6 +4836,18 @@ var numberProcessor = (schema, ctx, _json, params) => {
|
|
|
4783
4836
|
json.allOf = [...json.allOf ?? [], ...rest.map((m) => ({ multipleOf: m }))];
|
|
4784
4837
|
}
|
|
4785
4838
|
};
|
|
4839
|
+
var booleanProcessor = (_schema, _ctx, json, _params) => {
|
|
4840
|
+
json.type = "boolean";
|
|
4841
|
+
};
|
|
4842
|
+
var nullProcessor = (_schema, ctx, json, _params) => {
|
|
4843
|
+
if (ctx.target === "openapi-3.0") {
|
|
4844
|
+
json.type = "string";
|
|
4845
|
+
json.nullable = true;
|
|
4846
|
+
json.enum = [null];
|
|
4847
|
+
} else {
|
|
4848
|
+
json.type = "null";
|
|
4849
|
+
}
|
|
4850
|
+
};
|
|
4786
4851
|
var neverProcessor = (_schema, _ctx, json, _params) => {
|
|
4787
4852
|
json.not = {};
|
|
4788
4853
|
};
|
|
@@ -5577,6 +5642,22 @@ var ZodNumberFormat = /* @__PURE__ */ $constructor("ZodNumberFormat", (inst, def
|
|
|
5577
5642
|
function int(params) {
|
|
5578
5643
|
return _int(ZodNumberFormat, params);
|
|
5579
5644
|
}
|
|
5645
|
+
var ZodBoolean = /* @__PURE__ */ $constructor("ZodBoolean", (inst, def) => {
|
|
5646
|
+
$ZodBoolean.init(inst, def);
|
|
5647
|
+
ZodType.init(inst, def);
|
|
5648
|
+
inst._zod.processJSONSchema = (ctx, json, params) => booleanProcessor(inst, ctx, json, params);
|
|
5649
|
+
});
|
|
5650
|
+
function boolean2(params) {
|
|
5651
|
+
return _boolean(ZodBoolean, params);
|
|
5652
|
+
}
|
|
5653
|
+
var ZodNull = /* @__PURE__ */ $constructor("ZodNull", (inst, def) => {
|
|
5654
|
+
$ZodNull.init(inst, def);
|
|
5655
|
+
ZodType.init(inst, def);
|
|
5656
|
+
inst._zod.processJSONSchema = (ctx, json, params) => nullProcessor(inst, ctx, json, params);
|
|
5657
|
+
});
|
|
5658
|
+
function _null3(params) {
|
|
5659
|
+
return _null2(ZodNull, params);
|
|
5660
|
+
}
|
|
5580
5661
|
var ZodUnknown = /* @__PURE__ */ $constructor("ZodUnknown", (inst, def) => {
|
|
5581
5662
|
$ZodUnknown.init(inst, def);
|
|
5582
5663
|
ZodType.init(inst, def);
|
|
@@ -5995,6 +6076,9 @@ var storedCliCredentialsSchema = object({
|
|
|
5995
6076
|
session: cliSessionIdentitySchema
|
|
5996
6077
|
}).strict();
|
|
5997
6078
|
var cliProductSchema = _enum(cliProducts);
|
|
6079
|
+
// ../../cli-auth/src/client/secret-backend.ts
|
|
6080
|
+
import { execFile } from "child_process";
|
|
6081
|
+
|
|
5998
6082
|
// ../../cli-auth/src/client/runtime.ts
|
|
5999
6083
|
var MINIMUM_BUN_VERSION = [1, 4, 0];
|
|
6000
6084
|
var parseBunVersion = (version) => {
|
|
@@ -6020,27 +6104,145 @@ var assertBunRuntime = (runtime = globalThis.Bun) => {
|
|
|
6020
6104
|
}
|
|
6021
6105
|
};
|
|
6022
6106
|
|
|
6023
|
-
// ../../cli-auth/src/client/
|
|
6107
|
+
// ../../cli-auth/src/client/storage-error.ts
|
|
6108
|
+
var guidance = {
|
|
6109
|
+
"missing-library": "Install the runtime's native credential-store library and ensure your own native keyring/session is accessible and unlocked; Linux desktop storage requires libsecret AND an accessible, unlocked Secret Service.",
|
|
6110
|
+
"bus-unavailable": "The Linux user D-Bus session is absent or unreachable. --no-browser does not provide storage. Start or repair your own user session and native keyring before retrying.",
|
|
6111
|
+
"bus-denied": "Access to the Linux user bus was denied. Fix your own native keyring/session permissions or host policy; do not change bus/socket permissions or borrow another user's bus.",
|
|
6112
|
+
"service-unavailable": "The user bus is reachable but your own Secret Service is unavailable. Start or repair your own native keyring session; do not expose another user's store.",
|
|
6113
|
+
locked: "Unlock your own native credential collection/session before retrying. The CLI will not wait indefinitely for an unlock prompt.",
|
|
6114
|
+
timeout: "The native credential backend did not finish within 10 seconds and its process was terminated. Fix or unlock your own native keyring/session outside the CLI; the CLI will not bypass host permissions.",
|
|
6115
|
+
"invalid-response": "The native credential backend returned an invalid response. Check your own keyring/session and configuration; no credential output was disclosed.",
|
|
6116
|
+
"read-failed": "The native credential backend could not read credentials. Check your own keyring/session availability and permissions; this is not evidence that you are logged out.",
|
|
6117
|
+
"write-failed": "The native credential backend could not persist credentials. Check your own keyring/session write permissions and availability; no plaintext fallback is used.",
|
|
6118
|
+
"delete-failed": "The native credential backend could not delete its entry. Check your own keyring/session permissions and remove any non-secret preflight probe through that backend if necessary."
|
|
6119
|
+
};
|
|
6120
|
+
|
|
6024
6121
|
class CliCredentialStoreUnavailableError extends Error {
|
|
6025
6122
|
code = "CLI_CREDENTIAL_STORE_UNAVAILABLE";
|
|
6026
|
-
|
|
6027
|
-
|
|
6123
|
+
reason;
|
|
6124
|
+
constructor(product, reason = "read-failed") {
|
|
6125
|
+
super(`Secure credential storage is unavailable for the ${product} CLI [CLI_CREDENTIAL_STORE_UNAVAILABLE:${reason}]. ${guidance[reason]}`);
|
|
6028
6126
|
this.name = "CliCredentialStoreUnavailableError";
|
|
6029
|
-
|
|
6127
|
+
this.reason = reason;
|
|
6128
|
+
}
|
|
6129
|
+
}
|
|
6130
|
+
var classifyCredentialStoreFailure = (message, fallback) => {
|
|
6131
|
+
if (/libsecret.*(not found|not available|cannot open|load)|dlopen|shared (object|library).*not found/i.test(message))
|
|
6132
|
+
return "missing-library";
|
|
6133
|
+
if (/(d-?bus|user bus|session bus).*(denied|not permitted)|(?:denied|not permitted).*(d-?bus|user bus|session bus)/i.test(message))
|
|
6134
|
+
return "bus-denied";
|
|
6135
|
+
if (/locked|interaction.*not allowed|user.*cancel/i.test(message))
|
|
6136
|
+
return "locked";
|
|
6137
|
+
if (/ServiceUnknown|org\.freedesktop\.secrets.*(not provided|no owner|not available)|secret.?service.*(unavailable|not running)/i.test(message))
|
|
6138
|
+
return "service-unavailable";
|
|
6139
|
+
if (/d-?bus|user bus|session bus|autolaunch/i.test(message))
|
|
6140
|
+
return "bus-unavailable";
|
|
6141
|
+
return fallback;
|
|
6142
|
+
};
|
|
6143
|
+
|
|
6144
|
+
class CliCredentialPersistenceError extends Error {
|
|
6145
|
+
remoteRevoked;
|
|
6146
|
+
localRestored;
|
|
6147
|
+
phase;
|
|
6148
|
+
code = "CLI_CREDENTIAL_PERSISTENCE_FAILED";
|
|
6149
|
+
constructor(remoteRevoked, localRestored, phase) {
|
|
6150
|
+
super(`CLI credentials could not be saved after ${phase} [CLI_CREDENTIAL_PERSISTENCE_FAILED]. ${remoteRevoked ? "The newly issued session was revoked." : "Remote revocation could not be confirmed; revoke the session in the product's session management before retrying."} ${localRestored ? phase === "login" ? "The previous local credentials were preserved." : "The unusable rotated local credentials were removed; sign in again." : "Local cleanup could not be confirmed. Inspect your own native credential backend before retrying."}`);
|
|
6151
|
+
this.remoteRevoked = remoteRevoked;
|
|
6152
|
+
this.localRestored = localRestored;
|
|
6153
|
+
this.phase = phase;
|
|
6154
|
+
this.name = "CliCredentialPersistenceError";
|
|
6155
|
+
}
|
|
6156
|
+
}
|
|
6157
|
+
var getCliStorageError = (error) => {
|
|
6158
|
+
if (error instanceof CliCredentialStoreUnavailableError)
|
|
6159
|
+
return {
|
|
6160
|
+
code: error.code,
|
|
6161
|
+
message: error.message,
|
|
6162
|
+
details: { reason: error.reason }
|
|
6163
|
+
};
|
|
6164
|
+
if (error instanceof CliCredentialPersistenceError)
|
|
6165
|
+
return {
|
|
6166
|
+
code: error.code,
|
|
6167
|
+
message: error.message,
|
|
6168
|
+
details: {
|
|
6169
|
+
remoteRevoked: error.remoteRevoked,
|
|
6170
|
+
localRestored: error.localRestored,
|
|
6171
|
+
phase: error.phase
|
|
6172
|
+
}
|
|
6173
|
+
};
|
|
6174
|
+
return;
|
|
6175
|
+
};
|
|
6176
|
+
|
|
6177
|
+
// ../../cli-auth/src/client/secret-backend.ts
|
|
6178
|
+
var timeoutMs = 1e4;
|
|
6179
|
+
var maxBuffer = 64 * 1024;
|
|
6180
|
+
var runBackend = (product, executable, args, input, failure) => new Promise((resolve, reject) => {
|
|
6181
|
+
const child = execFile(executable, args, {
|
|
6182
|
+
timeout: timeoutMs,
|
|
6183
|
+
killSignal: "SIGKILL",
|
|
6184
|
+
maxBuffer,
|
|
6185
|
+
windowsHide: true
|
|
6186
|
+
}, (error, stdout, stderr) => {
|
|
6187
|
+
if (error) {
|
|
6188
|
+
const reason = error.code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER" ? "invalid-response" : error.killed ? "timeout" : classifyCredentialStoreFailure(`${stdout}
|
|
6189
|
+
${stderr}`, failure);
|
|
6190
|
+
reject(new CliCredentialStoreUnavailableError(product, reason));
|
|
6191
|
+
return;
|
|
6192
|
+
}
|
|
6193
|
+
resolve(stdout);
|
|
6194
|
+
});
|
|
6195
|
+
child.stdin?.on("error", () => {});
|
|
6196
|
+
child.stdin?.end(input);
|
|
6197
|
+
});
|
|
6198
|
+
var nativeWorker = `
|
|
6199
|
+
try {
|
|
6200
|
+
const { operation, input } = JSON.parse(await Bun.stdin.text());
|
|
6201
|
+
const value = await Bun.secrets[operation](input);
|
|
6202
|
+
process.stdout.write(JSON.stringify({ value: value ?? null }));
|
|
6203
|
+
} catch (error) {
|
|
6204
|
+
const message = error instanceof Error ? error.message : "Credential backend failed";
|
|
6205
|
+
// libsecret's bus transport errors can omit the D-Bus context.
|
|
6206
|
+
const context = process.platform === "linux" && /^Could not connect:/i.test(message) ? "D-Bus: " : "";
|
|
6207
|
+
process.stderr.write(context + message);
|
|
6208
|
+
process.exitCode = 1;
|
|
6030
6209
|
}
|
|
6031
|
-
|
|
6210
|
+
`;
|
|
6211
|
+
var nativeResponse = object({ value: union([string2(), boolean2(), _null3()]) }).strict();
|
|
6212
|
+
var createIsolatedBunSecrets = (product) => {
|
|
6213
|
+
const run = async (operation, input) => {
|
|
6214
|
+
assertBunRuntime();
|
|
6215
|
+
const output = await runBackend(product, process.execPath, ["--no-env-file", "--eval", nativeWorker], JSON.stringify({ operation, input }), operation === "set" ? "write-failed" : operation === "delete" ? "delete-failed" : "read-failed");
|
|
6216
|
+
try {
|
|
6217
|
+
return nativeResponse.parse(JSON.parse(output)).value;
|
|
6218
|
+
} catch {
|
|
6219
|
+
throw new CliCredentialStoreUnavailableError(product, "invalid-response");
|
|
6220
|
+
}
|
|
6221
|
+
};
|
|
6222
|
+
return {
|
|
6223
|
+
get: async (input) => {
|
|
6224
|
+
const value = await run("get", input);
|
|
6225
|
+
if (value === null || typeof value === "string")
|
|
6226
|
+
return value;
|
|
6227
|
+
throw new CliCredentialStoreUnavailableError(product, "invalid-response");
|
|
6228
|
+
},
|
|
6229
|
+
set: async (input) => {
|
|
6230
|
+
await run("set", input);
|
|
6231
|
+
},
|
|
6232
|
+
delete: async (input) => await run("delete", input) === true
|
|
6233
|
+
};
|
|
6234
|
+
};
|
|
6235
|
+
|
|
6236
|
+
// ../../cli-auth/src/client/credential-store.ts
|
|
6237
|
+
var runSecretOperation = async (product, operation, fallback = "read-failed") => {
|
|
6032
6238
|
try {
|
|
6033
6239
|
return await operation();
|
|
6034
6240
|
} catch (cause) {
|
|
6035
6241
|
if (cause instanceof CliCredentialStoreUnavailableError)
|
|
6036
6242
|
throw cause;
|
|
6037
|
-
throw new CliCredentialStoreUnavailableError(product,
|
|
6243
|
+
throw new CliCredentialStoreUnavailableError(product, classifyCredentialStoreFailure(cause instanceof Error ? cause.message : "", fallback));
|
|
6038
6244
|
}
|
|
6039
6245
|
};
|
|
6040
|
-
var defaultSecrets = () => {
|
|
6041
|
-
assertBunRuntime();
|
|
6042
|
-
return Bun.secrets;
|
|
6043
|
-
};
|
|
6044
6246
|
var refreshLockTimeoutMs = 5 * 60 * 1000;
|
|
6045
6247
|
var refreshLockRetryMs = 50;
|
|
6046
6248
|
var getCredentialLockDirectoryPath = () => {
|
|
@@ -6112,18 +6314,38 @@ var acquireRefreshLock = async (path) => {
|
|
|
6112
6314
|
}
|
|
6113
6315
|
throw new Error("Timed out waiting for another CLI process to refresh credentials. Retry the command.");
|
|
6114
6316
|
};
|
|
6317
|
+
var resolveCliCredentialAccount = (environment = process.env) => environment.GO_LABS_CLI_CREDENTIAL_ACCOUNT?.trim() || "default";
|
|
6115
6318
|
var createBunCredentialStore = ({
|
|
6116
|
-
account =
|
|
6319
|
+
account = resolveCliCredentialAccount(),
|
|
6117
6320
|
product,
|
|
6118
|
-
secrets =
|
|
6321
|
+
secrets = createIsolatedBunSecrets(product)
|
|
6119
6322
|
}) => {
|
|
6120
6323
|
const service = `go-labs-cli:${product}`;
|
|
6121
6324
|
return {
|
|
6325
|
+
preflight: async () => {
|
|
6326
|
+
const input = { name: `probe-${randomUUID()}`, service };
|
|
6327
|
+
const value = `go-labs-non-secret-probe:${randomUUID()}`;
|
|
6328
|
+
try {
|
|
6329
|
+
await runSecretOperation(product, () => secrets.set({ ...input, value }), "write-failed");
|
|
6330
|
+
if (await runSecretOperation(product, () => secrets.get(input)) !== value) {
|
|
6331
|
+
throw new CliCredentialStoreUnavailableError(product, "write-failed");
|
|
6332
|
+
}
|
|
6333
|
+
} catch (error) {
|
|
6334
|
+
await secrets.delete(input).catch(() => {
|
|
6335
|
+
return;
|
|
6336
|
+
});
|
|
6337
|
+
throw error;
|
|
6338
|
+
}
|
|
6339
|
+
await runSecretOperation(product, () => secrets.delete(input), "delete-failed");
|
|
6340
|
+
if (await runSecretOperation(product, () => secrets.get(input)) !== null) {
|
|
6341
|
+
throw new CliCredentialStoreUnavailableError(product, "delete-failed");
|
|
6342
|
+
}
|
|
6343
|
+
},
|
|
6122
6344
|
delete: async () => {
|
|
6123
|
-
await
|
|
6345
|
+
await runSecretOperation(product, () => secrets.delete({ name: account, service }), "delete-failed");
|
|
6124
6346
|
},
|
|
6125
6347
|
get: async () => {
|
|
6126
|
-
const value = await
|
|
6348
|
+
const value = await runSecretOperation(product, () => secrets.get({ name: account, service }));
|
|
6127
6349
|
if (value === null)
|
|
6128
6350
|
return null;
|
|
6129
6351
|
try {
|
|
@@ -6134,11 +6356,11 @@ var createBunCredentialStore = ({
|
|
|
6134
6356
|
},
|
|
6135
6357
|
set: async (credentials) => {
|
|
6136
6358
|
const parsed = storedCliCredentialsSchema.parse(credentials);
|
|
6137
|
-
await
|
|
6359
|
+
await runSecretOperation(product, () => secrets.set({
|
|
6138
6360
|
name: account,
|
|
6139
6361
|
service,
|
|
6140
6362
|
value: JSON.stringify(parsed)
|
|
6141
|
-
}));
|
|
6363
|
+
}), "write-failed");
|
|
6142
6364
|
},
|
|
6143
6365
|
withCredentialLock: async (operation) => {
|
|
6144
6366
|
const refreshLockPath = await getCredentialLockPath(product, account);
|
|
@@ -6159,6 +6381,7 @@ var createBunCredentialStore = ({
|
|
|
6159
6381
|
};
|
|
6160
6382
|
// ../../cli-auth/src/client/login.ts
|
|
6161
6383
|
import { hostname } from "os";
|
|
6384
|
+
import { isDeepStrictEqual } from "util";
|
|
6162
6385
|
var normalizeCliAuthIssuer = (value) => {
|
|
6163
6386
|
const url = new URL(value);
|
|
6164
6387
|
url.hash = "";
|
|
@@ -6221,12 +6444,15 @@ var openSystemBrowser = async (url) => {
|
|
|
6221
6444
|
"-NoProfile",
|
|
6222
6445
|
"-NonInteractive",
|
|
6223
6446
|
"-Command",
|
|
6224
|
-
"Start-Process -FilePath $
|
|
6225
|
-
url
|
|
6447
|
+
"Start-Process -FilePath $env:GO_LABS_CLI_VERIFICATION_URI -ErrorAction Stop"
|
|
6226
6448
|
] : ["xdg-open", url];
|
|
6227
6449
|
try {
|
|
6228
|
-
const
|
|
6229
|
-
|
|
6450
|
+
const browserProcess = Bun.spawn(command, {
|
|
6451
|
+
env: process.platform === "win32" ? { ...process.env, GO_LABS_CLI_VERIFICATION_URI: url } : process.env,
|
|
6452
|
+
stderr: "ignore",
|
|
6453
|
+
stdout: "ignore"
|
|
6454
|
+
});
|
|
6455
|
+
return await browserProcess.exited === 0;
|
|
6230
6456
|
} catch {
|
|
6231
6457
|
return false;
|
|
6232
6458
|
}
|
|
@@ -6244,6 +6470,8 @@ var loginWithBrowser = async ({
|
|
|
6244
6470
|
store
|
|
6245
6471
|
}) => {
|
|
6246
6472
|
const baseUrl = normalizeCliAuthIssuer(apiBaseUrl);
|
|
6473
|
+
if (store.preflight)
|
|
6474
|
+
await withCredentialLock(store, () => store.preflight?.() ?? Promise.resolve());
|
|
6247
6475
|
const pkce = createPkcePair();
|
|
6248
6476
|
const beginResponse = await postJson(fetchImplementation, `${baseUrl}/api/cli-auth/device`, {
|
|
6249
6477
|
clientName,
|
|
@@ -6271,7 +6499,19 @@ var loginWithBrowser = async ({
|
|
|
6271
6499
|
if (response.ok) {
|
|
6272
6500
|
const issued = tokenResponseSchema.parse(payload);
|
|
6273
6501
|
const credentials = toStoredCredentials(issued, baseUrl, now());
|
|
6274
|
-
|
|
6502
|
+
try {
|
|
6503
|
+
await withCredentialLock(store, () => persistIssuedCredentials({
|
|
6504
|
+
store,
|
|
6505
|
+
credentials,
|
|
6506
|
+
apiBaseUrl: baseUrl,
|
|
6507
|
+
fetch: fetchImplementation,
|
|
6508
|
+
phase: "login"
|
|
6509
|
+
}));
|
|
6510
|
+
} catch (error) {
|
|
6511
|
+
if (error instanceof CliCredentialPersistenceError)
|
|
6512
|
+
throw error;
|
|
6513
|
+
throw new CliCredentialPersistenceError(await revokeIssuedCredentials(fetchImplementation, baseUrl, credentials), false, "login");
|
|
6514
|
+
}
|
|
6275
6515
|
return credentials;
|
|
6276
6516
|
}
|
|
6277
6517
|
const error = deviceTokenErrorSchema.safeParse(payload);
|
|
@@ -6291,6 +6531,55 @@ var loginWithBrowser = async ({
|
|
|
6291
6531
|
}
|
|
6292
6532
|
throw new Error("CLI authentication expired. Run auth login again.");
|
|
6293
6533
|
};
|
|
6534
|
+
var revokeIssuedCredentials = async (fetchImplementation, apiBaseUrl, credentials) => {
|
|
6535
|
+
try {
|
|
6536
|
+
const response = await postJson(fetchImplementation, `${normalizeCliAuthIssuer(apiBaseUrl)}/api/cli-auth/revoke`, { refreshToken: credentials.refreshToken }, { signal: AbortSignal.timeout(refreshRequestTimeoutMs) });
|
|
6537
|
+
return response.ok;
|
|
6538
|
+
} catch {
|
|
6539
|
+
return false;
|
|
6540
|
+
}
|
|
6541
|
+
};
|
|
6542
|
+
var persistIssuedCredentials = async ({
|
|
6543
|
+
store,
|
|
6544
|
+
credentials,
|
|
6545
|
+
previous,
|
|
6546
|
+
apiBaseUrl,
|
|
6547
|
+
fetch: fetchImplementation,
|
|
6548
|
+
phase
|
|
6549
|
+
}) => {
|
|
6550
|
+
let prior = previous;
|
|
6551
|
+
try {
|
|
6552
|
+
if (phase === "login")
|
|
6553
|
+
prior = await store.get();
|
|
6554
|
+
await store.set(credentials);
|
|
6555
|
+
const saved = await store.get();
|
|
6556
|
+
if (!isDeepStrictEqual(saved, credentials))
|
|
6557
|
+
throw new Error("Credential write could not be verified");
|
|
6558
|
+
return;
|
|
6559
|
+
} catch {
|
|
6560
|
+
const remoteRevoked = await revokeIssuedCredentials(fetchImplementation, apiBaseUrl, credentials);
|
|
6561
|
+
let localRestored = false;
|
|
6562
|
+
try {
|
|
6563
|
+
const current = await store.get();
|
|
6564
|
+
if (phase === "login" && prior !== undefined) {
|
|
6565
|
+
if (isDeepStrictEqual(current, prior))
|
|
6566
|
+
localRestored = true;
|
|
6567
|
+
else if (current === null || current.refreshToken === credentials.refreshToken) {
|
|
6568
|
+
if (prior)
|
|
6569
|
+
await store.set(prior);
|
|
6570
|
+
else
|
|
6571
|
+
await store.delete();
|
|
6572
|
+
localRestored = isDeepStrictEqual(await store.get(), prior);
|
|
6573
|
+
}
|
|
6574
|
+
} else if (phase === "refresh" && (!current || current.refreshToken === credentials.refreshToken || current.refreshToken === prior?.refreshToken)) {
|
|
6575
|
+
if (current)
|
|
6576
|
+
await store.delete();
|
|
6577
|
+
localRestored = await store.get() === null;
|
|
6578
|
+
}
|
|
6579
|
+
} catch {}
|
|
6580
|
+
throw new CliCredentialPersistenceError(remoteRevoked, localRestored, phase);
|
|
6581
|
+
}
|
|
6582
|
+
};
|
|
6294
6583
|
var refreshes = new WeakMap;
|
|
6295
6584
|
var refreshRequestTimeoutMs = 30000;
|
|
6296
6585
|
var isInvalidGrantResponse = (payload) => {
|
|
@@ -6345,7 +6634,14 @@ var refreshAccessToken = async ({
|
|
|
6345
6634
|
throw new Error("Unable to refresh the CLI session because the API returned invalid credentials. Credentials were kept; retry the command.");
|
|
6346
6635
|
}
|
|
6347
6636
|
const stored = toStoredCredentials(issued, normalizeCliAuthIssuer(apiBaseUrl), now());
|
|
6348
|
-
await
|
|
6637
|
+
await persistIssuedCredentials({
|
|
6638
|
+
store,
|
|
6639
|
+
credentials: stored,
|
|
6640
|
+
previous: credentials,
|
|
6641
|
+
apiBaseUrl,
|
|
6642
|
+
fetch: fetchImplementation,
|
|
6643
|
+
phase: "refresh"
|
|
6644
|
+
});
|
|
6349
6645
|
return stored.accessToken;
|
|
6350
6646
|
};
|
|
6351
6647
|
var refreshWithCurrentCredentials = async ({
|
|
@@ -6901,6 +7197,9 @@ var isNetworkError = (error, depth = 0) => {
|
|
|
6901
7197
|
return isNetworkError(error.cause, depth + 1);
|
|
6902
7198
|
};
|
|
6903
7199
|
var normalizeCliError = (error) => {
|
|
7200
|
+
const storageError = getCliStorageError(error);
|
|
7201
|
+
if (storageError)
|
|
7202
|
+
return storageError;
|
|
6904
7203
|
if (error instanceof CliRuntimeError) {
|
|
6905
7204
|
return {
|
|
6906
7205
|
code: error.code,
|
|
@@ -8208,10 +8507,12 @@ var MAX_ASANA_LEAD_CANDIDATES = 5;
|
|
|
8208
8507
|
var ASANA_SEARCH_DETAIL_LIMIT = 15;
|
|
8209
8508
|
var NOTIFICATION_PAGE_LIMIT = 50;
|
|
8210
8509
|
var getAllNotifications = async () => {
|
|
8510
|
+
const user = await api2.user.getCurrentUser.query();
|
|
8211
8511
|
const notifications = [];
|
|
8212
8512
|
let cursor;
|
|
8213
8513
|
do {
|
|
8214
|
-
const page = await api2.budget.
|
|
8514
|
+
const page = await api2.budget.getNotificationPage.query({
|
|
8515
|
+
cacheUserId: user.id,
|
|
8215
8516
|
cursor,
|
|
8216
8517
|
limit: NOTIFICATION_PAGE_LIMIT
|
|
8217
8518
|
});
|
|
@@ -9267,12 +9568,30 @@ async function getBudgetCategories() {
|
|
|
9267
9568
|
const categories = await api2.budget.getBudgetCategories.query();
|
|
9268
9569
|
out(categories);
|
|
9269
9570
|
}
|
|
9270
|
-
async
|
|
9271
|
-
|
|
9272
|
-
|
|
9273
|
-
}
|
|
9274
|
-
|
|
9275
|
-
|
|
9571
|
+
var getBudgetVersions = async (budgetId, options = {}) => {
|
|
9572
|
+
if (options.all && options.cursor !== undefined) {
|
|
9573
|
+
throw new CliRuntimeError("USAGE", "--all cannot be combined with --cursor.");
|
|
9574
|
+
}
|
|
9575
|
+
const versions = [];
|
|
9576
|
+
const seenCursors = new Set;
|
|
9577
|
+
let cursor = options.cursor;
|
|
9578
|
+
do {
|
|
9579
|
+
const page = await api2.budgetVersion.getBudgetVersionsPage.query({
|
|
9580
|
+
budgetId,
|
|
9581
|
+
limit: options.limit,
|
|
9582
|
+
cursor
|
|
9583
|
+
});
|
|
9584
|
+
versions.push(...page.versions);
|
|
9585
|
+
cursor = page.nextCursor;
|
|
9586
|
+
if (!options.all || cursor === undefined)
|
|
9587
|
+
break;
|
|
9588
|
+
if (seenCursors.has(cursor)) {
|
|
9589
|
+
throw new Error("Budget history pagination did not advance; export aborted.");
|
|
9590
|
+
}
|
|
9591
|
+
seenCursors.add(cursor);
|
|
9592
|
+
} while (cursor !== undefined);
|
|
9593
|
+
out({ versions, nextCursor: cursor ?? null });
|
|
9594
|
+
};
|
|
9276
9595
|
async function createBudgetApproval(budgetId, preflight) {
|
|
9277
9596
|
await assertSensitiveWorkflowConfirmed({
|
|
9278
9597
|
action: "Create budget approval request",
|
|
@@ -10292,13 +10611,16 @@ async function listContacts(companyId) {
|
|
|
10292
10611
|
out(contacts);
|
|
10293
10612
|
}
|
|
10294
10613
|
async function listItems(opts) {
|
|
10295
|
-
const
|
|
10614
|
+
const filters = {
|
|
10296
10615
|
includeQuickBooksDeactivationPending: opts.includeQuickBooksDeactivationPending,
|
|
10297
|
-
page: opts.page ?? 1,
|
|
10298
|
-
perPage: opts.perPage ?? 20,
|
|
10299
10616
|
name: opts.name
|
|
10300
|
-
}
|
|
10301
|
-
|
|
10617
|
+
};
|
|
10618
|
+
const perPage = opts.perPage ?? 20;
|
|
10619
|
+
const [items, { total, counts }] = await Promise.all([
|
|
10620
|
+
api2.item.getItems.query({ ...filters, page: opts.page ?? 1, perPage }),
|
|
10621
|
+
api2.item.getItemCounts.query(filters)
|
|
10622
|
+
]);
|
|
10623
|
+
out({ items, pageCount: Math.ceil(total / perPage), counts });
|
|
10302
10624
|
}
|
|
10303
10625
|
async function getItem(id) {
|
|
10304
10626
|
const item = await api2.item.getItem.query({ id });
|
|
@@ -11033,7 +11355,20 @@ var registry2 = [
|
|
|
11033
11355
|
}
|
|
11034
11356
|
]),
|
|
11035
11357
|
legacyCommand("get-budget-categories", ["budget", "category", "list"]),
|
|
11036
|
-
legacyCommand("get-budget-versions", ["budget", "version", "list"]
|
|
11358
|
+
legacyCommand("get-budget-versions", ["budget", "version", "list"], [], [
|
|
11359
|
+
{
|
|
11360
|
+
name: "--limit <1-50>",
|
|
11361
|
+
description: "Versions per page (default 20). Returns versions and nextCursor, not a full-history array."
|
|
11362
|
+
},
|
|
11363
|
+
{
|
|
11364
|
+
name: "--cursor <versionId>",
|
|
11365
|
+
description: "Load the next page using the previous nextCursor; null means complete."
|
|
11366
|
+
},
|
|
11367
|
+
{
|
|
11368
|
+
name: "--all",
|
|
11369
|
+
description: "Explicitly fetch all visible history in bounded pages; cannot combine with --cursor. May be expensive."
|
|
11370
|
+
}
|
|
11371
|
+
]),
|
|
11037
11372
|
legacyCommand("rename-budget-version", ["budget", "version", "rename"]),
|
|
11038
11373
|
legacyCommand("restore-budget-version", ["budget", "version", "restore"]),
|
|
11039
11374
|
legacyCommand("update-budget-status", ["budget", "status", "update"]),
|
|
@@ -11469,9 +11804,12 @@ var resolveCommand = (argv) => {
|
|
|
11469
11804
|
isLegacyAlias: true
|
|
11470
11805
|
};
|
|
11471
11806
|
};
|
|
11807
|
+
var updateBeforeUse = "First step before each task: update to the latest published release with 'bun add --global @go-labs-sg/bb@latest', then run 'bb version' and 'bb --help' before using commands. Update once per task. If updating fails, stop and report it. Preserve an explicitly requested pinned or development/staging version.";
|
|
11472
11808
|
var createHumanHelp = (command) => {
|
|
11473
11809
|
if (command) {
|
|
11474
11810
|
return [
|
|
11811
|
+
updateBeforeUse,
|
|
11812
|
+
"",
|
|
11475
11813
|
`Usage: bb ${command.path.join(" ")} [options] [args]`,
|
|
11476
11814
|
"",
|
|
11477
11815
|
command.summary,
|
|
@@ -11496,6 +11834,8 @@ var createHumanHelp = (command) => {
|
|
|
11496
11834
|
return [
|
|
11497
11835
|
"bb \u2014 Budget Builder CLI",
|
|
11498
11836
|
"",
|
|
11837
|
+
updateBeforeUse,
|
|
11838
|
+
"",
|
|
11499
11839
|
"Usage: bb <resource> <operation> [options] [args]",
|
|
11500
11840
|
"",
|
|
11501
11841
|
"Global options",
|
|
@@ -11691,6 +12031,8 @@ function printLegacyHelp(runtime) {
|
|
|
11691
12031
|
const help = `
|
|
11692
12032
|
bb \u2014 Budget Builder CLI for AI agents (parity with MCP tools)
|
|
11693
12033
|
|
|
12034
|
+
First step before each task: update to the latest published release with 'bun add --global @go-labs-sg/bb@latest', then run 'bb version' and 'bb --help' before using commands. Update once per task. If updating fails, stop and report it. Preserve an explicitly requested pinned or development/staging version.
|
|
12035
|
+
|
|
11694
12036
|
Usage: bb <command> [options] [args]
|
|
11695
12037
|
|
|
11696
12038
|
Identity: bb version \u2014 print the installed CLI package version and API target as JSON.
|
|
@@ -11723,7 +12065,7 @@ Budgets
|
|
|
11723
12065
|
get-budget-items <id> Line items only
|
|
11724
12066
|
get-budget-details <id> Full detail (margins, suppliers, etc.)
|
|
11725
12067
|
get-budget-categories
|
|
11726
|
-
get-budget-versions <budgetId>
|
|
12068
|
+
get-budget-versions <budgetId> [--limit <1-50>] [--cursor <versionId> | --all]
|
|
11727
12069
|
rename-budget-version <versionId> --name <text>
|
|
11728
12070
|
restore-budget-version <versionId> Replace current budget data and return it to DRAFT.
|
|
11729
12071
|
update-budget-status <id> <status> [--projectManagerId <id>] [--markProjectWon true|false] [--projectStatusOnCommercialRejection PITCH|LOST]
|
|
@@ -11760,6 +12102,7 @@ Budgets
|
|
|
11760
12102
|
Bills
|
|
11761
12103
|
list-bills [--projectId] [--budgetId] [--status CSV] [--search <text>] [--isClaimable true|false] [--createdByIds <csv>] [--sortBy createdAt|amount|status] [--sortDir asc|desc] [--page] [--pageSize]
|
|
11762
12104
|
list-claims same flags as list-bills; only reimbursable claims (ignores --isClaimable)
|
|
12105
|
+
Both lists return summaries; use get-bill-details <billId> for full detail.
|
|
11763
12106
|
isClaimable differentiates the shared bill/claim records: false = bill, true = claim.
|
|
11764
12107
|
Create supplier bills from approved quotations from the same supplier and project. Use quotationAllocations plus lineAmounts when quotations split one bill line. Eligible exemptions require explicit --quotation-exempt intent; claims do not consume or link quotations.
|
|
11765
12108
|
Amounts for create/update (including drafts) exclude GST: amount = portion billed; extractedAmount = full invoice subtotal; lineAmounts[].amount and quotationAllocations[].amount also exclude GST. The CLI does not strip GST from input.
|
|
@@ -12139,7 +12482,10 @@ async function runCli(argv = process.argv, runtime = createProcessRuntime(), cre
|
|
|
12139
12482
|
if (commandArgv.length !== 2) {
|
|
12140
12483
|
throw new CliRuntimeError("USAGE", `auth ${authAction} accepts no positional arguments.`);
|
|
12141
12484
|
}
|
|
12142
|
-
const store = credentialStore ?? createBunCredentialStore({
|
|
12485
|
+
const store = credentialStore ?? createBunCredentialStore({
|
|
12486
|
+
account: resolveCliCredentialAccount(runtime.env),
|
|
12487
|
+
product: "budget-builder"
|
|
12488
|
+
});
|
|
12143
12489
|
if (authAction === "login") {
|
|
12144
12490
|
const credentials = await loginWithBrowser({
|
|
12145
12491
|
apiBaseUrl: BUDGET_BUILDER_API_BASE_URL,
|
|
@@ -12255,9 +12601,14 @@ Waiting for approval\u2026
|
|
|
12255
12601
|
try {
|
|
12256
12602
|
accessToken = await getAuthenticatedAccessToken({
|
|
12257
12603
|
apiBaseUrl: BUDGET_BUILDER_API_BASE_URL,
|
|
12258
|
-
store: credentialStore ?? createBunCredentialStore({
|
|
12604
|
+
store: credentialStore ?? createBunCredentialStore({
|
|
12605
|
+
account: resolveCliCredentialAccount(runtime.env),
|
|
12606
|
+
product: "budget-builder"
|
|
12607
|
+
})
|
|
12259
12608
|
});
|
|
12260
12609
|
} catch (error) {
|
|
12610
|
+
if (getCliStorageError(error))
|
|
12611
|
+
throw error;
|
|
12261
12612
|
throw new CliRuntimeError("AUTHENTICATION", error instanceof Error ? error.message : "Not authenticated. Run bb auth login first.");
|
|
12262
12613
|
}
|
|
12263
12614
|
configureAccessToken(accessToken);
|
|
@@ -12348,7 +12699,18 @@ Waiting for approval\u2026
|
|
|
12348
12699
|
const id = positional[0];
|
|
12349
12700
|
if (!id)
|
|
12350
12701
|
throw new Error("get-budget-versions requires <budgetId>");
|
|
12351
|
-
|
|
12702
|
+
const limit = flags.limit === undefined ? undefined : Number(flags.limit);
|
|
12703
|
+
if (flags.limit === true || limit !== undefined && (!Number.isInteger(limit) || limit < 1 || limit > 50)) {
|
|
12704
|
+
throw new CliRuntimeError("USAGE", "--limit must be an integer from 1 to 50.");
|
|
12705
|
+
}
|
|
12706
|
+
if (flags.cursor === true || flags.cursor === "") {
|
|
12707
|
+
throw new CliRuntimeError("USAGE", "--cursor requires a non-empty version ID.");
|
|
12708
|
+
}
|
|
12709
|
+
await getBudgetVersions(id, {
|
|
12710
|
+
limit,
|
|
12711
|
+
cursor: flags.cursor,
|
|
12712
|
+
all: flags.all === true || (parseOptionalBoolFlag(flags, "all") ?? false)
|
|
12713
|
+
});
|
|
12352
12714
|
break;
|
|
12353
12715
|
}
|
|
12354
12716
|
case "rename-budget-version": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@go-labs-sg/bb",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.47.0",
|
|
4
4
|
"description": "Budget Builder CLI for AI agents — manage budgets, bills, claims, quotations, and customer invoices with explicit workflow previews for sensitive changes.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|