@slothmoney/agent-cli 0.18.1 → 0.20.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/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.20.0 - 2026-08-27
4
+
5
+ - Add `portfolio` views for personal, partner-shared, and combined household
6
+ savings and investments.
7
+ - Add account-level partner visibility updates for private, balance-only, and
8
+ balance-and-holdings sharing.
9
+ - Keep portfolio results strict, machine-readable, and explicit about refresh
10
+ status and excluded currencies.
11
+
12
+ ## 0.19.0 - 2026-08-26
13
+
14
+ - Make every Goal use one explicit personal Goal-funding account and expose
15
+ the calculated `forecastMonthKey`, effective priority, account details, and
16
+ `forecastBasis` in Goal results.
17
+ - Make `goals create` preview against Sloth's active scenario on the server;
18
+ preview performs no writes and `--apply` remains the write boundary.
19
+ - Add `--account-ref` and `--priority` to Goal creation, allow account
20
+ reassignment during Goal updates, and rename account eligibility to
21
+ `isGoalFundingAccount` / `--goal-funding-account`.
22
+ - Surface corrective API error codes without adding affordability judgments or
23
+ desired-date comparisons to the CLI.
24
+
3
25
  ## 0.18.1 - 2026-08-25
4
26
 
5
27
  - Require the deployed historical-activity response for `budget status` and
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Sloth Agent CLI
2
2
 
3
- Use your own agent to inspect accounts, investments, and budgets, manage goals, move assigned budget money, update planned amounts, categorise transactions, and configure payment notifications through the
3
+ Use your own agent to inspect personal and household accounts, investments, and budgets, manage goals, move assigned budget money, update planned amounts, categorise transactions, and configure payment notifications through the
4
4
  [Sloth Money Agent API](https://slothmoney.app/developers/).
5
5
 
6
6
  ## Install
@@ -24,7 +24,7 @@ Create a personal access token in Sloth Money under
24
24
  **Settings > Developer access**, then choose the authentication method for
25
25
  where the CLI runs.
26
26
 
27
- New tokens are view-only. That is enough for `auth status`, `accounts`, `investments`,
27
+ New tokens are view-only. That is enough for `auth status`, `accounts`, `investments`, `portfolio`,
28
28
  `budget`, `categories`, `transactions`, and `goals` list. Enable **Allow changes** when
29
29
  creating the token only if the CLI must apply assignments, manage categories
30
30
  or line items, move assigned budget money, update planned budgets, manage accounts, ask a partner for an explanation, or manage goals. Token
@@ -103,6 +103,7 @@ options, output, and examples. For example:
103
103
  ```bash
104
104
  sloth-agent auth login --help
105
105
  sloth-agent accounts --help
106
+ sloth-agent portfolio --help
106
107
  sloth-agent budget --help
107
108
  sloth-agent budget status --help
108
109
  sloth-agent budget update --help
@@ -584,7 +585,8 @@ sloth-agent accounts
584
585
  The command is read-only and cache-only: it does not refresh linked banks or
585
586
  change account data. Each result contains an opaque `accountRef`, personal or
586
587
  joint ownership, connected or manual source, native balance/currency when
587
- known, `lastBalanceUpdatedAt`, `connectionState`, and `isGoalSavingsSource`.
588
+ known, `lastBalanceUpdatedAt`, `connectionState`, `isGoalFundingAccount`, and
589
+ `partnerVisibility`.
588
590
  Missing values are JSON
589
591
  `null`; currencies are never converted or combined. Partner personal accounts
590
592
  are excluded, while enabled shared joint accounts follow Sloth's existing
@@ -604,9 +606,9 @@ Copy the value from `sloth-agent accounts`. Account references are the public
604
606
  account identifier for transaction filtering.
605
607
 
606
608
  Account changes are previews unless `--apply` is present. Connected accounts
607
- support only goal-savings membership. Manual current accounts support their
609
+ support Goal-funding eligibility and partner visibility. Manual current accounts support their
608
610
  institution, name, currency, and ownership. Manual balance accounts also
609
- support balance, Savings/Investments type, and goal-savings membership.
611
+ support balance, Savings/Investments type, and Goal-funding eligibility.
610
612
  Partner-owned shared accounts return an explanatory error.
611
613
 
612
614
  ```bash
@@ -618,14 +620,31 @@ sloth-agent accounts update \
618
620
  --ownership individual \
619
621
  --balance-amount 12500.75 \
620
622
  --account-type investments \
621
- --goal-savings-source false
623
+ --goal-funding-account false \
624
+ --partner-visibility holdings
622
625
 
623
626
  sloth-agent accounts update \
624
627
  --account-ref sloth_account_v1_... \
625
- --goal-savings-source false \
628
+ --partner-visibility balance \
626
629
  --apply
627
630
  ```
628
631
 
632
+ Read the same current position from your, your partner's, or the combined
633
+ household perspective:
634
+
635
+ ```bash
636
+ sloth-agent portfolio
637
+ sloth-agent portfolio --view partner
638
+ sloth-agent portfolio --view household
639
+ ```
640
+
641
+ The command waits up to 45 seconds for eligible linked balances to refresh,
642
+ then returns cached data if work continues. Partner accounts appear only when
643
+ their owner has shared the balance or linked holdings. Sharing is for household
644
+ planning only. It does not change account ownership, transaction access, Goal
645
+ funding, or who can move money. Totals use the viewer's budget currency and
646
+ exclude other native currencies without converting them.
647
+
629
648
  Archive an owned manual account. The account disappears from active Sloth
630
649
  surfaces, but its underlying records are retained. Repeating an applied removal
631
650
  is safe and returns `changed: false`.
@@ -664,16 +683,24 @@ Goal writes are previews unless `--apply` is present:
664
683
  sloth-agent goals create \
665
684
  --name "Emergency fund" \
666
685
  --target-amount 12000 \
667
- --type keep
686
+ --type keep \
687
+ --account-ref sloth_account_v1_...
668
688
 
669
689
  sloth-agent goals create \
670
690
  --name "Wedding" \
671
691
  --target-amount 22000 \
672
692
  --target-month 2027-06 \
673
693
  --type spend \
694
+ --account-ref sloth_account_v1_... \
674
695
  --apply
675
696
  ```
676
697
 
698
+ Without `--apply`, Goal creation authenticates and asks Sloth to calculate the
699
+ Goal without writing it. Preview and apply use the same active-scenario planner
700
+ and return `forecastMonthKey`, the effective priority, the funding account, and
701
+ `forecastBasis`. A null forecast includes the final projected month. The CLI
702
+ does not compare the desired and forecast dates or return affordability advice.
703
+
677
704
  Every goal is either Keep or Spend. A Keep goal continues reserving its funded
678
705
  money. A Spend goal reserves money until you explicitly mark it spent. Goal
679
706
  list, create, and update output includes lowercase `goalType` and nullable
@@ -687,6 +714,7 @@ move it in the priority order:
687
714
  sloth-agent goals update \
688
715
  --goal-id goal-id \
689
716
  --target-month 2027-12 \
717
+ --account-ref sloth_account_v1_... \
690
718
  --type spend \
691
719
  --apply
692
720
 
@@ -715,13 +743,10 @@ required recovery action. Restoring clears `spentAt` and returns the goal to
715
743
  allocation at its saved priority. Deleting a goal also removes its forecast
716
744
  assignments and drift history.
717
745
 
718
- Goal sharing remains app-managed. Change an active shared goal's pot-tracked
719
- target amount in the Sloth Budget app, where account balances can be
720
- reallocated across goals in priority order. Goal list output includes a
746
+ Goal sharing remains app-managed. Goal list output includes a
721
747
  one-based `priority`; `1` is highest. Moving one goal automatically shifts the
722
- goals between its old and new positions. The priority option must be used on
723
- its own. Forecast assignments and shared pot progress are browser-owned
724
- derived state and refresh when the owner next opens the Forecast screen.
748
+ goals between its old and new positions. Sloth recalculates the active-scenario
749
+ roadmap before every applied Goal mutation and returns the updated forecast.
725
750
 
726
751
  Read uncategorised contributions to the joint budget:
727
752
 
package/dist/args.js CHANGED
@@ -389,7 +389,8 @@ function parseAccounts(args, baseUrl) {
389
389
  '--ownership',
390
390
  '--balance-amount',
391
391
  '--account-type',
392
- '--goal-savings-source',
392
+ '--goal-funding-account',
393
+ '--partner-visibility',
393
394
  ]));
394
395
  const institutionName = values.get('--institution-name');
395
396
  const accountName = values.get('--account-name');
@@ -397,7 +398,8 @@ function parseAccounts(args, baseUrl) {
397
398
  const ownershipValue = values.get('--ownership');
398
399
  const balanceValue = values.get('--balance-amount');
399
400
  const accountTypeValue = values.get('--account-type');
400
- const sourceValue = values.get('--goal-savings-source');
401
+ const sourceValue = values.get('--goal-funding-account');
402
+ const partnerVisibility = values.get('--partner-visibility');
401
403
  if (currencyValue !== undefined && !/^[A-Za-z]{3}$/.test(currencyValue)) {
402
404
  throw new UsageError('--currency must be a three-letter currency code');
403
405
  }
@@ -412,7 +414,13 @@ function parseAccounts(args, baseUrl) {
412
414
  throw new UsageError('--account-type must be savings or investments');
413
415
  }
414
416
  if (sourceValue !== undefined && sourceValue !== 'true' && sourceValue !== 'false') {
415
- throw new UsageError('--goal-savings-source must be true or false');
417
+ throw new UsageError('--goal-funding-account must be true or false');
418
+ }
419
+ if (partnerVisibility !== undefined
420
+ && partnerVisibility !== 'private'
421
+ && partnerVisibility !== 'balance'
422
+ && partnerVisibility !== 'holdings') {
423
+ throw new UsageError('--partner-visibility must be private, balance, or holdings');
416
424
  }
417
425
  const update = {
418
426
  ...(institutionName === undefined
@@ -429,7 +437,10 @@ function parseAccounts(args, baseUrl) {
429
437
  ...(accountTypeValue === undefined
430
438
  ? {}
431
439
  : { accountType: accountTypeValue }),
432
- ...(sourceValue === undefined ? {} : { isGoalSavingsSource: sourceValue === 'true' }),
440
+ ...(sourceValue === undefined ? {} : { isGoalFundingAccount: sourceValue === 'true' }),
441
+ ...(partnerVisibility === undefined ? {} : {
442
+ partnerVisibility: partnerVisibility,
443
+ }),
433
444
  };
434
445
  if (Object.keys(update).length === 0) {
435
446
  throw new UsageError('accounts update requires at least one field to update');
@@ -471,6 +482,16 @@ function parseInvestments(args, baseUrl) {
471
482
  }
472
483
  return withBaseUrl({ command: 'investments', ...(accountRef ? { accountRef } : {}) }, baseUrl);
473
484
  }
485
+ function parsePortfolio(args, baseUrl) {
486
+ const { values, apply } = parseNamedOptions(args, 'portfolio', new Set(['--view']));
487
+ if (apply)
488
+ throw new UsageError('portfolio does not accept --apply');
489
+ const view = values.get('--view') ?? 'mine';
490
+ if (view !== 'mine' && view !== 'partner' && view !== 'household') {
491
+ throw new UsageError('--view must be mine, partner, or household');
492
+ }
493
+ return withBaseUrl({ command: 'portfolio', view }, baseUrl);
494
+ }
474
495
  function parseBudget(args, baseUrl) {
475
496
  const subcommand = args[0] === 'status' || args[0] === 'update' || args[0] === 'move'
476
497
  ? args.shift()
@@ -652,6 +673,8 @@ function parseGoals(args, baseUrl) {
652
673
  let targetAmount;
653
674
  let targetMonthKey;
654
675
  let goalType;
676
+ let fundingAccountRef;
677
+ let priority;
655
678
  let apply = false;
656
679
  for (let index = 0; index < args.length; index += 1) {
657
680
  const argument = args[index];
@@ -667,7 +690,9 @@ function parseGoals(args, baseUrl) {
667
690
  if (option !== '--name'
668
691
  && option !== '--target-amount'
669
692
  && option !== '--target-month'
670
- && option !== '--type') {
693
+ && option !== '--type'
694
+ && option !== '--account-ref'
695
+ && option !== '--priority') {
671
696
  throw new UsageError(`Unknown goals create option: ${argument}`);
672
697
  }
673
698
  const value = requireNonEmpty(inlineValue ?? readOptionValue(args, index, option), option);
@@ -682,9 +707,15 @@ function parseGoals(args, baseUrl) {
682
707
  else if (option === '--target-month') {
683
708
  targetMonthKey = setOnce(targetMonthKey, parseGoalMonthKey(value, option), option);
684
709
  }
685
- else {
710
+ else if (option === '--type') {
686
711
  goalType = setOnce(goalType, parseGoalType(value), option);
687
712
  }
713
+ else if (option === '--account-ref') {
714
+ fundingAccountRef = setOnce(fundingAccountRef, parseAccountRef(value), option);
715
+ }
716
+ else {
717
+ priority = setOnce(priority, parseGoalPriority(value), option);
718
+ }
688
719
  }
689
720
  if (!name)
690
721
  throw new UsageError('goals create requires --name <name>');
@@ -694,12 +725,17 @@ function parseGoals(args, baseUrl) {
694
725
  if (goalType === undefined) {
695
726
  throw new UsageError('goals create requires --type <keep|spend>');
696
727
  }
728
+ if (fundingAccountRef === undefined) {
729
+ throw new UsageError('goals create requires --account-ref <accountRef>');
730
+ }
697
731
  return withBaseUrl({
698
732
  command: 'goals-create',
699
733
  name,
700
734
  targetAmount,
701
735
  ...(targetMonthKey === undefined ? {} : { targetMonthKey }),
702
736
  goalType,
737
+ fundingAccountRef,
738
+ ...(priority === undefined ? {} : { priority }),
703
739
  apply,
704
740
  }, baseUrl);
705
741
  }
@@ -709,6 +745,7 @@ function parseGoals(args, baseUrl) {
709
745
  let targetAmount;
710
746
  let targetMonthKey;
711
747
  let goalType;
748
+ let fundingAccountRef;
712
749
  let priority;
713
750
  let apply = false;
714
751
  for (let index = 0; index < args.length; index += 1) {
@@ -734,6 +771,7 @@ function parseGoals(args, baseUrl) {
734
771
  && option !== '--target-amount'
735
772
  && option !== '--target-month'
736
773
  && option !== '--type'
774
+ && option !== '--account-ref'
737
775
  && option !== '--priority') {
738
776
  throw new UsageError(`Unknown goals update option: ${argument}`);
739
777
  }
@@ -758,6 +796,9 @@ function parseGoals(args, baseUrl) {
758
796
  else if (option === '--priority') {
759
797
  priority = setOnce(priority, parseGoalPriority(value), option);
760
798
  }
799
+ else if (option === '--account-ref') {
800
+ fundingAccountRef = setOnce(fundingAccountRef, parseAccountRef(value), option);
801
+ }
761
802
  else {
762
803
  goalType = setOnce(goalType, parseGoalType(value), option);
763
804
  }
@@ -768,16 +809,10 @@ function parseGoals(args, baseUrl) {
768
809
  && targetAmount === undefined
769
810
  && targetMonthKey === undefined
770
811
  && goalType === undefined
812
+ && fundingAccountRef === undefined
771
813
  && priority === undefined) {
772
814
  throw new UsageError('goals update requires at least one field to update');
773
815
  }
774
- if (priority !== undefined
775
- && (name !== undefined
776
- || targetAmount !== undefined
777
- || targetMonthKey !== undefined
778
- || goalType !== undefined)) {
779
- throw new UsageError('--priority must be used on its own');
780
- }
781
816
  return withBaseUrl({
782
817
  command: 'goals-update',
783
818
  goalId,
@@ -785,6 +820,7 @@ function parseGoals(args, baseUrl) {
785
820
  ...(targetAmount === undefined ? {} : { targetAmount }),
786
821
  ...(targetMonthKey === undefined ? {} : { targetMonthKey }),
787
822
  ...(goalType === undefined ? {} : { goalType }),
823
+ ...(fundingAccountRef === undefined ? {} : { fundingAccountRef }),
788
824
  ...(priority === undefined ? {} : { priority }),
789
825
  apply,
790
826
  }, baseUrl);
@@ -964,8 +1000,8 @@ function helpTopic(argv) {
964
1000
  return 'accounts-remove';
965
1001
  return command;
966
1002
  }
967
- if (command === 'investments')
968
- return 'investments';
1003
+ if (command === 'investments' || command === 'portfolio')
1004
+ return command;
969
1005
  return undefined;
970
1006
  }
971
1007
  export function parseArgs(argv) {
@@ -1000,6 +1036,9 @@ export function parseArgs(argv) {
1000
1036
  if (command === 'investments') {
1001
1037
  return parseInvestments(args, baseUrl);
1002
1038
  }
1039
+ if (command === 'portfolio') {
1040
+ return parsePortfolio(args, baseUrl);
1041
+ }
1003
1042
  if (command === 'budget') {
1004
1043
  return parseBudget(args, baseUrl);
1005
1044
  }
package/dist/cli.js CHANGED
@@ -6,7 +6,7 @@ import { ICON_KEYS } from './category-metadata.js';
6
6
  import { parseApiResponse, parseAssignmentOperationResponse, toLegacyAssignmentResponse, validateAssignmentPayload, validateBudgetMovementResponse, validateBudgetUpdatePayload, validateNotificationRulePayload, validateReceiptConfirmation, } from './contracts.js';
7
7
  import { createSystemCredentialStore, secureStorageUnavailableError, } from './credential-store.js';
8
8
  import { ApiError, CliError, ConfigError, UsageError, } from './errors.js';
9
- export const CLI_VERSION = '0.18.1';
9
+ export const CLI_VERSION = '0.20.0';
10
10
  const REQUEST_TIMEOUT_MS = 60_000;
11
11
  const MAX_CONTRACT_PDF_BYTES = 6_000_000;
12
12
  const API_ORIGIN_HELP_LINES = [
@@ -34,6 +34,7 @@ export function usageText() {
34
34
  ' sloth-agent accounts update --account-ref REF [fields] [--apply]',
35
35
  ' sloth-agent accounts remove --account-ref REF [--apply]',
36
36
  ' sloth-agent investments [--account-ref REF] [--base-url URL]',
37
+ ' sloth-agent portfolio [--view mine|partner|household] [--base-url URL]',
37
38
  ' sloth-agent budget --scope personal|joint [--period YYYY-MM] [--base-url URL]',
38
39
  ' sloth-agent budget status --scope personal|joint [--period YYYY-MM] [--base-url URL]',
39
40
  ' sloth-agent budget update --scope personal|joint [--period YYYY-MM]',
@@ -61,7 +62,8 @@ export function usageText() {
61
62
  ' sloth-agent receipts remove --transaction-ref REF --revision N [--apply]',
62
63
  ' sloth-agent goals [list] [--base-url URL]',
63
64
  ' sloth-agent goals create --name NAME --target-amount AMOUNT',
64
- ' --type keep|spend [--target-month YYYY-MM] [--apply] [--base-url URL]',
65
+ ' --type keep|spend --account-ref REF [--target-month YYYY-MM]',
66
+ ' [--priority POSITION] [--apply] [--base-url URL]',
65
67
  ' sloth-agent goals update --goal-id ID [fields] [--apply] [--base-url URL]',
66
68
  ' sloth-agent goals mark-spent --goal-id ID [--apply] [--base-url URL]',
67
69
  ' sloth-agent goals restore --goal-id ID [--apply] [--base-url URL]',
@@ -354,7 +356,7 @@ export function accountsHelpText() {
354
356
  ' accounts[].ownership personal or joint',
355
357
  ' accounts[].balanceAmount and currency in the native currency when known',
356
358
  ' accounts[].connectionState and lastBalanceUpdatedAt for freshness',
357
- ' accounts[].isGoalSavingsSource whether the owner uses it for goal savings',
359
+ ' accounts[].isGoalFundingAccount whether Goals may use the account',
358
360
  ].join('\n');
359
361
  }
360
362
  export function accountsUpdateHelpText() {
@@ -376,13 +378,16 @@ export function accountsUpdateHelpText() {
376
378
  ' --ownership individual|joint Manual account ownership.',
377
379
  ' --balance-amount AMOUNT Balance-only account balance.',
378
380
  ' --account-type savings|investments Balance-only account type.',
379
- ' --goal-savings-source true|false Goal-savings membership.',
381
+ ' --goal-funding-account true|false Whether Goals may use this account.',
382
+ ' --partner-visibility private|balance|holdings',
383
+ ' What this account shares with your partner.',
380
384
  '',
381
385
  'Write behavior:',
382
386
  ' Without --apply, returns a JSON preview without credentials or a network request.',
383
387
  ' With --apply, requires agent:write on a write-enabled token and updates saved Sloth metadata.',
384
- ' Connected accounts support only --goal-savings-source.',
385
- ' Manual current accounts cannot change type, balance, or goal-savings membership.',
388
+ ' Connected accounts support --goal-funding-account and --partner-visibility.',
389
+ ' Sharing exposes planning data only. It does not change ownership or assign the account to Goals.',
390
+ ' Manual current accounts cannot change type, balance, or Goal-funding membership.',
386
391
  ' Partner-owned shared accounts cannot be changed.',
387
392
  ' Unknown, disconnected, or inaccessible references return Account not found.',
388
393
  ...API_ORIGIN_HELP_LINES,
@@ -441,6 +446,33 @@ export function investmentsHelpText() {
441
446
  ' provider-native and are not converted or guaranteed to reconcile to totals.',
442
447
  ].join('\n');
443
448
  }
449
+ export function portfolioHelpText() {
450
+ return [
451
+ 'Sloth Agent CLI — portfolio',
452
+ '',
453
+ 'Read your savings and investments from one household planning perspective.',
454
+ '',
455
+ 'Usage:',
456
+ ' sloth-agent portfolio [--view mine|partner|household] [--base-url URL]',
457
+ '',
458
+ 'Options:',
459
+ ' --view mine|partner|household Optional. Defaults to mine.',
460
+ ' --base-url URL Optional. Override the API origin.',
461
+ ' -h, --help Show this help.',
462
+ ...API_ORIGIN_HELP_LINES,
463
+ '',
464
+ 'Access:',
465
+ ' This read-only command waits up to 45 seconds for eligible linked balances to refresh.',
466
+ ' Partner shows only balances or holdings your partner explicitly shared.',
467
+ ' Household combines your accounts with those shared balances and deduplicates joint accounts.',
468
+ ' Shared data supports planning only. It does not assign partner accounts to Goals or change ownership.',
469
+ '',
470
+ 'Output:',
471
+ ' totals gives savings, investments, and tracked amounts in the viewer currency.',
472
+ ' accounts includes ownerRole, freshness, sharing level, and permitted holdings.',
473
+ ' refresh reports whether eligible linked balances refreshed or cached data was returned.',
474
+ ].join('\n');
475
+ }
444
476
  export function budgetHelpText() {
445
477
  return [
446
478
  'Sloth Agent CLI — budget',
@@ -720,7 +752,7 @@ export function goalsHelpText() {
720
752
  return [
721
753
  'Sloth Agent CLI — goals',
722
754
  '',
723
- 'List, create, update, mark spent, restore, or delete your savings goals.',
755
+ 'List, preview, create, update, mark spent, restore, or delete account-funded Goals.',
724
756
  '',
725
757
  'Commands:',
726
758
  ' sloth-agent goals list List goals; "sloth-agent goals" is equivalent.',
@@ -754,9 +786,9 @@ export function goalsListHelpText() {
754
786
  ' This command is read-only.',
755
787
  '',
756
788
  'Output:',
757
- ' JSON containing currency and goals. Each goal contains id, name, priority,',
758
- ' targetAmount, targetMonthKey, goalType, nullable spentAt, and',
759
- ' sharedWithPartner.',
789
+ ' JSON containing currency, forecastBasis, and goals. Each goal includes its',
790
+ ' effectivePriority, funding account, desired targetMonthKey, and calculated',
791
+ ' forecastMonthKey.',
760
792
  ].join('\n');
761
793
  }
762
794
  export function goalsCreateHelpText() {
@@ -766,30 +798,33 @@ export function goalsCreateHelpText() {
766
798
  'Preview or create a goal.',
767
799
  '',
768
800
  'Usage:',
769
- ' sloth-agent goals create --name NAME --target-amount AMOUNT --type keep|spend [options]',
801
+ ' sloth-agent goals create --name NAME --target-amount AMOUNT --type keep|spend --account-ref REF [options]',
770
802
  '',
771
803
  'Options:',
772
804
  ' --name NAME Required. Goal name, 1 to 200 characters.',
773
805
  ' --target-amount AMOUNT Required. Positive major-unit amount with up to 2 decimals.',
774
806
  ' --type keep|spend Required. Keep reserves funded money; Spend is spent later.',
775
- ' --target-month YYYY-MM Optional. Target calendar month.',
807
+ ' --account-ref REF Required. Personal Goal-funding account from accounts list.',
808
+ ' --target-month YYYY-MM Optional. Desired calendar month; it does not change the forecast.',
809
+ ' --priority POSITION Optional. One-based priority; defaults to append.',
776
810
  ' --apply Optional. Create the goal in Sloth Money.',
777
811
  ' --base-url URL Optional. Override the API origin.',
778
812
  ' -h, --help Show this help.',
779
813
  ...API_ORIGIN_HELP_LINES,
780
814
  '',
781
815
  'Safety:',
782
- ' Without --apply, the command returns a dry-run preview and does not write.',
816
+ ' Without --apply, the command authenticates and asks Sloth to calculate the Goal.',
817
+ ' The server performs no writes for a preview.',
783
818
  ' Applying requires a write-enabled token created with Allow changes.',
784
- ' New goals are private to the owner and appended to the existing goal order.',
819
+ ' New goals are private to the owner and append unless --priority is supplied.',
785
820
  '',
786
821
  'Example:',
787
- ' sloth-agent goals create --name "Emergency fund" --target-amount 12000 --type keep',
788
- ' sloth-agent goals create --name "Wedding" --target-amount 22000 --type spend --target-month 2027-06 --apply',
822
+ ' sloth-agent goals create --name "Emergency fund" --target-amount 12000 --type keep --account-ref REF',
823
+ ' sloth-agent goals create --name "Wedding" --target-amount 22000 --type spend --account-ref REF --target-month 2027-06 --apply',
789
824
  '',
790
825
  'Output:',
791
- ' Preview mode returns dryRun, method, endpoint, and payload.',
792
- ' Apply mode returns the persisted goal and currency from the 201 response.',
826
+ ' Preview and apply return the Goal, currency, calculated forecastMonthKey,',
827
+ ' effective priority, funding-account details, and forecastBasis.',
793
828
  ].join('\n');
794
829
  }
795
830
  export function goalsUpdateHelpText() {
@@ -808,6 +843,7 @@ export function goalsUpdateHelpText() {
808
843
  ' --target-month YYYY-MM Optional. Replace the target month.',
809
844
  ' --clear-target-month Optional. Remove the target month.',
810
845
  ' --type keep|spend Optional. Change how funded money is treated.',
846
+ ' --account-ref REF Optional. Reassign to another personal Goal-funding account.',
811
847
  ' --priority POSITION Optional. Positive whole-number position; 1 is highest.',
812
848
  ' --apply Optional. Write the partial update.',
813
849
  ' --base-url URL Optional. Override the API origin.',
@@ -816,17 +852,13 @@ export function goalsUpdateHelpText() {
816
852
  '',
817
853
  'Constraints:',
818
854
  ' Provide at least one field to update.',
819
- ' Priority must be updated on its own.',
820
855
  ' Priority 1 is highest. The position cannot exceed the current goal count.',
821
856
  ' Moving a goal shifts the intervening goals automatically.',
822
- ' Forecast assignments and shared progress refresh when the owner next opens',
823
- ' the Forecast screen.',
857
+ ' Sloth recalculates the Goal roadmap before saving the update.',
824
858
  ' Set and clear target-month options are mutually exclusive.',
825
859
  ' Restore a spent goal before changing its type.',
826
- ' Change active shared pot target amounts in the Sloth Budget app, where',
827
- ' account balances can be reconciled across goals in priority order.',
828
- ' Sharing remains app-managed. Updates to an already shared goal remain visible',
829
- ' to the connected partner.',
860
+ ' Sharing remains app-managed. Updates to a shared Goal remain visible to the',
861
+ ' connected partner, but its assigned account remains private to the owner.',
830
862
  '',
831
863
  'Safety:',
832
864
  ' Without --apply, the command returns a dry-run preview and does not write.',
@@ -838,7 +870,8 @@ export function goalsUpdateHelpText() {
838
870
  '',
839
871
  'Output:',
840
872
  ' Preview mode returns dryRun, method, endpoint, and payload.',
841
- ' Apply mode returns the complete persisted goal and currency.',
873
+ ' Apply mode returns the complete persisted Goal, calculated forecastMonthKey,',
874
+ ' effective priority, funding-account details, currency, and forecastBasis.',
842
875
  ].join('\n');
843
876
  }
844
877
  export function goalsMarkSpentHelpText() {
@@ -1171,6 +1204,7 @@ export function commandHelpText(topic) {
1171
1204
  'accounts-update': accountsUpdateHelpText,
1172
1205
  'accounts-remove': accountsRemoveHelpText,
1173
1206
  investments: investmentsHelpText,
1207
+ portfolio: portfolioHelpText,
1174
1208
  budget: budgetHelpText,
1175
1209
  'budget-status': budgetStatusHelpText,
1176
1210
  'budget-move': budgetMoveHelpText,
@@ -1207,23 +1241,6 @@ export function commandHelpText(topic) {
1207
1241
  function writeJson(write, data) {
1208
1242
  write(`${JSON.stringify(data, null, 2)}\n`);
1209
1243
  }
1210
- function withListedGoalPriorities(value) {
1211
- const response = value;
1212
- return {
1213
- ...response,
1214
- goals: response.goals.map((goal, index) => ({
1215
- ...goal,
1216
- priority: index + 1,
1217
- })),
1218
- };
1219
- }
1220
- function withUpdatedGoalPriority(value, priority) {
1221
- const response = value;
1222
- return {
1223
- ...response,
1224
- goal: { ...response.goal, priority },
1225
- };
1226
- }
1227
1244
  function redact(value, token) {
1228
1245
  return token ? value.split(token).join('[REDACTED]') : value;
1229
1246
  }
@@ -1404,7 +1421,11 @@ async function parseHttpResponse(response, token) {
1404
1421
  && typeof data.error === 'string')
1405
1422
  ? data.error
1406
1423
  : `Agent API request failed with status ${response.status}`;
1407
- throw new ApiError(redact(message, token), response.status);
1424
+ const code = (data
1425
+ && typeof data === 'object'
1426
+ && 'code' in data
1427
+ && typeof data.code === 'string') ? data.code : undefined;
1428
+ throw new ApiError(redact(message, token), response.status, code);
1408
1429
  }
1409
1430
  return data;
1410
1431
  }
@@ -1891,7 +1912,7 @@ export async function runCli(argv = process.argv.slice(2), options = {}) {
1891
1912
  return 0;
1892
1913
  }
1893
1914
  if (parsed.command === 'goals-create') {
1894
- const endpoint = `${baseUrl}/api/agent/v1/goals`;
1915
+ const endpoint = `${baseUrl}/api/agent/v1/goals${parsed.apply ? '' : '/preview'}`;
1895
1916
  const payload = {
1896
1917
  name: parsed.name,
1897
1918
  targetAmount: parsed.targetAmount,
@@ -1899,16 +1920,9 @@ export async function runCli(argv = process.argv.slice(2), options = {}) {
1899
1920
  ? {}
1900
1921
  : { targetMonthKey: parsed.targetMonthKey }),
1901
1922
  goalType: parsed.goalType,
1923
+ fundingAccountRef: parsed.fundingAccountRef,
1924
+ ...(parsed.priority === undefined ? {} : { priority: parsed.priority }),
1902
1925
  };
1903
- if (!parsed.apply) {
1904
- writeJson(writeStdout, {
1905
- dryRun: true,
1906
- endpoint,
1907
- method: 'POST',
1908
- payload,
1909
- });
1910
- return 0;
1911
- }
1912
1926
  const response = await fetchImplementation(endpoint, {
1913
1927
  method: 'POST',
1914
1928
  headers: {
@@ -1918,7 +1932,7 @@ export async function runCli(argv = process.argv.slice(2), options = {}) {
1918
1932
  body: JSON.stringify(payload),
1919
1933
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
1920
1934
  });
1921
- const data = parseApiResponse('goals-create', await parseHttpResponse(response, token));
1935
+ const data = parseApiResponse(parsed.apply ? 'goals-create' : 'goals-preview', await parseHttpResponse(response, token));
1922
1936
  writeJson(writeStdout, data);
1923
1937
  return 0;
1924
1938
  }
@@ -1938,6 +1952,9 @@ export async function runCli(argv = process.argv.slice(2), options = {}) {
1938
1952
  ...(parsed.goalType === undefined
1939
1953
  ? {}
1940
1954
  : { goalType: parsed.goalType }),
1955
+ ...(parsed.fundingAccountRef === undefined
1956
+ ? {}
1957
+ : { fundingAccountRef: parsed.fundingAccountRef }),
1941
1958
  ...(parsed.priority === undefined
1942
1959
  ? {}
1943
1960
  : { priority: parsed.priority }),
@@ -1962,9 +1979,7 @@ export async function runCli(argv = process.argv.slice(2), options = {}) {
1962
1979
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
1963
1980
  });
1964
1981
  const data = parseApiResponse(parsed.command, await parseHttpResponse(response, token));
1965
- writeJson(writeStdout, parsed.command !== 'goals-update' || parsed.priority === undefined
1966
- ? data
1967
- : withUpdatedGoalPriority(data, parsed.priority));
1982
+ writeJson(writeStdout, data);
1968
1983
  return 0;
1969
1984
  }
1970
1985
  if (parsed.command === 'goals-delete') {
@@ -2059,7 +2074,7 @@ export async function runCli(argv = process.argv.slice(2), options = {}) {
2059
2074
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
2060
2075
  });
2061
2076
  const data = parseApiResponse('goals-list', await parseHttpResponse(response, token));
2062
- writeJson(writeStdout, withListedGoalPriorities(data));
2077
+ writeJson(writeStdout, data);
2063
2078
  return 0;
2064
2079
  }
2065
2080
  if (parsed.command === 'budget' || parsed.command === 'budget-status') {
@@ -2080,19 +2095,21 @@ export async function runCli(argv = process.argv.slice(2), options = {}) {
2080
2095
  }
2081
2096
  const path = parsed.command === 'accounts'
2082
2097
  ? '/api/agent/v1/accounts'
2083
- : parsed.command === 'investments'
2084
- ? `/api/agent/v1/investments${parsed.accountRef
2085
- ? `?${new URLSearchParams({ accountRef: parsed.accountRef }).toString()}`
2086
- : ''}`
2087
- : parsed.command === 'categories'
2088
- ? '/api/agent/v1/categories'
2089
- : `/api/agent/v1/transactions${(() => {
2090
- const query = buildTransactionsQuery(parsed.filters);
2091
- return query ? `?${query}` : '';
2092
- })()}`;
2098
+ : parsed.command === 'portfolio'
2099
+ ? `/api/agent/v1/portfolio?${new URLSearchParams({ view: parsed.view }).toString()}`
2100
+ : parsed.command === 'investments'
2101
+ ? `/api/agent/v1/investments${parsed.accountRef
2102
+ ? `?${new URLSearchParams({ accountRef: parsed.accountRef }).toString()}`
2103
+ : ''}`
2104
+ : parsed.command === 'categories'
2105
+ ? '/api/agent/v1/categories'
2106
+ : `/api/agent/v1/transactions${(() => {
2107
+ const query = buildTransactionsQuery(parsed.filters);
2108
+ return query ? `?${query}` : '';
2109
+ })()}`;
2093
2110
  const response = await fetchImplementation(`${baseUrl}${path}`, {
2094
2111
  method: 'GET',
2095
- headers: parsed.command === 'transactions'
2112
+ headers: parsed.command === 'transactions' || parsed.command === 'portfolio'
2096
2113
  ? { ...headers, Prefer: 'wait=45' }
2097
2114
  : headers,
2098
2115
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
@@ -2104,7 +2121,8 @@ export async function runCli(argv = process.argv.slice(2), options = {}) {
2104
2121
  catch (error) {
2105
2122
  const exitCode = error instanceof CliError ? error.exitCode : 1;
2106
2123
  const message = error instanceof Error ? error.message : String(error);
2107
- writeStderr(`${redact(message, token)}\n`);
2124
+ const prefix = error instanceof ApiError && error.code ? `${error.code}: ` : '';
2125
+ writeStderr(`${prefix}${redact(message, token)}\n`);
2108
2126
  return exitCode;
2109
2127
  }
2110
2128
  }
package/dist/contracts.js CHANGED
@@ -610,6 +610,25 @@ function hasOnlyFields(value, fields) {
610
610
  const allowed = new Set(fields);
611
611
  return Object.keys(value).every((key) => allowed.has(key));
612
612
  }
613
+ function isMonthKeyOrNull(value) {
614
+ return value === null || (typeof value === 'string'
615
+ && /^\d{4}-(0[1-9]|1[0-2])$/.test(value));
616
+ }
617
+ function isPlannedGoalFields(value) {
618
+ return (typeof value.name === 'string'
619
+ && value.name.trim().length > 0
620
+ && typeof value.targetAmount === 'number'
621
+ && Number.isFinite(value.targetAmount)
622
+ && value.targetAmount > 0
623
+ && isMonthKeyOrNull(value.targetMonthKey)
624
+ && isMonthKeyOrNull(value.forecastMonthKey)
625
+ && isGoalType(value.goalType)
626
+ && Number.isSafeInteger(value.effectivePriority)
627
+ && Number(value.effectivePriority) > 0
628
+ && isAccountRef(value.fundingAccountRef)
629
+ && typeof value.fundingAccountLabel === 'string'
630
+ && value.fundingAccountLabel.trim().length > 0);
631
+ }
613
632
  function isGoal(value) {
614
633
  return (isObject(value)
615
634
  && hasOnlyFields(value, [
@@ -617,20 +636,17 @@ function isGoal(value) {
617
636
  'name',
618
637
  'targetAmount',
619
638
  'targetMonthKey',
639
+ 'forecastMonthKey',
620
640
  'goalType',
621
641
  'spentAt',
622
642
  'sharedWithPartner',
643
+ 'effectivePriority',
644
+ 'fundingAccountRef',
645
+ 'fundingAccountLabel',
623
646
  ])
624
647
  && typeof value.id === 'string'
625
648
  && value.id.trim().length > 0
626
- && typeof value.name === 'string'
627
- && value.name.trim().length > 0
628
- && (value.targetAmount === null
629
- || (typeof value.targetAmount === 'number' && Number.isFinite(value.targetAmount)))
630
- && (value.targetMonthKey === null
631
- || (typeof value.targetMonthKey === 'string'
632
- && /^\d{4}-(0[1-9]|1[0-2])$/.test(value.targetMonthKey)))
633
- && isGoalType(value.goalType)
649
+ && isPlannedGoalFields(value)
634
650
  && (value.goalType === 'spend'
635
651
  ? value.spentAt === null || isIsoDateTime(value.spentAt)
636
652
  : value.spentAt === null)
@@ -844,7 +860,8 @@ function isAccount(value) {
844
860
  'source',
845
861
  'lastBalanceUpdatedAt',
846
862
  'connectionState',
847
- 'isGoalSavingsSource',
863
+ 'isGoalFundingAccount',
864
+ 'partnerVisibility',
848
865
  ])
849
866
  && isAccountRef(value.accountRef)
850
867
  && isNullableNonEmptyString(value.accountName)
@@ -863,7 +880,10 @@ function isAccount(value) {
863
880
  || value.connectionState === 'expired'
864
881
  || value.connectionState === 'manual'
865
882
  || value.connectionState === 'unknown')
866
- && typeof value.isGoalSavingsSource === 'boolean');
883
+ && typeof value.isGoalFundingAccount === 'boolean'
884
+ && (value.partnerVisibility === 'private'
885
+ || value.partnerVisibility === 'balance'
886
+ || value.partnerVisibility === 'holdings'));
867
887
  }
868
888
  function isAccountsResponse(value) {
869
889
  return (isObject(value)
@@ -929,19 +949,104 @@ function isInvestmentsResponse(value) {
929
949
  && holdings.every(isInvestmentHolding));
930
950
  }));
931
951
  }
952
+ function isPortfolioAccount(value) {
953
+ return isObject(value)
954
+ && hasOnlyFields(value, [
955
+ 'accountRef', 'ownerRole', 'accountName', 'institutionName', 'accountType',
956
+ 'ownership', 'balanceAmount', 'currency', 'source', 'lastBalanceUpdatedAt',
957
+ 'connectionState', 'partnerVisibility', 'isGoalFundingAccount', 'holdings',
958
+ ])
959
+ && isAccountRef(value.accountRef)
960
+ && (value.ownerRole === 'you' || value.ownerRole === 'partner')
961
+ && isNullableNonEmptyString(value.accountName)
962
+ && isNullableNonEmptyString(value.institutionName)
963
+ && (value.accountType === 'savings' || value.accountType === 'investments')
964
+ && (value.ownership === 'personal' || value.ownership === 'joint')
965
+ && (value.balanceAmount === null || (typeof value.balanceAmount === 'number' && Number.isFinite(value.balanceAmount)))
966
+ && isCurrency(value.currency)
967
+ && (value.source === 'connected' || value.source === 'manual')
968
+ && (value.lastBalanceUpdatedAt === null || isIsoDateTime(value.lastBalanceUpdatedAt))
969
+ && ['active', 'expired', 'manual', 'unknown'].includes(String(value.connectionState))
970
+ && ['private', 'balance', 'holdings'].includes(String(value.partnerVisibility))
971
+ && (value.isGoalFundingAccount === null || typeof value.isGoalFundingAccount === 'boolean')
972
+ && Array.isArray(value.holdings)
973
+ && value.holdings.every(isInvestmentHolding);
974
+ }
975
+ function isPortfolioResponse(value) {
976
+ if (!isObject(value) || !hasOnlyFields(value, [
977
+ 'asOf', 'currency', 'view', 'hasPartner', 'totals',
978
+ 'excludedCurrencyAccountCount', 'accounts', 'refresh',
979
+ ]))
980
+ return false;
981
+ const totals = value.totals;
982
+ const refresh = value.refresh;
983
+ return isIsoDateTime(value.asOf)
984
+ && isCurrency(value.currency)
985
+ && ['mine', 'partner', 'household'].includes(String(value.view))
986
+ && typeof value.hasPartner === 'boolean'
987
+ && isObject(totals)
988
+ && hasOnlyFields(totals, ['savingsAmount', 'investmentsAmount', 'trackedAmount'])
989
+ && ['savingsAmount', 'investmentsAmount', 'trackedAmount'].every(field => (typeof totals[field] === 'number' && Number.isFinite(totals[field])))
990
+ && Number.isSafeInteger(value.excludedCurrencyAccountCount)
991
+ && Number(value.excludedCurrencyAccountCount) >= 0
992
+ && Array.isArray(value.accounts)
993
+ && value.accounts.every(isPortfolioAccount)
994
+ && isObject(refresh)
995
+ && hasOnlyFields(refresh, ['status', 'reason', 'utcDate'])
996
+ && ['skipped', 'completed', 'in_progress', 'partial', 'failed'].includes(String(refresh.status))
997
+ && typeof refresh.reason === 'string'
998
+ && refresh.reason.length > 0
999
+ && isIsoDate(refresh.utcDate);
1000
+ }
1001
+ function isForecastBasis(value) {
1002
+ return isObject(value)
1003
+ && hasOnlyFields(value, [
1004
+ 'calculatedAt',
1005
+ 'activeScenarioRevision',
1006
+ 'projectionThroughMonthKey',
1007
+ ])
1008
+ && isIsoDateTime(value.calculatedAt)
1009
+ && Number.isSafeInteger(value.activeScenarioRevision)
1010
+ && Number(value.activeScenarioRevision) >= 0
1011
+ && isMonthKeyOrNull(value.projectionThroughMonthKey) === true
1012
+ && value.projectionThroughMonthKey !== null;
1013
+ }
1014
+ function isGoalPreview(value) {
1015
+ return isObject(value)
1016
+ && hasOnlyFields(value, [
1017
+ 'name',
1018
+ 'targetAmount',
1019
+ 'targetMonthKey',
1020
+ 'forecastMonthKey',
1021
+ 'goalType',
1022
+ 'effectivePriority',
1023
+ 'fundingAccountRef',
1024
+ 'fundingAccountLabel',
1025
+ ])
1026
+ && isPlannedGoalFields(value);
1027
+ }
932
1028
  function isGoalsResponse(value) {
933
1029
  return (isObject(value)
934
- && hasOnlyFields(value, ['currency', 'goals'])
1030
+ && hasOnlyFields(value, ['currency', 'forecastBasis', 'goals'])
935
1031
  && isCurrency(value.currency)
1032
+ && isForecastBasis(value.forecastBasis)
936
1033
  && Array.isArray(value.goals)
937
1034
  && value.goals.every(isGoal));
938
1035
  }
939
1036
  function isGoalMutationResponse(value) {
940
1037
  return (isObject(value)
941
- && hasOnlyFields(value, ['currency', 'goal'])
1038
+ && hasOnlyFields(value, ['currency', 'forecastBasis', 'goal'])
942
1039
  && isCurrency(value.currency)
1040
+ && isForecastBasis(value.forecastBasis)
943
1041
  && isGoal(value.goal));
944
1042
  }
1043
+ function isGoalPreviewResponse(value) {
1044
+ return isObject(value)
1045
+ && hasOnlyFields(value, ['currency', 'forecastBasis', 'goal'])
1046
+ && isCurrency(value.currency)
1047
+ && isForecastBasis(value.forecastBasis)
1048
+ && isGoalPreview(value.goal);
1049
+ }
945
1050
  function isGoalDeleteResponse(value) {
946
1051
  return (isObject(value)
947
1052
  && hasOnlyFields(value, ['deleted', 'deletedGoalId'])
@@ -1007,45 +1112,49 @@ export function parseApiResponse(command, value) {
1007
1112
  ? isAccountRemovalResponse(value)
1008
1113
  : command === 'investments'
1009
1114
  ? isInvestmentsResponse(value)
1010
- : command === 'budget' || command === 'budget-update'
1011
- ? isBudgetResponse(value)
1012
- : command === 'budget-status'
1013
- ? isBudgetActivityStatusResponse(value)
1014
- : command === 'budget-move'
1015
- ? isBudgetMovementResponse(value)
1016
- : command === 'categories'
1017
- ? isCategoryResponse(value)
1018
- : command === 'categories-create' || command === 'categories-rename'
1019
- ? isCategoryMutationResponse(value)
1020
- : command === 'line-items-create' || command === 'line-items-rename'
1021
- ? isLineItemMutationResponse(value)
1022
- : command === 'transactions'
1023
- ? isTransactionsResponse(value)
1024
- : command === 'rules-list'
1025
- ? isNotificationRuleListResponse(value)
1026
- : command === 'rules-get' || command === 'rules-set'
1027
- ? isNotificationRuleResponse(value)
1028
- : command === 'rules-delete'
1029
- ? isNotificationRuleDeleteResponse(value)
1030
- : command === 'rules-scan-contract'
1031
- ? isRenewalExtractionResponse(value)
1032
- : command === 'receipts-extract'
1033
- ? isReceiptExtractResponse(value)
1034
- : command === 'receipts-get'
1035
- ? isReceiptLookupResponse(value)
1036
- : command === 'receipts-attach'
1037
- ? isReceiptMutationResponse(value)
1038
- : command === 'receipts-remove'
1039
- ? isReceiptDeleteResponse(value)
1040
- : command === 'assign'
1041
- ? isAssignmentResponse(value)
1042
- : command === 'ask-partner'
1043
- ? isPartnerResponse(value)
1044
- : command === 'goals-list'
1045
- ? isGoalsResponse(value)
1046
- : command === 'goals-delete'
1047
- ? isGoalDeleteResponse(value)
1048
- : isGoalMutationResponse(value);
1115
+ : command === 'portfolio'
1116
+ ? isPortfolioResponse(value)
1117
+ : command === 'budget' || command === 'budget-update'
1118
+ ? isBudgetResponse(value)
1119
+ : command === 'budget-status'
1120
+ ? isBudgetActivityStatusResponse(value)
1121
+ : command === 'budget-move'
1122
+ ? isBudgetMovementResponse(value)
1123
+ : command === 'categories'
1124
+ ? isCategoryResponse(value)
1125
+ : command === 'categories-create' || command === 'categories-rename'
1126
+ ? isCategoryMutationResponse(value)
1127
+ : command === 'line-items-create' || command === 'line-items-rename'
1128
+ ? isLineItemMutationResponse(value)
1129
+ : command === 'transactions'
1130
+ ? isTransactionsResponse(value)
1131
+ : command === 'rules-list'
1132
+ ? isNotificationRuleListResponse(value)
1133
+ : command === 'rules-get' || command === 'rules-set'
1134
+ ? isNotificationRuleResponse(value)
1135
+ : command === 'rules-delete'
1136
+ ? isNotificationRuleDeleteResponse(value)
1137
+ : command === 'rules-scan-contract'
1138
+ ? isRenewalExtractionResponse(value)
1139
+ : command === 'receipts-extract'
1140
+ ? isReceiptExtractResponse(value)
1141
+ : command === 'receipts-get'
1142
+ ? isReceiptLookupResponse(value)
1143
+ : command === 'receipts-attach'
1144
+ ? isReceiptMutationResponse(value)
1145
+ : command === 'receipts-remove'
1146
+ ? isReceiptDeleteResponse(value)
1147
+ : command === 'assign'
1148
+ ? isAssignmentResponse(value)
1149
+ : command === 'ask-partner'
1150
+ ? isPartnerResponse(value)
1151
+ : command === 'goals-list'
1152
+ ? isGoalsResponse(value)
1153
+ : command === 'goals-preview'
1154
+ ? isGoalPreviewResponse(value)
1155
+ : command === 'goals-delete'
1156
+ ? isGoalDeleteResponse(value)
1157
+ : isGoalMutationResponse(value);
1049
1158
  if (!valid) {
1050
1159
  const label = command === 'assign' ? 'assignment' : command;
1051
1160
  throw new ApiError(`Invalid ${label} response from the Agent API`);
package/dist/errors.js CHANGED
@@ -17,8 +17,10 @@ export class ConfigError extends CliError {
17
17
  }
18
18
  export class ApiError extends CliError {
19
19
  status;
20
- constructor(message, status) {
20
+ code;
21
+ constructor(message, status, code) {
21
22
  super(message, 1);
22
23
  this.status = status;
24
+ this.code = code;
23
25
  }
24
26
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@slothmoney/agent-cli",
3
- "version": "0.18.1",
3
+ "version": "0.20.0",
4
4
  "description": "Command-line access to the Sloth Money Agent API.",
5
5
  "type": "module",
6
6
  "bin": {