@go-labs-sg/bb 2.41.0 → 2.42.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/dist/index.js CHANGED
@@ -6789,6 +6789,186 @@ var ExtendedErrorStatus = {
6789
6789
  ...ErrorStatus
6790
6790
  };
6791
6791
 
6792
+ // src/runtime/error.ts
6793
+ var invocationErrorCodes = new Set([
6794
+ "USAGE",
6795
+ "VALIDATION",
6796
+ "CONFIRMATION_REQUIRED"
6797
+ ]);
6798
+ var trpcCodeMap = {
6799
+ BAD_REQUEST: "VALIDATION",
6800
+ CONFLICT: "CONFLICT",
6801
+ FORBIDDEN: "AUTHORIZATION",
6802
+ INTERNAL_SERVER_ERROR: "API",
6803
+ METHOD_NOT_SUPPORTED: "API",
6804
+ NOT_FOUND: "NOT_FOUND",
6805
+ PARSE_ERROR: "VALIDATION",
6806
+ PAYLOAD_TOO_LARGE: "VALIDATION",
6807
+ PRECONDITION_FAILED: "CONFLICT",
6808
+ TIMEOUT: "NETWORK",
6809
+ TOO_MANY_REQUESTS: "API",
6810
+ UNAUTHORIZED: "AUTHENTICATION",
6811
+ UNPROCESSABLE_CONTENT: "VALIDATION"
6812
+ };
6813
+ var cliErrorCodes = new Set([
6814
+ "USAGE",
6815
+ "VALIDATION",
6816
+ "CONFIRMATION_REQUIRED",
6817
+ "AUTHENTICATION",
6818
+ "AUTHORIZATION",
6819
+ "NOT_FOUND",
6820
+ "CONFLICT",
6821
+ "API",
6822
+ "NETWORK",
6823
+ "ABORTED",
6824
+ "INTERRUPTED",
6825
+ "INTERNAL"
6826
+ ]);
6827
+
6828
+ class CliRuntimeError extends Error {
6829
+ code;
6830
+ details;
6831
+ requestId;
6832
+ constructor(code, message, options = {}) {
6833
+ super(message);
6834
+ this.name = "CliRuntimeError";
6835
+ this.code = code;
6836
+ this.details = options.details;
6837
+ this.requestId = options.requestId;
6838
+ }
6839
+ }
6840
+ var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
6841
+ var asNonEmptyString = (value) => typeof value === "string" && value.trim() !== "" ? value : undefined;
6842
+ var formatValidationMessage = (message, data) => {
6843
+ const zodError = isRecord(data?.zodError) ? data.zodError : undefined;
6844
+ const fieldErrors = isRecord(zodError?.fieldErrors) ? zodError.fieldErrors : undefined;
6845
+ const fields = fieldErrors ? Object.entries(fieldErrors).flatMap(([field, errors]) => Array.isArray(errors) ? errors.filter((entry) => typeof entry === "string").map((entry) => `${field}: ${entry}`) : []) : [];
6846
+ const formErrors = Array.isArray(zodError?.formErrors) ? zodError.formErrors.filter((entry) => typeof entry === "string") : [];
6847
+ const validationIssues = [...fields, ...formErrors];
6848
+ if (validationIssues.length > 0)
6849
+ return validationIssues.join("; ");
6850
+ let parsed = message;
6851
+ for (let depth = 0;depth < 3 && typeof parsed === "string"; depth++) {
6852
+ try {
6853
+ parsed = JSON.parse(parsed);
6854
+ } catch {
6855
+ return message;
6856
+ }
6857
+ }
6858
+ if (!Array.isArray(parsed) || parsed.length === 0 || !parsed.every(isRecord))
6859
+ return message;
6860
+ const issues = parsed.map((issue) => {
6861
+ if (!Array.isArray(issue.path) || typeof issue.message !== "string" || typeof issue.code !== "string")
6862
+ return;
6863
+ const path = issue.path.filter((part) => typeof part === "string" || typeof part === "number").join(".") || "payload";
6864
+ if (issue.code === "invalid_type" && typeof issue.expected === "string" && issue.message.toLowerCase().includes("required")) {
6865
+ return `${path} must be a ${issue.expected}${path === "cost" ? ', e.g. "19"' : ""}`;
6866
+ }
6867
+ if (issue.code === "invalid_format" && issue.format === "regex" && path === "name" && String(issue.pattern ?? issue.message).includes("A-Za-z0-9,.?@&!#'~* _;+-")) {
6868
+ return `${path} contains characters that are not allowed. Allowed: letters, numbers, spaces and , . ? @ & ! # ' ~ * _ ; + -`;
6869
+ }
6870
+ return `${path}: ${issue.message}`;
6871
+ });
6872
+ return issues.every((issue) => issue !== undefined) ? issues.join("; ") : message;
6873
+ };
6874
+ var asDetails = (value) => {
6875
+ if (!isRecord(value))
6876
+ return;
6877
+ const details = {};
6878
+ for (const [key, nestedValue] of Object.entries(value)) {
6879
+ if (isJsonValue(nestedValue))
6880
+ details[key] = nestedValue;
6881
+ }
6882
+ return Object.keys(details).length > 0 ? details : undefined;
6883
+ };
6884
+ var isJsonValue = (value) => {
6885
+ if (value === null || typeof value === "string" || typeof value === "boolean" || typeof value === "number") {
6886
+ return true;
6887
+ }
6888
+ if (Array.isArray(value))
6889
+ return value.every(isJsonValue);
6890
+ return isRecord(value) && Object.values(value).every(isJsonValue);
6891
+ };
6892
+ var mapErrorCode = (value) => {
6893
+ if (typeof value !== "string")
6894
+ return;
6895
+ if (cliErrorCodes.has(value))
6896
+ return value;
6897
+ if (value in trpcCodeMap)
6898
+ return trpcCodeMap[value];
6899
+ return;
6900
+ };
6901
+ var networkMessagePattern = /\b(fetch failed|network error|unable to connect|econnrefused|econnreset|enotfound|etimedout|socket hang up)\b/i;
6902
+ var isNetworkError = (error, depth = 0) => {
6903
+ if (depth > 3 || !isRecord(error))
6904
+ return false;
6905
+ const message = asNonEmptyString(error.message);
6906
+ if (message !== undefined && networkMessagePattern.test(message))
6907
+ return true;
6908
+ const code = asNonEmptyString(error.code);
6909
+ if (code !== undefined && networkMessagePattern.test(code))
6910
+ return true;
6911
+ return isNetworkError(error.cause, depth + 1);
6912
+ };
6913
+ var normalizeCliError = (error) => {
6914
+ if (error instanceof CliRuntimeError) {
6915
+ return {
6916
+ code: error.code,
6917
+ message: error.message,
6918
+ ...error.details === undefined ? {} : { details: error.details },
6919
+ ...error.requestId === undefined ? {} : { requestId: error.requestId }
6920
+ };
6921
+ }
6922
+ if (isNetworkError(error)) {
6923
+ return {
6924
+ code: "NETWORK",
6925
+ message: error instanceof Error ? error.message : "The Budget Builder API could not be reached."
6926
+ };
6927
+ }
6928
+ if (isRecord(error)) {
6929
+ const data = isRecord(error.data) ? error.data : undefined;
6930
+ const transportCode = mapErrorCode(data?.code ?? error.code);
6931
+ if (error instanceof Error && data === undefined && transportCode === undefined) {
6932
+ return { code: "INTERNAL", message: error.message };
6933
+ }
6934
+ const code = transportCode ?? "API";
6935
+ const rawMessage = asNonEmptyString(error.message) ?? "The Budget Builder API request failed.";
6936
+ const message = code === "VALIDATION" ? formatValidationMessage(rawMessage, data) : rawMessage;
6937
+ const requestId = asNonEmptyString(data?.requestId) ?? asNonEmptyString(data?.requestID) ?? asNonEmptyString(error.requestId);
6938
+ const details = asDetails(data?.details ?? error.details);
6939
+ return {
6940
+ code,
6941
+ message,
6942
+ ...details === undefined ? {} : { details },
6943
+ ...requestId === undefined ? {} : { requestId }
6944
+ };
6945
+ }
6946
+ if (error instanceof Error) {
6947
+ return { code: "INTERNAL", message: error.message };
6948
+ }
6949
+ return { code: "INTERNAL", message: "An unexpected CLI error occurred." };
6950
+ };
6951
+ var exitCodeForCliError = (error) => {
6952
+ if (error.code === "INTERRUPTED")
6953
+ return 130;
6954
+ return invocationErrorCodes.has(error.code) ? 2 : 1;
6955
+ };
6956
+
6957
+ // src/budget-create-outcome.ts
6958
+ var isP2028Error = (value, depth = 0) => {
6959
+ if (depth > 3 || typeof value !== "object" || value === null)
6960
+ return false;
6961
+ const error = value;
6962
+ return error.code === "P2028" || typeof error.message === "string" && /\bP2028\b/.test(error.message) || isP2028Error(error.cause, depth + 1) || isP2028Error(error.data, depth + 1) || isP2028Error(error.error, depth + 1);
6963
+ };
6964
+ var budgetCreateUncertainError = (error, input) => {
6965
+ const message = error instanceof Error ? error.message : String(error);
6966
+ const normalized = normalizeCliError(error);
6967
+ if (!isP2028Error(error) && normalized.code !== "NETWORK")
6968
+ return;
6969
+ return new CliRuntimeError(normalized.code === "NETWORK" ? "NETWORK" : "API", `Budget creation outcome is uncertain (the write may have committed). Run list-budgets --projectId ${input.projectId} and check for ${JSON.stringify(input.name)} before retrying; matching names alone may be ambiguous. Original error: ${message}`);
6970
+ };
6971
+
6792
6972
  // src/commands/invoice-splits.ts
6793
6973
  var invoiceSplitSetupNextAction = (budgetId) => ({
6794
6974
  prompt: "Review the uninvoiced balance, choose all or part of the remaining percentage, then supply invoice and due dates, billing address, and customer message. Non-admin creation requests admin approval; admin creation auto-approves. Sending remains separate.",
@@ -7135,142 +7315,11 @@ var normalizePlainTextRichTextField = ({
7135
7315
  };
7136
7316
  };
7137
7317
 
7138
- // src/runtime/error.ts
7139
- var invocationErrorCodes = new Set([
7140
- "USAGE",
7141
- "VALIDATION",
7142
- "CONFIRMATION_REQUIRED"
7143
- ]);
7144
- var trpcCodeMap = {
7145
- BAD_REQUEST: "VALIDATION",
7146
- CONFLICT: "CONFLICT",
7147
- FORBIDDEN: "AUTHORIZATION",
7148
- INTERNAL_SERVER_ERROR: "API",
7149
- METHOD_NOT_SUPPORTED: "API",
7150
- NOT_FOUND: "NOT_FOUND",
7151
- PARSE_ERROR: "VALIDATION",
7152
- PAYLOAD_TOO_LARGE: "VALIDATION",
7153
- PRECONDITION_FAILED: "CONFLICT",
7154
- TIMEOUT: "NETWORK",
7155
- TOO_MANY_REQUESTS: "API",
7156
- UNAUTHORIZED: "AUTHENTICATION",
7157
- UNPROCESSABLE_CONTENT: "VALIDATION"
7158
- };
7159
- var cliErrorCodes = new Set([
7160
- "USAGE",
7161
- "VALIDATION",
7162
- "CONFIRMATION_REQUIRED",
7163
- "AUTHENTICATION",
7164
- "AUTHORIZATION",
7165
- "NOT_FOUND",
7166
- "CONFLICT",
7167
- "API",
7168
- "NETWORK",
7169
- "ABORTED",
7170
- "INTERRUPTED",
7171
- "INTERNAL"
7172
- ]);
7173
-
7174
- class CliRuntimeError extends Error {
7175
- code;
7176
- details;
7177
- requestId;
7178
- constructor(code, message, options = {}) {
7179
- super(message);
7180
- this.name = "CliRuntimeError";
7181
- this.code = code;
7182
- this.details = options.details;
7183
- this.requestId = options.requestId;
7184
- }
7185
- }
7186
- var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
7187
- var asNonEmptyString = (value) => typeof value === "string" && value.trim() !== "" ? value : undefined;
7188
- var asDetails = (value) => {
7189
- if (!isRecord(value))
7190
- return;
7191
- const details = {};
7192
- for (const [key, nestedValue] of Object.entries(value)) {
7193
- if (isJsonValue(nestedValue))
7194
- details[key] = nestedValue;
7195
- }
7196
- return Object.keys(details).length > 0 ? details : undefined;
7197
- };
7198
- var isJsonValue = (value) => {
7199
- if (value === null || typeof value === "string" || typeof value === "boolean" || typeof value === "number") {
7200
- return true;
7201
- }
7202
- if (Array.isArray(value))
7203
- return value.every(isJsonValue);
7204
- return isRecord(value) && Object.values(value).every(isJsonValue);
7205
- };
7206
- var mapErrorCode = (value) => {
7207
- if (typeof value !== "string")
7208
- return;
7209
- if (cliErrorCodes.has(value))
7210
- return value;
7211
- if (value in trpcCodeMap)
7212
- return trpcCodeMap[value];
7213
- return;
7214
- };
7215
- var networkMessagePattern = /\b(fetch failed|network error|unable to connect|econnrefused|econnreset|enotfound|etimedout|socket hang up)\b/i;
7216
- var isNetworkError = (error, depth = 0) => {
7217
- if (depth > 3 || !isRecord(error))
7218
- return false;
7219
- const message = asNonEmptyString(error.message);
7220
- if (message !== undefined && networkMessagePattern.test(message))
7221
- return true;
7222
- const code = asNonEmptyString(error.code);
7223
- if (code !== undefined && networkMessagePattern.test(code))
7224
- return true;
7225
- return isNetworkError(error.cause, depth + 1);
7226
- };
7227
- var normalizeCliError = (error) => {
7228
- if (error instanceof CliRuntimeError) {
7229
- return {
7230
- code: error.code,
7231
- message: error.message,
7232
- ...error.details === undefined ? {} : { details: error.details },
7233
- ...error.requestId === undefined ? {} : { requestId: error.requestId }
7234
- };
7235
- }
7236
- if (isNetworkError(error)) {
7237
- return {
7238
- code: "NETWORK",
7239
- message: error instanceof Error ? error.message : "The Budget Builder API could not be reached."
7240
- };
7241
- }
7242
- if (isRecord(error)) {
7243
- const data = isRecord(error.data) ? error.data : undefined;
7244
- const transportCode = mapErrorCode(data?.code ?? error.code);
7245
- if (error instanceof Error && data === undefined && transportCode === undefined) {
7246
- return { code: "INTERNAL", message: error.message };
7247
- }
7248
- const code = transportCode ?? "API";
7249
- const message = asNonEmptyString(error.message) ?? "The Budget Builder API request failed.";
7250
- const requestId = asNonEmptyString(data?.requestId) ?? asNonEmptyString(data?.requestID) ?? asNonEmptyString(error.requestId);
7251
- const details = asDetails(data?.details ?? error.details);
7252
- return {
7253
- code,
7254
- message,
7255
- ...details === undefined ? {} : { details },
7256
- ...requestId === undefined ? {} : { requestId }
7257
- };
7258
- }
7259
- if (error instanceof Error) {
7260
- return { code: "INTERNAL", message: error.message };
7261
- }
7262
- return { code: "INTERNAL", message: "An unexpected CLI error occurred." };
7263
- };
7264
- var exitCodeForCliError = (error) => {
7265
- if (error.code === "INTERRUPTED")
7266
- return 130;
7267
- return invocationErrorCodes.has(error.code) ? 2 : 1;
7268
- };
7269
-
7270
7318
  // src/parse-mutation-payload.ts
7319
+ var validationError = (message) => new CliRuntimeError("VALIDATION", message);
7271
7320
  function requireObject(raw, label) {
7272
7321
  if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
7273
- throw new Error(`${label} must be a JSON object.`);
7322
+ throw validationError(`${label} must be a JSON object.`);
7274
7323
  }
7275
7324
  return raw;
7276
7325
  }
@@ -7283,7 +7332,7 @@ function toDate(value, field) {
7283
7332
  if (!Number.isNaN(d.getTime()))
7284
7333
  return d;
7285
7334
  }
7286
- throw new Error(`${field} must be an ISO date/datetime string.`);
7335
+ throw validationError(`${field} must be an ISO date/datetime string.`);
7287
7336
  }
7288
7337
  function optionalString(v) {
7289
7338
  if (v === undefined || v === null)
@@ -7297,14 +7346,14 @@ var parseQuotationReferences = (input, commandName) => {
7297
7346
  let quotationAllocations;
7298
7347
  if (rawAllocations !== undefined && rawAllocations !== null) {
7299
7348
  if (!Array.isArray(rawAllocations) || rawAllocations.length === 0) {
7300
- throw new Error(`${commandName} payload.quotationAllocations must be a non-empty array.`);
7349
+ throw validationError(`${commandName} payload.quotationAllocations must be a non-empty array.`);
7301
7350
  }
7302
7351
  quotationAllocations = rawAllocations.map((raw, index) => {
7303
7352
  const label = `${commandName} payload.quotationAllocations[${index}]`;
7304
7353
  const allocation = requireObject(raw, label);
7305
7354
  const amount = Number(allocation.amount);
7306
7355
  if (!Number.isFinite(amount) || amount < 0 || !Number.isInteger(amount * 100)) {
7307
- throw new Error(`${label}.amount must be a non-negative cent value.`);
7356
+ throw validationError(`${label}.amount must be a non-negative cent value.`);
7308
7357
  }
7309
7358
  return {
7310
7359
  budgetItemId: requiredString(allocation.budgetItemId, `${label}.budgetItemId`),
@@ -7314,22 +7363,22 @@ var parseQuotationReferences = (input, commandName) => {
7314
7363
  });
7315
7364
  const keys = quotationAllocations.map((allocation) => `${allocation.budgetItemId}\x00${allocation.quotationId}`);
7316
7365
  if (new Set(keys).size !== keys.length) {
7317
- throw new Error(`${commandName} payload.quotationAllocations must not repeat a budget-item/quotation pair.`);
7366
+ throw validationError(`${commandName} payload.quotationAllocations must not repeat a budget-item/quotation pair.`);
7318
7367
  }
7319
7368
  }
7320
7369
  const rawQuotationIds = input.quotationIds;
7321
7370
  let quotationIds;
7322
7371
  if (rawQuotationIds !== undefined && rawQuotationIds !== null) {
7323
7372
  if (!Array.isArray(rawQuotationIds)) {
7324
- throw new Error(`${commandName} payload.quotationIds must be a string array.`);
7373
+ throw validationError(`${commandName} payload.quotationIds must be a string array.`);
7325
7374
  }
7326
7375
  quotationIds = rawQuotationIds.map((id, index) => requiredString(id, `${commandName} payload.quotationIds[${index}]`));
7327
7376
  if (new Set(quotationIds).size !== quotationIds.length) {
7328
- throw new Error(`${commandName} payload.quotationIds must not contain duplicates.`);
7377
+ throw validationError(`${commandName} payload.quotationIds must not contain duplicates.`);
7329
7378
  }
7330
7379
  }
7331
7380
  if (quotationId && quotationIds && (quotationIds.length !== 1 || quotationIds[0] !== quotationId)) {
7332
- throw new Error(`${commandName} payload.quotationId conflicts with payload.quotationIds.`);
7381
+ throw validationError(`${commandName} payload.quotationId conflicts with payload.quotationIds.`);
7333
7382
  }
7334
7383
  if (quotationAllocations && quotationIds) {
7335
7384
  const allocationIds = [
@@ -7337,7 +7386,7 @@ var parseQuotationReferences = (input, commandName) => {
7337
7386
  ].sort();
7338
7387
  const selectedIds = [...quotationIds].sort();
7339
7388
  if (allocationIds.join("\x00") !== selectedIds.join("\x00")) {
7340
- throw new Error(`${commandName} payload.quotationIds conflicts with payload.quotationAllocations.`);
7389
+ throw validationError(`${commandName} payload.quotationIds conflicts with payload.quotationAllocations.`);
7341
7390
  }
7342
7391
  }
7343
7392
  return {
@@ -7350,14 +7399,14 @@ var parseBillLineAmounts = (input, commandName) => {
7350
7399
  if (input.lineAmounts === undefined || input.lineAmounts === null)
7351
7400
  return;
7352
7401
  if (!Array.isArray(input.lineAmounts) || input.lineAmounts.length === 0) {
7353
- throw new Error(`${commandName} payload.lineAmounts must be a non-empty array.`);
7402
+ throw validationError(`${commandName} payload.lineAmounts must be a non-empty array.`);
7354
7403
  }
7355
7404
  const lineAmounts = input.lineAmounts.map((raw, index) => {
7356
7405
  const label = `${commandName} payload.lineAmounts[${index}]`;
7357
7406
  const line = requireObject(raw, label);
7358
7407
  const amount = Number(line.amount);
7359
7408
  if (!Number.isFinite(amount) || amount < 0 || !Number.isInteger(amount * 100)) {
7360
- throw new Error(`${label}.amount must be a non-negative cent value.`);
7409
+ throw validationError(`${label}.amount must be a non-negative cent value.`);
7361
7410
  }
7362
7411
  return {
7363
7412
  budgetItemId: requiredString(line.budgetItemId, `${label}.budgetItemId`),
@@ -7365,7 +7414,7 @@ var parseBillLineAmounts = (input, commandName) => {
7365
7414
  };
7366
7415
  });
7367
7416
  if (new Set(lineAmounts.map((line) => line.budgetItemId)).size !== lineAmounts.length) {
7368
- throw new Error(`${commandName} payload.lineAmounts must contain each budget item once.`);
7417
+ throw validationError(`${commandName} payload.lineAmounts must contain each budget item once.`);
7369
7418
  }
7370
7419
  return lineAmounts;
7371
7420
  };
@@ -7380,14 +7429,14 @@ var validateExplicitBillAllocations = ({
7380
7429
  if (!lineAmounts && !quotationAllocations)
7381
7430
  return;
7382
7431
  if (!lineAmounts) {
7383
- throw new Error(`${commandName} payload.lineAmounts is required with quotationAllocations.`);
7432
+ throw validationError(`${commandName} payload.lineAmounts is required with quotationAllocations.`);
7384
7433
  }
7385
7434
  const selected = new Set(budgetItemIds);
7386
7435
  if (lineAmounts.length !== selected.size || lineAmounts.some((line) => !selected.has(line.budgetItemId))) {
7387
- throw new Error(`${commandName} payload.lineAmounts must contain each selected budget item exactly once.`);
7436
+ throw validationError(`${commandName} payload.lineAmounts must contain each selected budget item exactly once.`);
7388
7437
  }
7389
7438
  if (lineAmounts.reduce((sum, line) => sum + cents(line.amount), 0) !== cents(amount)) {
7390
- throw new Error(`${commandName} payload.lineAmounts must sum to payload.amount.`);
7439
+ throw validationError(`${commandName} payload.lineAmounts must sum to payload.amount.`);
7391
7440
  }
7392
7441
  if (!quotationAllocations)
7393
7442
  return;
@@ -7395,27 +7444,27 @@ var validateExplicitBillAllocations = ({
7395
7444
  const allocatedById = new Map;
7396
7445
  for (const allocation of quotationAllocations) {
7397
7446
  if (!selected.has(allocation.budgetItemId)) {
7398
- throw new Error(`${commandName} payload.quotationAllocations references an unselected budget item.`);
7447
+ throw validationError(`${commandName} payload.quotationAllocations references an unselected budget item.`);
7399
7448
  }
7400
7449
  allocatedById.set(allocation.budgetItemId, (allocatedById.get(allocation.budgetItemId) ?? 0) + cents(allocation.amount));
7401
7450
  if (cents(allocation.amount) === 0 && lineAmountById.get(allocation.budgetItemId) !== 0) {
7402
- throw new Error(`${commandName} zero quotation allocations are only valid for zero-value bill lines.`);
7451
+ throw validationError(`${commandName} zero quotation allocations are only valid for zero-value bill lines.`);
7403
7452
  }
7404
7453
  }
7405
7454
  for (const [budgetItemId, allocated] of allocatedById) {
7406
7455
  if (allocated > (lineAmountById.get(budgetItemId) ?? 0)) {
7407
- throw new Error(`${commandName} quotation allocations exceed the line amount for ${budgetItemId}.`);
7456
+ throw validationError(`${commandName} quotation allocations exceed the line amount for ${budgetItemId}.`);
7408
7457
  }
7409
7458
  }
7410
7459
  };
7411
7460
  function requiredString(value, fieldLabel) {
7412
7461
  if (value === undefined || value === null) {
7413
- throw new Error(`${fieldLabel} is required.`);
7462
+ throw validationError(`${fieldLabel} is required.`);
7414
7463
  }
7415
7464
  const s = typeof value === "string" ? value : String(value);
7416
7465
  const t = s.trim();
7417
7466
  if (t === "") {
7418
- throw new Error(`${fieldLabel} is required.`);
7467
+ throw validationError(`${fieldLabel} is required.`);
7419
7468
  }
7420
7469
  return t;
7421
7470
  }
@@ -7423,13 +7472,13 @@ var EMAIL_ADDRESS_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
7423
7472
  var requiredEmail = (value, fieldLabel) => {
7424
7473
  const email = requiredString(value, fieldLabel);
7425
7474
  if (!EMAIL_ADDRESS_PATTERN.test(email)) {
7426
- throw new Error(`${fieldLabel} must be a valid email address.`);
7475
+ throw validationError(`${fieldLabel} must be a valid email address.`);
7427
7476
  }
7428
7477
  return email;
7429
7478
  };
7430
7479
  var parseContactPersonEmailPayload = (input, commandName) => {
7431
7480
  if (!Array.isArray(input.cc)) {
7432
- throw new Error(`${commandName} payload.cc must be an email array.`);
7481
+ throw validationError(`${commandName} payload.cc must be an email array.`);
7433
7482
  }
7434
7483
  return {
7435
7484
  to: requiredEmail(input.to, `${commandName} payload.to`),
@@ -7459,7 +7508,7 @@ var parseSendCustomerInvoiceToContactPersonPayload = (raw) => {
7459
7508
  function parseCreateBudgetPayload(raw) {
7460
7509
  const o = requireObject(raw, "create-budget payload");
7461
7510
  if (o.unavailableItemReviewAcknowledged !== undefined && typeof o.unavailableItemReviewAcknowledged !== "boolean") {
7462
- throw new Error("create-budget payload.unavailableItemReviewAcknowledged must be a boolean when set.");
7511
+ throw validationError("create-budget payload.unavailableItemReviewAcknowledged must be a boolean when set.");
7463
7512
  }
7464
7513
  return {
7465
7514
  name: requiredString(o.name, "create-budget payload.name"),
@@ -7493,7 +7542,7 @@ function parseBudgetDiscountPayload(raw) {
7493
7542
  const o = requireObject(raw, "update-budget-discount payload");
7494
7543
  const type = o.type;
7495
7544
  if (type !== "percentage" && type !== "amount") {
7496
- throw new Error('update-budget-discount payload.type must be "percentage" or "amount".');
7545
+ throw validationError('update-budget-discount payload.type must be "percentage" or "amount".');
7497
7546
  }
7498
7547
  return {
7499
7548
  budgetId: requiredString(o.budgetId, "update-budget-discount payload.budgetId"),
@@ -7517,28 +7566,28 @@ function parseCreateBillPayload(raw, draftOnlyFlag, quotationExempt = false, req
7517
7566
  const o = requireObject(raw, "create-bill payload");
7518
7567
  const isClaimable = o.isClaimable;
7519
7568
  if (typeof isClaimable !== "boolean") {
7520
- throw new Error("create-bill payload.isClaimable must be boolean.");
7569
+ throw validationError("create-bill payload.isClaimable must be boolean.");
7521
7570
  }
7522
7571
  const budgetItemIds = o.budgetItemIds;
7523
7572
  if (!Array.isArray(budgetItemIds) || budgetItemIds.length === 0) {
7524
- throw new Error("create-bill payload.budgetItemIds must be a non-empty string array.");
7573
+ throw validationError("create-bill payload.budgetItemIds must be a non-empty string array.");
7525
7574
  }
7526
7575
  const attachmentsRaw = o.attachments;
7527
7576
  const attachments = Array.isArray(attachmentsRaw) ? attachmentsRaw.map((x, i) => billAttachment(x, i)) : [];
7528
7577
  const draftOnlyRaw = o.draftOnly;
7529
7578
  if (draftOnlyRaw !== undefined && typeof draftOnlyRaw !== "boolean") {
7530
- throw new Error("create-bill payload.draftOnly must be boolean.");
7579
+ throw validationError("create-bill payload.draftOnly must be boolean.");
7531
7580
  }
7532
7581
  if (draftOnlyFlag !== undefined && draftOnlyRaw !== undefined && draftOnlyFlag !== draftOnlyRaw) {
7533
- throw new Error("--draft-only conflicts with payload.draftOnly.");
7582
+ throw validationError("--draft-only conflicts with payload.draftOnly.");
7534
7583
  }
7535
7584
  const draftOnly = draftOnlyFlag ?? draftOnlyRaw;
7536
7585
  const alreadyPaidRaw = o.alreadyPaid;
7537
7586
  if (draftOnly && alreadyPaidRaw === true) {
7538
- throw new Error("Draft-only creation cannot be combined with alreadyPaid: true.");
7587
+ throw validationError("Draft-only creation cannot be combined with alreadyPaid: true.");
7539
7588
  }
7540
7589
  if (alreadyPaidRaw !== undefined && alreadyPaidRaw !== null && typeof alreadyPaidRaw !== "boolean") {
7541
- throw new Error("create-bill payload.alreadyPaid must be boolean.");
7590
+ throw validationError("create-bill payload.alreadyPaid must be boolean.");
7542
7591
  }
7543
7592
  const paymentProofAttachmentsRaw = o.paymentProofAttachments;
7544
7593
  const paymentProofAttachments = Array.isArray(paymentProofAttachmentsRaw) ? paymentProofAttachmentsRaw.map((x, i) => billAttachment(x, i, "paymentProofAttachments")) : [];
@@ -7548,7 +7597,7 @@ function parseCreateBillPayload(raw, draftOnlyFlag, quotationExempt = false, req
7548
7597
  const v = requireObject(vr, "verificationResults");
7549
7598
  const verificationResultStatus = v.status;
7550
7599
  if (verificationResultStatus !== BillVerificationStatus.PENDING && verificationResultStatus !== BillVerificationStatus.PASS && verificationResultStatus !== BillVerificationStatus.FAIL && verificationResultStatus !== BillVerificationStatus.MANUAL_REVIEW) {
7551
- throw new Error("create-bill payload.verificationResults.status must be PENDING | PASS | FAIL | MANUAL_REVIEW.");
7600
+ throw validationError("create-bill payload.verificationResults.status must be PENDING | PASS | FAIL | MANUAL_REVIEW.");
7552
7601
  }
7553
7602
  verificationResults = {
7554
7603
  status: verificationResultStatus,
@@ -7563,11 +7612,11 @@ function parseCreateBillPayload(raw, draftOnlyFlag, quotationExempt = false, req
7563
7612
  if (verificationStatus === BillVerificationStatus.PENDING || verificationStatus === BillVerificationStatus.PASS || verificationStatus === BillVerificationStatus.FAIL || verificationStatus === BillVerificationStatus.MANUAL_REVIEW) {
7564
7613
  vs = verificationStatus;
7565
7614
  } else if (verificationStatus !== undefined && verificationStatus !== null) {
7566
- throw new Error("create-bill payload.verificationStatus must be PENDING | PASS | FAIL | MANUAL_REVIEW.");
7615
+ throw validationError("create-bill payload.verificationStatus must be PENDING | PASS | FAIL | MANUAL_REVIEW.");
7567
7616
  }
7568
7617
  const amount = Number(o.amount);
7569
7618
  if (!Number.isFinite(amount) || amount < 0) {
7570
- throw new Error("create-bill payload.amount must be a number >= 0.");
7619
+ throw validationError("create-bill payload.amount must be a number >= 0.");
7571
7620
  }
7572
7621
  const quotationReferences = parseQuotationReferences(o, "create-bill");
7573
7622
  const parsedBudgetItemIds = budgetItemIds.map((id, i) => requiredString(id, `create-bill payload.budgetItemIds[${i}]`));
@@ -7580,16 +7629,16 @@ function parseCreateBillPayload(raw, draftOnlyFlag, quotationExempt = false, req
7580
7629
  quotationAllocations: quotationReferences.quotationAllocations
7581
7630
  });
7582
7631
  if (quotationExempt && isClaimable) {
7583
- throw new Error("--quotation-exempt is only valid for supplier bills.");
7632
+ throw validationError("--quotation-exempt is only valid for supplier bills.");
7584
7633
  }
7585
7634
  if (quotationExempt && (quotationReferences.quotationId || (quotationReferences.quotationIds?.length ?? 0) > 0 || (quotationReferences.quotationAllocations?.length ?? 0) > 0)) {
7586
- throw new Error("--quotation-exempt cannot be combined with payload quotation IDs.");
7635
+ throw validationError("--quotation-exempt cannot be combined with payload quotation IDs.");
7587
7636
  }
7588
7637
  if (isClaimable && (quotationReferences.quotationId || (quotationReferences.quotationIds?.length ?? 0) > 0 || (quotationReferences.quotationAllocations?.length ?? 0) > 0)) {
7589
- throw new Error("Employee claims cannot include quotation source inputs.");
7638
+ throw validationError("Employee claims cannot include quotation source inputs.");
7590
7639
  }
7591
7640
  if (requireSupplierQuotation && !quotationExempt && !isClaimable && !quotationReferences.quotationId && (quotationReferences.quotationIds?.length ?? 0) === 0 && (quotationReferences.quotationAllocations?.length ?? 0) === 0) {
7592
- throw new Error("create-bill supplier bills require payload.quotationAllocations, payload.quotationIds, or legacy payload.quotationId.");
7641
+ throw validationError("create-bill supplier bills require payload.quotationAllocations, payload.quotationIds, or legacy payload.quotationId.");
7593
7642
  }
7594
7643
  return {
7595
7644
  budgetId: requiredString(o.budgetId, "create-bill payload.budgetId"),
@@ -7618,7 +7667,7 @@ function parseUpdateBillPayload(raw) {
7618
7667
  const o = requireObject(raw, "update-bill payload");
7619
7668
  const { draftOnly, ...input } = parseCreateBillPayload(raw, undefined, false, false);
7620
7669
  if (draftOnly !== undefined) {
7621
- throw new Error("draftOnly is only supported when creating a bill.");
7670
+ throw validationError("draftOnly is only supported when creating a bill.");
7622
7671
  }
7623
7672
  return {
7624
7673
  ...input,
@@ -7632,21 +7681,21 @@ function parseValidateBillSelectionPayload(raw, quotationFlags = {}) {
7632
7681
  const payloadIds = payloadReferences.quotationIds ?? (payloadReferences.quotationId ? [payloadReferences.quotationId] : undefined);
7633
7682
  const flagIds = flagReferences.quotationIds ?? (flagReferences.quotationId ? [flagReferences.quotationId] : undefined);
7634
7683
  if (payloadIds && flagIds && payloadIds.join("\x00") !== flagIds.join("\x00")) {
7635
- throw new Error("validate-bill-selection quotation flags conflict with the payload quotation IDs.");
7684
+ throw validationError("validate-bill-selection quotation flags conflict with the payload quotation IDs.");
7636
7685
  }
7637
7686
  const isClaimable = input.isClaimable;
7638
7687
  if (isClaimable !== undefined && typeof isClaimable !== "boolean") {
7639
- throw new Error("validate-bill-selection payload.isClaimable must be boolean.");
7688
+ throw validationError("validate-bill-selection payload.isClaimable must be boolean.");
7640
7689
  }
7641
7690
  if (isClaimable === true && (payloadReferences.quotationId || (payloadReferences.quotationIds?.length ?? 0) > 0 || (payloadReferences.quotationAllocations?.length ?? 0) > 0 || !!flagReferences.quotationId || (flagReferences.quotationIds?.length ?? 0) > 0)) {
7642
- throw new Error("Employee claims cannot include quotation source inputs.");
7691
+ throw validationError("Employee claims cannot include quotation source inputs.");
7643
7692
  }
7644
7693
  const lineAmounts = parseBillLineAmounts(input, "validate-bill-selection");
7645
7694
  const budgetItemIds = Array.isArray(input.budgetItemIds) ? input.budgetItemIds.map((id, index) => requiredString(id, `validate-bill-selection payload.budgetItemIds[${index}]`)) : [];
7646
7695
  const amount = input.amount === undefined || input.amount === null ? undefined : Number(input.amount);
7647
7696
  if (lineAmounts || payloadReferences.quotationAllocations) {
7648
7697
  if (amount === undefined || !Number.isFinite(amount) || budgetItemIds.length === 0) {
7649
- throw new Error("validate-bill-selection explicit allocations require payload.amount and payload.budgetItemIds.");
7698
+ throw validationError("validate-bill-selection explicit allocations require payload.amount and payload.budgetItemIds.");
7650
7699
  }
7651
7700
  validateExplicitBillAllocations({
7652
7701
  amount,
@@ -7669,10 +7718,10 @@ function parseLinkBillQuotationsPayload(billId, raw) {
7669
7718
  const input = requireObject(raw, "link-bill-quotations payload");
7670
7719
  const references = parseQuotationReferences(input, "link-bill-quotations");
7671
7720
  if (references.quotationId) {
7672
- throw new Error("link-bill-quotations payload uses quotationIds or quotationAllocations; legacy quotationId is not supported.");
7721
+ throw validationError("link-bill-quotations payload uses quotationIds or quotationAllocations; legacy quotationId is not supported.");
7673
7722
  }
7674
7723
  if ((references.quotationIds?.length ?? 0) === 0 && (references.quotationAllocations?.length ?? 0) === 0) {
7675
- throw new Error("link-bill-quotations payload requires quotationIds or quotationAllocations.");
7724
+ throw validationError("link-bill-quotations payload requires quotationIds or quotationAllocations.");
7676
7725
  }
7677
7726
  return {
7678
7727
  billId: requiredString(billId, "billId"),
@@ -7725,14 +7774,14 @@ function parseUpdateQuotationPayload(raw) {
7725
7774
  var parseCreateCustomerInvoicePayload = (raw) => {
7726
7775
  const input = requireObject(raw, "create-customer-invoice payload");
7727
7776
  if (!Array.isArray(input.splits) || input.splits.length !== 1) {
7728
- throw new Error("create-customer-invoice payload.splits must contain exactly one invoice.");
7777
+ throw validationError("create-customer-invoice payload.splits must contain exactly one invoice.");
7729
7778
  }
7730
7779
  if (input.isCustomPercentage !== undefined && typeof input.isCustomPercentage !== "boolean") {
7731
- throw new Error("create-customer-invoice payload.isCustomPercentage must be a boolean.");
7780
+ throw validationError("create-customer-invoice payload.isCustomPercentage must be a boolean.");
7732
7781
  }
7733
7782
  const details = input.details === undefined ? undefined : requireObject(input.details, "invoice details");
7734
7783
  if (details && typeof details.billingAddress !== "string")
7735
- throw new Error("invoice details.billingAddress must be a string.");
7784
+ throw validationError("invoice details.billingAddress must be a string.");
7736
7785
  return {
7737
7786
  ...input.requestId === undefined ? {} : { requestId: requiredString(input.requestId, "requestId") },
7738
7787
  ...input.invoiceDate === undefined ? {} : { invoiceDate: toDate(input.invoiceDate, "invoiceDate") },
@@ -7748,7 +7797,7 @@ var parseCreateCustomerInvoicePayload = (raw) => {
7748
7797
  const split = requireObject(rawSplit, `create-customer-invoice payload.splits[${index}]`);
7749
7798
  const percentage = Number(split.percentage);
7750
7799
  if (!Number.isFinite(percentage) || percentage <= 0 || percentage > 100 || Math.round(percentage * 100) !== percentage * 100) {
7751
- throw new Error(`create-customer-invoice payload.splits[${index}].percentage must be between 0.01 and 100 with at most two decimal places.`);
7800
+ throw validationError(`create-customer-invoice payload.splits[${index}].percentage must be between 0.01 and 100 with at most two decimal places.`);
7752
7801
  }
7753
7802
  return {
7754
7803
  dueDate: toDate(split.dueDate, `create-customer-invoice payload.splits[${index}].dueDate`),
@@ -7762,7 +7811,7 @@ function parseUpdateProjectPayload(raw) {
7762
7811
  const o = requireObject(raw, "update-project payload");
7763
7812
  const dr = o.dateRange;
7764
7813
  if (dr === null || typeof dr !== "object" || Array.isArray(dr)) {
7765
- throw new Error("update-project payload.dateRange must be an object.");
7814
+ throw validationError("update-project payload.dateRange must be an object.");
7766
7815
  }
7767
7816
  const range = dr;
7768
7817
  const notification = o.requestQboAccountantNotification;
@@ -7770,14 +7819,14 @@ function parseUpdateProjectPayload(raw) {
7770
7819
  if (typeof notification === "boolean") {
7771
7820
  requestQboAccountantNotification = notification;
7772
7821
  } else if (notification !== undefined && notification !== null) {
7773
- throw new Error("update-project payload.requestQboAccountantNotification must be a boolean when set.");
7822
+ throw validationError("update-project payload.requestQboAccountantNotification must be a boolean when set.");
7774
7823
  }
7775
7824
  const estimateInvalidation = o.confirmEstimateInvalidation;
7776
7825
  let confirmEstimateInvalidation;
7777
7826
  if (typeof estimateInvalidation === "boolean") {
7778
7827
  confirmEstimateInvalidation = estimateInvalidation;
7779
7828
  } else if (estimateInvalidation !== undefined && estimateInvalidation !== null) {
7780
- throw new Error("update-project payload.confirmEstimateInvalidation must be a boolean when set.");
7829
+ throw validationError("update-project payload.confirmEstimateInvalidation must be a boolean when set.");
7781
7830
  }
7782
7831
  const slackChannelId = optionalString(o.slackChannelId);
7783
7832
  const slackChannelUrl = optionalString(o.slackChannelUrl);
@@ -7820,21 +7869,30 @@ function parseSupplierUpdatePayload(raw) {
7820
7869
  requireObject(raw, "update-supplier payload");
7821
7870
  return raw;
7822
7871
  }
7823
- function parseItemCreatePayload(raw) {
7824
- const input = requireObject(raw, "create-item payload");
7872
+ var normalizeItemPayload = (input, command) => {
7873
+ if (typeof input.name === "string") {
7874
+ const invalidCharacters = [
7875
+ ...new Set([...input.name].filter((character) => !/^[A-Za-z0-9,.?@&!#'~* _;+-]$/.test(character)))
7876
+ ];
7877
+ if (invalidCharacters.length > 0) {
7878
+ throw validationError(`${command} payload.name contains characters that are not allowed: ${invalidCharacters.join(" ")}. Allowed: letters, numbers, spaces and , . ? @ & ! # ' ~ * _ ; + -`);
7879
+ }
7880
+ }
7881
+ const normalized = {
7882
+ ...input,
7883
+ ...typeof input.cost === "number" && Number.isFinite(input.cost) ? { cost: String(input.cost) } : {}
7884
+ };
7825
7885
  return normalizePlainTextRichTextField({
7826
- input,
7886
+ input: normalized,
7827
7887
  textField: "description",
7828
7888
  richTextField: "descriptionRichText"
7829
7889
  });
7890
+ };
7891
+ function parseItemCreatePayload(raw) {
7892
+ return normalizeItemPayload(requireObject(raw, "create-item payload"), "create-item");
7830
7893
  }
7831
7894
  function parseItemUpdatePayload(raw) {
7832
- const input = requireObject(raw, "update-item payload");
7833
- return normalizePlainTextRichTextField({
7834
- input,
7835
- textField: "description",
7836
- richTextField: "descriptionRichText"
7837
- });
7895
+ return normalizeItemPayload(requireObject(raw, "update-item payload"), "update-item");
7838
7896
  }
7839
7897
  function parseContactCreatePayload(raw) {
7840
7898
  requireObject(raw, "create-contact-person payload");
@@ -8211,10 +8269,10 @@ var formatAsanaCandidateMetadata = (candidate) => {
8211
8269
  };
8212
8270
  var promptForAsanaLeadCandidate = async (candidates) => {
8213
8271
  if (candidates.length === 0) {
8214
- throw new Error("No open Asana lead tasks were found in the Deals project. Pass --asanaTaskId explicitly or update the Deals project first.");
8272
+ throw new Error("create-project requires an existing Asana deal task in the Deals project; none matched. For a new direct client, create the deal task using the website form first, then pass --asanaTaskId or --asanaSearch. The Slack channel comes from that task.");
8215
8273
  }
8216
8274
  if (!process.stdin.isTTY || !process.stderr.isTTY) {
8217
- 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.");
8275
+ throw new Error("create-project requires an existing Asana deal task: pass --asanaTaskId in non-interactive runs. Create the deal task using the website form first for a new direct client. The Slack channel comes from that task; interactive runs can choose from matching tasks.");
8218
8276
  }
8219
8277
  console.error("Select the Asana lead task to link to this project:");
8220
8278
  candidates.forEach((candidate, index) => {
@@ -9141,7 +9199,15 @@ async function getBudgetDetails(budgetId) {
9141
9199
  }
9142
9200
  async function createBudgetFromPayload(raw) {
9143
9201
  const input = parseCreateBudgetPayload(raw);
9144
- const result = await api2.budget.createBudget.mutate(input);
9202
+ let result;
9203
+ try {
9204
+ result = await api2.budget.createBudget.mutate(input);
9205
+ } catch (error) {
9206
+ const uncertain = budgetCreateUncertainError(error, input);
9207
+ if (uncertain)
9208
+ throw uncertain;
9209
+ throw error;
9210
+ }
9145
9211
  out(result);
9146
9212
  }
9147
9213
  async function updateBudgetFromPayload(raw) {
@@ -9543,31 +9609,44 @@ async function deleteBillById(id) {
9543
9609
  }
9544
9610
  async function createSupplierFromPayload(raw) {
9545
9611
  const input = parseSupplierCreatePayload(raw);
9546
- const created = await api2.supplier.createSupplier.mutate(input);
9612
+ await assertSensitiveWorkflowConfirmed({
9613
+ action: "Create supplier and request approval if required",
9614
+ entity: `supplier ${input.name}`,
9615
+ details: "creates the supplier first; pending-approval suppliers also request approval and send emails"
9616
+ });
9617
+ let created;
9618
+ try {
9619
+ created = await api2.supplier.createSupplier.mutate(input);
9620
+ } catch (error) {
9621
+ const normalized = normalizeCliError(error);
9622
+ if (normalized.code !== "NETWORK" && normalized.code !== "API") {
9623
+ throw error;
9624
+ }
9625
+ throw new CliRuntimeError(normalized.code, `Supplier creation outcome is uncertain for ${JSON.stringify(input.name)}. Run list-suppliers and inspect matching supplier details before retrying; names may not be unique, so do not retry create-supplier until you can verify that no supplier was created. Original error: ${normalized.message}`);
9626
+ }
9547
9627
  const supplier = created.result;
9548
9628
  if (supplier.status !== ApprovalStatus.PENDING_APPROVAL) {
9549
9629
  out(created);
9550
9630
  return;
9551
9631
  }
9552
- await assertSensitiveWorkflowConfirmed({
9553
- action: "Create supplier approval request",
9554
- entity: `supplier ${supplier.id}`,
9555
- details: "and send approval request emails"
9556
- });
9557
- const approval = await api2.supplier.createSupplierApproval.mutate({
9558
- supplierId: supplier.id
9559
- });
9632
+ let approval;
9633
+ try {
9634
+ approval = await api2.supplier.createSupplierApproval.mutate({
9635
+ supplierId: supplier.id
9636
+ });
9637
+ } catch (error) {
9638
+ throw new CliRuntimeError("API", `Supplier ${supplier.id} was created, but the approval request did not complete or its outcome is uncertain. Run get-supplier-details ${supplier.id} and inspect its approvals before retrying; do not run create-supplier again. Original error: ${normalizeCliError(error).message}`);
9639
+ }
9560
9640
  let emailResult;
9561
9641
  const approvalIds = (approval.results ?? []).map((item) => ({ id: item.id }));
9562
9642
  if (approvalIds.length === 0) {
9563
- emailResult = createApprovalEmailDeliveryFailure("No approval ids returned");
9564
- } else {
9565
- try {
9566
- const delivery = approval.emailDelivery;
9567
- emailResult = createApprovalEmailDeliveryReport(delivery);
9568
- } catch (error) {
9569
- emailResult = createApprovalEmailDeliveryFailure(error instanceof Error ? error.message : String(error));
9570
- }
9643
+ throw new CliRuntimeError("API", `Supplier ${supplier.id} was created, but the approval request returned no approval IDs. Run get-supplier-details ${supplier.id} and inspect its approvals before retrying the approval request; do not run create-supplier again.`);
9644
+ }
9645
+ try {
9646
+ const delivery = approval.emailDelivery;
9647
+ emailResult = createApprovalEmailDeliveryReport(delivery);
9648
+ } catch (error) {
9649
+ emailResult = createApprovalEmailDeliveryFailure(error instanceof Error ? error.message : String(error));
9571
9650
  }
9572
9651
  out({
9573
9652
  ...created,
@@ -10312,24 +10391,167 @@ function getFlag(flags, key, parse) {
10312
10391
  // src/parse-json-flag.ts
10313
10392
  function parseJsonFlag(raw, label) {
10314
10393
  if (raw === undefined || raw === "") {
10315
- throw new Error(`${label} is required (JSON string).`);
10394
+ throw new CliRuntimeError("VALIDATION", `${label} is required (JSON string).`);
10316
10395
  }
10317
10396
  try {
10318
10397
  return JSON.parse(raw);
10319
10398
  } catch {
10320
- throw new Error(`${label} must be valid JSON.`);
10399
+ throw new CliRuntimeError("VALIDATION", `${label} must be valid JSON.`);
10321
10400
  }
10322
10401
  }
10402
+ function parseJsonObjectFlag(raw, label) {
10403
+ const value = parseJsonFlag(raw, label);
10404
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
10405
+ throw new CliRuntimeError("VALIDATION", `${label} must be a JSON object.`);
10406
+ }
10407
+ return value;
10408
+ }
10323
10409
  function parseOptionalNumber(raw, label) {
10324
10410
  if (raw === undefined || raw === "")
10325
10411
  return;
10326
10412
  const n = typeof raw === "number" ? raw : Number.parseFloat(String(raw).trim());
10327
10413
  if (!Number.isFinite(n)) {
10328
- throw new Error(`${label} must be a number.`);
10414
+ throw new CliRuntimeError("VALIDATION", `${label} must be a number.`);
10329
10415
  }
10330
10416
  return n;
10331
10417
  }
10332
10418
 
10419
+ // src/registry/payload-help.ts
10420
+ var payloadHelp = {
10421
+ "create-budget": {
10422
+ required: "name: string; budget: decimal string; paymentTerm: string; categoryId: string; projectId: string",
10423
+ optional: "sourceBudgetId: string; unavailableItemReviewAcknowledged: boolean (only after review); pipedriveDealId: string",
10424
+ example: '{"name":"Event budget","budget":"1500","paymentTerm":"Net 30","categoryId":"<category-id>","projectId":"<project-id>"}'
10425
+ },
10426
+ "update-budget": {
10427
+ required: "id: string; name: string; budget: decimal string; paymentTerm: string; categoryId: string; projectId: string",
10428
+ optional: "sourceBudgetId: string; pipedriveDealId: string",
10429
+ example: '{"id":"<budget-id>","name":"Event budget","budget":"1500","paymentTerm":"Net 30","categoryId":"<category-id>","projectId":"<project-id>"}'
10430
+ },
10431
+ "create-item": {
10432
+ required: "name: string (QuickBooks-safe characters); categoryId: string; supplierId: string; cost: decimal string or number",
10433
+ optional: "description: string; gstInclusive: boolean; gstOutOfScope: boolean; goLabsProduct: boolean; attachments: array",
10434
+ example: '{"name":"Event Crew per hour","categoryId":"<category-id>","supplierId":"<supplier-id>","cost":19}'
10435
+ },
10436
+ "update-item": {
10437
+ required: "id: string; name: string; categoryId: string; supplierId: string; cost: decimal string or number",
10438
+ optional: "description: string; gstInclusive: boolean; gstOutOfScope: boolean; goLabsProduct: boolean; attachments: array",
10439
+ example: '{"id":"<item-id>","name":"Event Crew per hour","categoryId":"<category-id>","supplierId":"<supplier-id>","cost":"19"}'
10440
+ },
10441
+ "create-contact-person": {
10442
+ required: "companyId: string; title: MR|MRS|MS|DR; firstName: string; lastName: string|null; email: string; mobile: string",
10443
+ optional: "address: {streetAddress1?, city?, state?, postalCode?, country?}",
10444
+ example: '{"companyId":"<company-id>","title":"MS","firstName":"Jane","lastName":"Tan","email":"jane@example.com","mobile":"+65 9123 4567"}'
10445
+ },
10446
+ "update-contact-person": {
10447
+ required: "id: string; companyId: string; title: MR|MRS|MS|DR; firstName: string; lastName: string|null; email: string; mobile: string",
10448
+ optional: "address: object",
10449
+ example: '{"id":"<contact-id>","companyId":"<company-id>","title":"MS","firstName":"Jane","lastName":"Tan","email":"jane@example.com","mobile":"+65 9123 4567"}'
10450
+ },
10451
+ "update-company": {
10452
+ required: "id: string; name: string; website: URL string",
10453
+ optional: "confirmDistinct: boolean",
10454
+ example: '{"id":"<company-id>","name":"Example Co","website":"https://example.com"}'
10455
+ },
10456
+ "update-project": {
10457
+ required: "id: string; name: string; companyId: string; contactPersonId: string; asanaTaskId: string; insideSalesId: string; businessDevelopmentId: string; venue: string; dateRange: {from: ISO date}",
10458
+ optional: "dateRange.to: ISO date; projectManagerId: string; pax: string; description: string; slackChannelId/slackChannelUrl/slackChannelName: strings (supply all three); requestQboAccountantNotification: boolean; confirmEstimateInvalidation: boolean",
10459
+ example: '{"id":"<project-id>","name":"Launch","companyId":"<company-id>","contactPersonId":"<contact-id>","asanaTaskId":"<asana-task-id>","insideSalesId":"<user-id>","businessDevelopmentId":"<user-id>","venue":"Hall","dateRange":{"from":"2026-10-01T09:00:00Z"}}'
10460
+ },
10461
+ "create-supplier": {
10462
+ required: "name: string; primaryContactPerson: string; email: string; phone: string; paymentMethodId: string; paymentTerm: string; roleId: string",
10463
+ optional: "gstRegistered: boolean; tags: string[]; website: URL string; address: object; attachments: array; bank and PayNow fields",
10464
+ example: '{"name":"Example Supplier","primaryContactPerson":"Jane Tan","email":"jane@example.com","phone":"91234567","paymentMethodId":"<method-id>","paymentTerm":"Net 30","roleId":"<role-id>"}'
10465
+ },
10466
+ "update-supplier": {
10467
+ required: "id: string; name: string; primaryContactPerson: string; email: string; phone: string; paymentMethodId: string; paymentTerm: string; roleId: string",
10468
+ optional: "gstRegistered: boolean; tags: string[]; website: URL string; address: object; attachments: array; bank and PayNow fields",
10469
+ example: '{"id":"<supplier-id>","name":"Example Supplier","primaryContactPerson":"Jane Tan","email":"jane@example.com","phone":"91234567","paymentMethodId":"<method-id>","paymentTerm":"Net 30","roleId":"<role-id>"}'
10470
+ },
10471
+ "update-budget-commission": {
10472
+ required: "budgetId: string; name: string; percentage: decimal string; totalSellingAfterDiscount: number",
10473
+ optional: "none",
10474
+ example: '{"budgetId":"<budget-id>","name":"Sales commission","percentage":"10","totalSellingAfterDiscount":1500}'
10475
+ },
10476
+ "update-budget-discount": {
10477
+ required: "budgetId: string; name: string; type: percentage|amount; value: decimal string; totalSellingBeforeDiscount: number",
10478
+ optional: "none",
10479
+ example: '{"budgetId":"<budget-id>","name":"Discount","type":"amount","value":"100","totalSellingBeforeDiscount":1500}'
10480
+ },
10481
+ "send-estimate-to-contact-person": {
10482
+ required: "estimateId: string; budgetId: string; to: email; cc: email[]; replyTo: email; subject: string; content: HTML string; signature: HTML string",
10483
+ optional: "estimateDocNumber: string",
10484
+ example: '{"estimateId":"<estimate-id>","budgetId":"<budget-id>","to":"client@example.com","cc":[],"replyTo":"team@example.com","subject":"Estimate","content":"<p>Attached estimate</p>","signature":"<p>Team</p>"}'
10485
+ },
10486
+ "create-bill": {
10487
+ required: "budgetId: string; projectId: string; supplierId: string; amount: number (ex-GST); budgetItemIds: string[]; isClaimable: boolean; supplier bills: quotationIds: string[] or quotationAllocations: {budgetItemId, quotationId, amount}[] (unless --quotation-exempt)",
10488
+ optional: "lineAmounts: {budgetItemId, amount}[] (required with quotationAllocations); attachments: {id,name,key,size}[]; invoiceNumber: string; invoiceDate: ISO date; draftOnly: boolean; alreadyPaid: boolean; extractedAmount: number",
10489
+ example: '{"budgetId":"<budget-id>","projectId":"<project-id>","supplierId":"<supplier-id>","amount":1800,"budgetItemIds":["<budget-item-id>"],"isClaimable":false,"quotationIds":["<quotation-id>"],"attachments":[]}'
10490
+ },
10491
+ "update-bill": {
10492
+ required: "id: string; budgetId: string; projectId: string; supplierId: string; amount: number (ex-GST); budgetItemIds: string[]; isClaimable: boolean",
10493
+ optional: "quotationIds: string[]; quotationAllocations: {budgetItemId, quotationId, amount}[]; lineAmounts: {budgetItemId, amount}[]; attachments: {id,name,key,size}[]; invoiceNumber: string; invoiceDate: ISO date",
10494
+ example: '{"id":"<bill-id>","budgetId":"<budget-id>","projectId":"<project-id>","supplierId":"<supplier-id>","amount":1800,"budgetItemIds":["<budget-item-id>"],"isClaimable":false,"quotationIds":["<quotation-id>"],"attachments":[]}'
10495
+ },
10496
+ "validate-bill-selection": {
10497
+ required: "projectId: string; supplierId: string; budgetItemIds: string[]",
10498
+ optional: "quotationIds: string[] for supplier bills; isClaimable: boolean; amount: number; lineAmounts: {budgetItemId, amount}[]; quotationAllocations: {budgetItemId, quotationId, amount}[] (requires amount and lineAmounts)",
10499
+ example: '{"projectId":"<project-id>","supplierId":"<supplier-id>","budgetItemIds":["<budget-item-id>"],"quotationIds":["<quotation-id>"]}'
10500
+ },
10501
+ "link-bill-quotations": {
10502
+ required: "positional <billId>; quotationIds: string[] or quotationAllocations: {budgetItemId, quotationId, amount}[]",
10503
+ optional: "none (legacy quotationId is not supported)",
10504
+ example: '{"quotationIds":["<quotation-id>"]}'
10505
+ },
10506
+ "update-bill-payment-evidence": {
10507
+ required: "id: string; paymentReference: string; paymentProofAttachments: {id,name (PDF),key,size}[] (at least one)",
10508
+ optional: "none",
10509
+ example: '{"id":"<bill-id>","paymentReference":"TRANSFER-123","paymentProofAttachments":[{"id":"<attachment-id>","name":"receipt.pdf","key":"<staged-key>","size":1000}]}'
10510
+ },
10511
+ "create-quotation": {
10512
+ required: "projectId: string; budgetId: string; supplierId: string; budgetItemIds: string[]; amountWithoutGst: number; gstAmount: number; attachments: array (uploaded attachment)",
10513
+ optional: "lineAmounts: {budgetItemId, amount}[] (required for multiple budget items)",
10514
+ example: '{"projectId":"<project-id>","budgetId":"<budget-id>","supplierId":"<supplier-id>","budgetItemIds":["<budget-item-id>"],"amountWithoutGst":1200,"gstAmount":0,"attachments":[{"id":"<attachment-id>","name":"quote.pdf","key":"<staged-key>","size":1000}]}'
10515
+ },
10516
+ "update-quotation": {
10517
+ required: "id: string; projectId: string; budgetId: string; supplierId: string; budgetItemIds: string[]; amountWithoutGst: number; gstAmount: number; attachments: array",
10518
+ optional: "lineAmounts: {budgetItemId, amount}[] (required for multiple budget items)",
10519
+ example: '{"id":"<quotation-id>","projectId":"<project-id>","budgetId":"<budget-id>","supplierId":"<supplier-id>","budgetItemIds":["<budget-item-id>"],"amountWithoutGst":1200,"gstAmount":0,"attachments":[{"id":"<attachment-id>","name":"quote.pdf","key":"<staged-key>","size":1000}]}'
10520
+ },
10521
+ "create-customer-invoice": {
10522
+ required: "budgetId: string; splits: [{label: string, percentage: number, dueDate: ISO date}] (exactly one)",
10523
+ optional: "requestId: UUID string (recommended); invoiceDate: ISO date; details: {billingAddress: string, customerMemo: string}; isCustomPercentage: boolean",
10524
+ example: '{"budgetId":"<budget-id>","splits":[{"label":"Deposit","percentage":50,"dueDate":"2026-10-15"}],"details":{"billingAddress":"1 Example St","customerMemo":"Thanks"}}'
10525
+ },
10526
+ "send-customer-invoice-to-contact-person": {
10527
+ required: "invoiceId: string; to: email; cc: email[]; replyTo: email; subject: string; content: HTML string; signature: HTML string",
10528
+ optional: "none",
10529
+ example: '{"invoiceId":"<invoice-id>","to":"client@example.com","cc":[],"replyTo":"team@example.com","subject":"Invoice","content":"<p>Attached invoice</p>","signature":"<p>Team</p>"}'
10530
+ },
10531
+ "save-inbound-mapping": {
10532
+ required: "positional <sourceId>; payload: [{canonicalField: enum, typeformFieldId: string, required?: boolean, typeformFieldRef?: string|null}] (1-50 entries)",
10533
+ optional: "required defaults false; typeformFieldRef may be omitted",
10534
+ example: '[{"canonicalField":"CONTACT_FIRST_NAME","typeformFieldId":"<typeform-field-id>"}]'
10535
+ },
10536
+ "save-inbound-automation-config": {
10537
+ required: "positional <sourceId>; asanaCustomFields: object; asanaDefaultAssigneeId: string|null; asanaDefaultAssigneeName: string|null; asanaFollowerIds: string[]; asanaProjectId: string; asanaSectionId: string; asanaWonSectionId: string; asanaLostSectionId: string; googleCalendarId: string; googleCalendarTimeZone: string; goshBriefBaseUrl: URL; slackDefaultAssigneeId: string|null; slackInboundChannelId: string; slackInviteUserIds: string[]; slackPitchChannelPrefix: string",
10538
+ optional: "asanaDiscussionSectionId, asanaFirstContactSectionId, asanaFirstProposalSentSectionId, asanaProposalWipSectionId: string|null; asanaCustomFields.*: mapped IDs",
10539
+ example: '{"asanaCustomFields":{},"asanaDefaultAssigneeId":null,"asanaDefaultAssigneeName":null,"asanaFollowerIds":[],"asanaProjectId":"<asana-project-id>","asanaSectionId":"<asana-section-id>","asanaWonSectionId":"<asana-won-id>","asanaLostSectionId":"<asana-lost-id>","googleCalendarId":"team@example.com","googleCalendarTimeZone":"Asia/Singapore","goshBriefBaseUrl":"https://example.com/brief","slackDefaultAssigneeId":null,"slackInboundChannelId":"<slack-channel-id>","slackInviteUserIds":[],"slackPitchChannelPrefix":"pitch"}'
10540
+ }
10541
+ };
10542
+ var formatPayloadHelp = (command) => {
10543
+ const help = payloadHelp[command];
10544
+ if (!help)
10545
+ return [];
10546
+ return [
10547
+ "Payload fields (--payload JSON)",
10548
+ ` Required: ${help.required}`,
10549
+ ` Optional: ${help.optional}`,
10550
+ ` Example: bb ${command}${command === "link-bill-quotations" ? " <billId>" : command.startsWith("save-inbound-") ? " <sourceId>" : ""} --payload '${help.example}'`,
10551
+ ""
10552
+ ];
10553
+ };
10554
+
10333
10555
  // src/registry/index.ts
10334
10556
  var globalCommandOptions = [
10335
10557
  { name: "--help", description: "Show help for this command." },
@@ -10530,6 +10752,8 @@ var summaryFor = (legacyTarget) => {
10530
10752
  return "Reuse a company in BB and QuickBooks, creating only its missing counterpart. Supply --companyId <bbId> and/or --quickbooksId <qboId>; both explicitly connect records with different names. --website <url> fills a missing QBO import website only. --confirmDistinct true confirms no similar record represents this entity.";
10531
10753
  if (legacyTarget === "create-company")
10532
10754
  return "Create or reuse a company in BB and QuickBooks. --name <name> --website <url>. Similar names require --confirmDistinct true; inactive companies are never reactivated.";
10755
+ if (legacyTarget === "create-project")
10756
+ return "Create a project linked to an existing Asana Deals lead task. Supply --asanaTaskId in non-interactive use; interactive use searches for and selects a lead task (optionally --asanaSearch). The Slack channel is resolved from the selected Asana task unless all three Slack channel flags are supplied. For a new direct client, create the lead task using the website form first.";
10533
10757
  if (legacyTarget === "preview-qbo-customer-invoice") {
10534
10758
  return "Preview an available QuickBooks invoice selected for a BB budget using read-only QBO access. Arguments: <budgetId> <qboInvoiceId>.";
10535
10759
  }
@@ -10587,6 +10811,20 @@ var legacyCommand = (legacyTarget, path, aliases = [], commandOptions = []) => (
10587
10811
  effects: effectsFor(legacyTarget),
10588
10812
  legacyTarget
10589
10813
  });
10814
+ var budgetPreflightOptions = [
10815
+ {
10816
+ name: "--companyPayload <json>",
10817
+ description: "Company update JSON object with id, name, and website fields."
10818
+ },
10819
+ {
10820
+ name: "--contactPayload <json>",
10821
+ description: "Contact update JSON object with companyId, title, firstName, lastName, email, and mobile fields."
10822
+ },
10823
+ {
10824
+ name: "--eventPayload <json>",
10825
+ description: "Event timing JSON object with date, startTime, and endTime fields."
10826
+ }
10827
+ ];
10590
10828
  var authCommand = (action, summary) => ({
10591
10829
  path: ["auth", action],
10592
10830
  legacyAliases: [],
@@ -10611,8 +10849,8 @@ var registry2 = [
10611
10849
  legacyCommand("rename-budget-version", ["budget", "version", "rename"]),
10612
10850
  legacyCommand("restore-budget-version", ["budget", "version", "restore"]),
10613
10851
  legacyCommand("update-budget-status", ["budget", "status", "update"]),
10614
- legacyCommand("create-budget-approval", ["budget", "approval", "create"]),
10615
- legacyCommand("create-estimate", ["budget", "estimate", "create"]),
10852
+ legacyCommand("create-budget-approval", ["budget", "approval", "create"], [], budgetPreflightOptions),
10853
+ legacyCommand("create-estimate", ["budget", "estimate", "create"], [], budgetPreflightOptions),
10616
10854
  legacyCommand("send-estimate-to-contact-person", ["budget", "estimate", "send"], ["send-estimate-email"]),
10617
10855
  legacyCommand("create-budget", ["budget", "create"]),
10618
10856
  legacyCommand("update-budget", ["budget", "update"]),
@@ -11046,6 +11284,7 @@ var createHumanHelp = (command) => {
11046
11284
  ...command.options.map((option) => ` ${option.name}
11047
11285
  ${option.description}`),
11048
11286
  "",
11287
+ ...formatPayloadHelp(command.legacyTarget ?? ""),
11049
11288
  "Use `bb help --legacy` for additional command arguments."
11050
11289
  ].join(`
11051
11290
  `);
@@ -11932,9 +12171,9 @@ Waiting for approval\u2026
11932
12171
  const contactPayloadRaw = getFlag(flags, "contactPayload");
11933
12172
  const eventPayloadRaw = getFlag(flags, "eventPayload");
11934
12173
  await createBudgetApproval(id, {
11935
- company: companyPayloadRaw !== undefined ? parseJsonFlag(String(companyPayloadRaw), "--companyPayload") : undefined,
11936
- contactPerson: contactPayloadRaw !== undefined ? parseJsonFlag(String(contactPayloadRaw), "--contactPayload") : undefined,
11937
- eventDetails: eventPayloadRaw !== undefined ? parseJsonFlag(String(eventPayloadRaw), "--eventPayload") : undefined
12174
+ company: companyPayloadRaw !== undefined ? parseJsonObjectFlag(String(companyPayloadRaw), "--companyPayload") : undefined,
12175
+ contactPerson: contactPayloadRaw !== undefined ? parseJsonObjectFlag(String(contactPayloadRaw), "--contactPayload") : undefined,
12176
+ eventDetails: eventPayloadRaw !== undefined ? parseJsonObjectFlag(String(eventPayloadRaw), "--eventPayload") : undefined
11938
12177
  });
11939
12178
  break;
11940
12179
  }
@@ -11946,9 +12185,9 @@ Waiting for approval\u2026
11946
12185
  const contactPayloadRaw = getFlag(flags, "contactPayload");
11947
12186
  const eventPayloadRaw = getFlag(flags, "eventPayload");
11948
12187
  await createEstimate(id, {
11949
- company: companyPayloadRaw !== undefined ? parseJsonFlag(String(companyPayloadRaw), "--companyPayload") : undefined,
11950
- contactPerson: contactPayloadRaw !== undefined ? parseJsonFlag(String(contactPayloadRaw), "--contactPayload") : undefined,
11951
- eventDetails: eventPayloadRaw !== undefined ? parseJsonFlag(String(eventPayloadRaw), "--eventPayload") : undefined
12188
+ company: companyPayloadRaw !== undefined ? parseJsonObjectFlag(String(companyPayloadRaw), "--companyPayload") : undefined,
12189
+ contactPerson: contactPayloadRaw !== undefined ? parseJsonObjectFlag(String(contactPayloadRaw), "--contactPayload") : undefined,
12190
+ eventDetails: eventPayloadRaw !== undefined ? parseJsonObjectFlag(String(eventPayloadRaw), "--eventPayload") : undefined
11952
12191
  });
11953
12192
  break;
11954
12193
  }