@slothmoney/agent-cli 0.18.0 → 0.19.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 +20 -0
- package/README.md +22 -15
- package/dist/args.js +28 -13
- package/dist/cli.js +44 -60
- package/dist/contracts.js +70 -77
- package/dist/errors.js +3 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.19.0 - 2026-08-26
|
|
4
|
+
|
|
5
|
+
- Make every Goal use one explicit personal Goal-funding account and expose
|
|
6
|
+
the calculated `forecastMonthKey`, effective priority, account details, and
|
|
7
|
+
`forecastBasis` in Goal results.
|
|
8
|
+
- Make `goals create` preview against Sloth's active scenario on the server;
|
|
9
|
+
preview performs no writes and `--apply` remains the write boundary.
|
|
10
|
+
- Add `--account-ref` and `--priority` to Goal creation, allow account
|
|
11
|
+
reassignment during Goal updates, and rename account eligibility to
|
|
12
|
+
`isGoalFundingAccount` / `--goal-funding-account`.
|
|
13
|
+
- Surface corrective API error codes without adding affordability judgments or
|
|
14
|
+
desired-date comparisons to the CLI.
|
|
15
|
+
|
|
16
|
+
## 0.18.1 - 2026-08-25
|
|
17
|
+
|
|
18
|
+
- Require the deployed historical-activity response for `budget status` and
|
|
19
|
+
remove the temporary validator for the retired current-only response.
|
|
20
|
+
- Report every PATH-resolved `sloth-agent` installation during release
|
|
21
|
+
preflight, so Homebrew and Node-version-manager copies cannot silently drift.
|
|
22
|
+
|
|
3
23
|
## 0.18.0 - 2026-08-25
|
|
4
24
|
|
|
5
25
|
- Add optional `--period YYYY-MM` to `budget status` for current and historical
|
package/README.md
CHANGED
|
@@ -584,7 +584,7 @@ sloth-agent accounts
|
|
|
584
584
|
The command is read-only and cache-only: it does not refresh linked banks or
|
|
585
585
|
change account data. Each result contains an opaque `accountRef`, personal or
|
|
586
586
|
joint ownership, connected or manual source, native balance/currency when
|
|
587
|
-
known, `lastBalanceUpdatedAt`, `connectionState`, and `
|
|
587
|
+
known, `lastBalanceUpdatedAt`, `connectionState`, and `isGoalFundingAccount`.
|
|
588
588
|
Missing values are JSON
|
|
589
589
|
`null`; currencies are never converted or combined. Partner personal accounts
|
|
590
590
|
are excluded, while enabled shared joint accounts follow Sloth's existing
|
|
@@ -604,9 +604,9 @@ Copy the value from `sloth-agent accounts`. Account references are the public
|
|
|
604
604
|
account identifier for transaction filtering.
|
|
605
605
|
|
|
606
606
|
Account changes are previews unless `--apply` is present. Connected accounts
|
|
607
|
-
support only
|
|
607
|
+
support only Goal-funding eligibility. Manual current accounts support their
|
|
608
608
|
institution, name, currency, and ownership. Manual balance accounts also
|
|
609
|
-
support balance, Savings/Investments type, and
|
|
609
|
+
support balance, Savings/Investments type, and Goal-funding eligibility.
|
|
610
610
|
Partner-owned shared accounts return an explanatory error.
|
|
611
611
|
|
|
612
612
|
```bash
|
|
@@ -618,11 +618,11 @@ sloth-agent accounts update \
|
|
|
618
618
|
--ownership individual \
|
|
619
619
|
--balance-amount 12500.75 \
|
|
620
620
|
--account-type investments \
|
|
621
|
-
--goal-
|
|
621
|
+
--goal-funding-account false
|
|
622
622
|
|
|
623
623
|
sloth-agent accounts update \
|
|
624
624
|
--account-ref sloth_account_v1_... \
|
|
625
|
-
--goal-
|
|
625
|
+
--goal-funding-account false \
|
|
626
626
|
--apply
|
|
627
627
|
```
|
|
628
628
|
|
|
@@ -664,16 +664,24 @@ Goal writes are previews unless `--apply` is present:
|
|
|
664
664
|
sloth-agent goals create \
|
|
665
665
|
--name "Emergency fund" \
|
|
666
666
|
--target-amount 12000 \
|
|
667
|
-
--type keep
|
|
667
|
+
--type keep \
|
|
668
|
+
--account-ref sloth_account_v1_...
|
|
668
669
|
|
|
669
670
|
sloth-agent goals create \
|
|
670
671
|
--name "Wedding" \
|
|
671
672
|
--target-amount 22000 \
|
|
672
673
|
--target-month 2027-06 \
|
|
673
674
|
--type spend \
|
|
675
|
+
--account-ref sloth_account_v1_... \
|
|
674
676
|
--apply
|
|
675
677
|
```
|
|
676
678
|
|
|
679
|
+
Without `--apply`, Goal creation authenticates and asks Sloth to calculate the
|
|
680
|
+
Goal without writing it. Preview and apply use the same active-scenario planner
|
|
681
|
+
and return `forecastMonthKey`, the effective priority, the funding account, and
|
|
682
|
+
`forecastBasis`. A null forecast includes the final projected month. The CLI
|
|
683
|
+
does not compare the desired and forecast dates or return affordability advice.
|
|
684
|
+
|
|
677
685
|
Every goal is either Keep or Spend. A Keep goal continues reserving its funded
|
|
678
686
|
money. A Spend goal reserves money until you explicitly mark it spent. Goal
|
|
679
687
|
list, create, and update output includes lowercase `goalType` and nullable
|
|
@@ -687,6 +695,7 @@ move it in the priority order:
|
|
|
687
695
|
sloth-agent goals update \
|
|
688
696
|
--goal-id goal-id \
|
|
689
697
|
--target-month 2027-12 \
|
|
698
|
+
--account-ref sloth_account_v1_... \
|
|
690
699
|
--type spend \
|
|
691
700
|
--apply
|
|
692
701
|
|
|
@@ -715,13 +724,10 @@ required recovery action. Restoring clears `spentAt` and returns the goal to
|
|
|
715
724
|
allocation at its saved priority. Deleting a goal also removes its forecast
|
|
716
725
|
assignments and drift history.
|
|
717
726
|
|
|
718
|
-
Goal sharing remains app-managed.
|
|
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
|
|
727
|
+
Goal sharing remains app-managed. Goal list output includes a
|
|
721
728
|
one-based `priority`; `1` is highest. Moving one goal automatically shifts the
|
|
722
|
-
goals between its old and new positions.
|
|
723
|
-
|
|
724
|
-
derived state and refresh when the owner next opens the Forecast screen.
|
|
729
|
+
goals between its old and new positions. Sloth recalculates the active-scenario
|
|
730
|
+
roadmap before every applied Goal mutation and returns the updated forecast.
|
|
725
731
|
|
|
726
732
|
Read uncategorised contributions to the joint budget:
|
|
727
733
|
|
|
@@ -805,9 +811,10 @@ npm run release:preflight
|
|
|
805
811
|
`npm run test:package` packs the exact npm artifact, installs it into a clean
|
|
806
812
|
temporary project, and runs the installed binary.
|
|
807
813
|
|
|
808
|
-
`npm run release:preflight` reports the local, packed,
|
|
809
|
-
|
|
810
|
-
fails when a parent help page stops advertising one of its
|
|
814
|
+
`npm run release:preflight` reports the local, packed, npm-registry, and every
|
|
815
|
+
PATH-resolved globally installed version, then exercises every parser command's
|
|
816
|
+
nested help. It fails when a parent help page stops advertising one of its
|
|
817
|
+
child commands.
|
|
811
818
|
|
|
812
819
|
## Releasing
|
|
813
820
|
|
package/dist/args.js
CHANGED
|
@@ -389,7 +389,7 @@ function parseAccounts(args, baseUrl) {
|
|
|
389
389
|
'--ownership',
|
|
390
390
|
'--balance-amount',
|
|
391
391
|
'--account-type',
|
|
392
|
-
'--goal-
|
|
392
|
+
'--goal-funding-account',
|
|
393
393
|
]));
|
|
394
394
|
const institutionName = values.get('--institution-name');
|
|
395
395
|
const accountName = values.get('--account-name');
|
|
@@ -397,7 +397,7 @@ function parseAccounts(args, baseUrl) {
|
|
|
397
397
|
const ownershipValue = values.get('--ownership');
|
|
398
398
|
const balanceValue = values.get('--balance-amount');
|
|
399
399
|
const accountTypeValue = values.get('--account-type');
|
|
400
|
-
const sourceValue = values.get('--goal-
|
|
400
|
+
const sourceValue = values.get('--goal-funding-account');
|
|
401
401
|
if (currencyValue !== undefined && !/^[A-Za-z]{3}$/.test(currencyValue)) {
|
|
402
402
|
throw new UsageError('--currency must be a three-letter currency code');
|
|
403
403
|
}
|
|
@@ -412,7 +412,7 @@ function parseAccounts(args, baseUrl) {
|
|
|
412
412
|
throw new UsageError('--account-type must be savings or investments');
|
|
413
413
|
}
|
|
414
414
|
if (sourceValue !== undefined && sourceValue !== 'true' && sourceValue !== 'false') {
|
|
415
|
-
throw new UsageError('--goal-
|
|
415
|
+
throw new UsageError('--goal-funding-account must be true or false');
|
|
416
416
|
}
|
|
417
417
|
const update = {
|
|
418
418
|
...(institutionName === undefined
|
|
@@ -429,7 +429,7 @@ function parseAccounts(args, baseUrl) {
|
|
|
429
429
|
...(accountTypeValue === undefined
|
|
430
430
|
? {}
|
|
431
431
|
: { accountType: accountTypeValue }),
|
|
432
|
-
...(sourceValue === undefined ? {} : {
|
|
432
|
+
...(sourceValue === undefined ? {} : { isGoalFundingAccount: sourceValue === 'true' }),
|
|
433
433
|
};
|
|
434
434
|
if (Object.keys(update).length === 0) {
|
|
435
435
|
throw new UsageError('accounts update requires at least one field to update');
|
|
@@ -652,6 +652,8 @@ function parseGoals(args, baseUrl) {
|
|
|
652
652
|
let targetAmount;
|
|
653
653
|
let targetMonthKey;
|
|
654
654
|
let goalType;
|
|
655
|
+
let fundingAccountRef;
|
|
656
|
+
let priority;
|
|
655
657
|
let apply = false;
|
|
656
658
|
for (let index = 0; index < args.length; index += 1) {
|
|
657
659
|
const argument = args[index];
|
|
@@ -667,7 +669,9 @@ function parseGoals(args, baseUrl) {
|
|
|
667
669
|
if (option !== '--name'
|
|
668
670
|
&& option !== '--target-amount'
|
|
669
671
|
&& option !== '--target-month'
|
|
670
|
-
&& option !== '--type'
|
|
672
|
+
&& option !== '--type'
|
|
673
|
+
&& option !== '--account-ref'
|
|
674
|
+
&& option !== '--priority') {
|
|
671
675
|
throw new UsageError(`Unknown goals create option: ${argument}`);
|
|
672
676
|
}
|
|
673
677
|
const value = requireNonEmpty(inlineValue ?? readOptionValue(args, index, option), option);
|
|
@@ -682,9 +686,15 @@ function parseGoals(args, baseUrl) {
|
|
|
682
686
|
else if (option === '--target-month') {
|
|
683
687
|
targetMonthKey = setOnce(targetMonthKey, parseGoalMonthKey(value, option), option);
|
|
684
688
|
}
|
|
685
|
-
else {
|
|
689
|
+
else if (option === '--type') {
|
|
686
690
|
goalType = setOnce(goalType, parseGoalType(value), option);
|
|
687
691
|
}
|
|
692
|
+
else if (option === '--account-ref') {
|
|
693
|
+
fundingAccountRef = setOnce(fundingAccountRef, parseAccountRef(value), option);
|
|
694
|
+
}
|
|
695
|
+
else {
|
|
696
|
+
priority = setOnce(priority, parseGoalPriority(value), option);
|
|
697
|
+
}
|
|
688
698
|
}
|
|
689
699
|
if (!name)
|
|
690
700
|
throw new UsageError('goals create requires --name <name>');
|
|
@@ -694,12 +704,17 @@ function parseGoals(args, baseUrl) {
|
|
|
694
704
|
if (goalType === undefined) {
|
|
695
705
|
throw new UsageError('goals create requires --type <keep|spend>');
|
|
696
706
|
}
|
|
707
|
+
if (fundingAccountRef === undefined) {
|
|
708
|
+
throw new UsageError('goals create requires --account-ref <accountRef>');
|
|
709
|
+
}
|
|
697
710
|
return withBaseUrl({
|
|
698
711
|
command: 'goals-create',
|
|
699
712
|
name,
|
|
700
713
|
targetAmount,
|
|
701
714
|
...(targetMonthKey === undefined ? {} : { targetMonthKey }),
|
|
702
715
|
goalType,
|
|
716
|
+
fundingAccountRef,
|
|
717
|
+
...(priority === undefined ? {} : { priority }),
|
|
703
718
|
apply,
|
|
704
719
|
}, baseUrl);
|
|
705
720
|
}
|
|
@@ -709,6 +724,7 @@ function parseGoals(args, baseUrl) {
|
|
|
709
724
|
let targetAmount;
|
|
710
725
|
let targetMonthKey;
|
|
711
726
|
let goalType;
|
|
727
|
+
let fundingAccountRef;
|
|
712
728
|
let priority;
|
|
713
729
|
let apply = false;
|
|
714
730
|
for (let index = 0; index < args.length; index += 1) {
|
|
@@ -734,6 +750,7 @@ function parseGoals(args, baseUrl) {
|
|
|
734
750
|
&& option !== '--target-amount'
|
|
735
751
|
&& option !== '--target-month'
|
|
736
752
|
&& option !== '--type'
|
|
753
|
+
&& option !== '--account-ref'
|
|
737
754
|
&& option !== '--priority') {
|
|
738
755
|
throw new UsageError(`Unknown goals update option: ${argument}`);
|
|
739
756
|
}
|
|
@@ -758,6 +775,9 @@ function parseGoals(args, baseUrl) {
|
|
|
758
775
|
else if (option === '--priority') {
|
|
759
776
|
priority = setOnce(priority, parseGoalPriority(value), option);
|
|
760
777
|
}
|
|
778
|
+
else if (option === '--account-ref') {
|
|
779
|
+
fundingAccountRef = setOnce(fundingAccountRef, parseAccountRef(value), option);
|
|
780
|
+
}
|
|
761
781
|
else {
|
|
762
782
|
goalType = setOnce(goalType, parseGoalType(value), option);
|
|
763
783
|
}
|
|
@@ -768,16 +788,10 @@ function parseGoals(args, baseUrl) {
|
|
|
768
788
|
&& targetAmount === undefined
|
|
769
789
|
&& targetMonthKey === undefined
|
|
770
790
|
&& goalType === undefined
|
|
791
|
+
&& fundingAccountRef === undefined
|
|
771
792
|
&& priority === undefined) {
|
|
772
793
|
throw new UsageError('goals update requires at least one field to update');
|
|
773
794
|
}
|
|
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
795
|
return withBaseUrl({
|
|
782
796
|
command: 'goals-update',
|
|
783
797
|
goalId,
|
|
@@ -785,6 +799,7 @@ function parseGoals(args, baseUrl) {
|
|
|
785
799
|
...(targetAmount === undefined ? {} : { targetAmount }),
|
|
786
800
|
...(targetMonthKey === undefined ? {} : { targetMonthKey }),
|
|
787
801
|
...(goalType === undefined ? {} : { goalType }),
|
|
802
|
+
...(fundingAccountRef === undefined ? {} : { fundingAccountRef }),
|
|
788
803
|
...(priority === undefined ? {} : { priority }),
|
|
789
804
|
apply,
|
|
790
805
|
}, baseUrl);
|
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.
|
|
9
|
+
export const CLI_VERSION = '0.19.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 = [
|
|
@@ -61,7 +61,8 @@ export function usageText() {
|
|
|
61
61
|
' sloth-agent receipts remove --transaction-ref REF --revision N [--apply]',
|
|
62
62
|
' sloth-agent goals [list] [--base-url URL]',
|
|
63
63
|
' sloth-agent goals create --name NAME --target-amount AMOUNT',
|
|
64
|
-
' --type keep|spend [--target-month YYYY-MM]
|
|
64
|
+
' --type keep|spend --account-ref REF [--target-month YYYY-MM]',
|
|
65
|
+
' [--priority POSITION] [--apply] [--base-url URL]',
|
|
65
66
|
' sloth-agent goals update --goal-id ID [fields] [--apply] [--base-url URL]',
|
|
66
67
|
' sloth-agent goals mark-spent --goal-id ID [--apply] [--base-url URL]',
|
|
67
68
|
' sloth-agent goals restore --goal-id ID [--apply] [--base-url URL]',
|
|
@@ -354,7 +355,7 @@ export function accountsHelpText() {
|
|
|
354
355
|
' accounts[].ownership personal or joint',
|
|
355
356
|
' accounts[].balanceAmount and currency in the native currency when known',
|
|
356
357
|
' accounts[].connectionState and lastBalanceUpdatedAt for freshness',
|
|
357
|
-
' accounts[].
|
|
358
|
+
' accounts[].isGoalFundingAccount whether Goals may use the account',
|
|
358
359
|
].join('\n');
|
|
359
360
|
}
|
|
360
361
|
export function accountsUpdateHelpText() {
|
|
@@ -376,13 +377,13 @@ export function accountsUpdateHelpText() {
|
|
|
376
377
|
' --ownership individual|joint Manual account ownership.',
|
|
377
378
|
' --balance-amount AMOUNT Balance-only account balance.',
|
|
378
379
|
' --account-type savings|investments Balance-only account type.',
|
|
379
|
-
' --goal-
|
|
380
|
+
' --goal-funding-account true|false Whether Goals may use this account.',
|
|
380
381
|
'',
|
|
381
382
|
'Write behavior:',
|
|
382
383
|
' Without --apply, returns a JSON preview without credentials or a network request.',
|
|
383
384
|
' With --apply, requires agent:write on a write-enabled token and updates saved Sloth metadata.',
|
|
384
|
-
' Connected accounts support only --goal-
|
|
385
|
-
' Manual current accounts cannot change type, balance, or
|
|
385
|
+
' Connected accounts support only --goal-funding-account.',
|
|
386
|
+
' Manual current accounts cannot change type, balance, or Goal-funding membership.',
|
|
386
387
|
' Partner-owned shared accounts cannot be changed.',
|
|
387
388
|
' Unknown, disconnected, or inaccessible references return Account not found.',
|
|
388
389
|
...API_ORIGIN_HELP_LINES,
|
|
@@ -720,7 +721,7 @@ export function goalsHelpText() {
|
|
|
720
721
|
return [
|
|
721
722
|
'Sloth Agent CLI — goals',
|
|
722
723
|
'',
|
|
723
|
-
'List, create, update, mark spent, restore, or delete
|
|
724
|
+
'List, preview, create, update, mark spent, restore, or delete account-funded Goals.',
|
|
724
725
|
'',
|
|
725
726
|
'Commands:',
|
|
726
727
|
' sloth-agent goals list List goals; "sloth-agent goals" is equivalent.',
|
|
@@ -754,9 +755,9 @@ export function goalsListHelpText() {
|
|
|
754
755
|
' This command is read-only.',
|
|
755
756
|
'',
|
|
756
757
|
'Output:',
|
|
757
|
-
' JSON containing currency and goals. Each goal
|
|
758
|
-
'
|
|
759
|
-
'
|
|
758
|
+
' JSON containing currency, forecastBasis, and goals. Each goal includes its',
|
|
759
|
+
' effectivePriority, funding account, desired targetMonthKey, and calculated',
|
|
760
|
+
' forecastMonthKey.',
|
|
760
761
|
].join('\n');
|
|
761
762
|
}
|
|
762
763
|
export function goalsCreateHelpText() {
|
|
@@ -766,30 +767,33 @@ export function goalsCreateHelpText() {
|
|
|
766
767
|
'Preview or create a goal.',
|
|
767
768
|
'',
|
|
768
769
|
'Usage:',
|
|
769
|
-
' sloth-agent goals create --name NAME --target-amount AMOUNT --type keep|spend [options]',
|
|
770
|
+
' sloth-agent goals create --name NAME --target-amount AMOUNT --type keep|spend --account-ref REF [options]',
|
|
770
771
|
'',
|
|
771
772
|
'Options:',
|
|
772
773
|
' --name NAME Required. Goal name, 1 to 200 characters.',
|
|
773
774
|
' --target-amount AMOUNT Required. Positive major-unit amount with up to 2 decimals.',
|
|
774
775
|
' --type keep|spend Required. Keep reserves funded money; Spend is spent later.',
|
|
775
|
-
' --
|
|
776
|
+
' --account-ref REF Required. Personal Goal-funding account from accounts list.',
|
|
777
|
+
' --target-month YYYY-MM Optional. Desired calendar month; it does not change the forecast.',
|
|
778
|
+
' --priority POSITION Optional. One-based priority; defaults to append.',
|
|
776
779
|
' --apply Optional. Create the goal in Sloth Money.',
|
|
777
780
|
' --base-url URL Optional. Override the API origin.',
|
|
778
781
|
' -h, --help Show this help.',
|
|
779
782
|
...API_ORIGIN_HELP_LINES,
|
|
780
783
|
'',
|
|
781
784
|
'Safety:',
|
|
782
|
-
' Without --apply, the command
|
|
785
|
+
' Without --apply, the command authenticates and asks Sloth to calculate the Goal.',
|
|
786
|
+
' The server performs no writes for a preview.',
|
|
783
787
|
' Applying requires a write-enabled token created with Allow changes.',
|
|
784
|
-
' New goals are private to the owner and
|
|
788
|
+
' New goals are private to the owner and append unless --priority is supplied.',
|
|
785
789
|
'',
|
|
786
790
|
'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',
|
|
791
|
+
' sloth-agent goals create --name "Emergency fund" --target-amount 12000 --type keep --account-ref REF',
|
|
792
|
+
' sloth-agent goals create --name "Wedding" --target-amount 22000 --type spend --account-ref REF --target-month 2027-06 --apply',
|
|
789
793
|
'',
|
|
790
794
|
'Output:',
|
|
791
|
-
' Preview
|
|
792
|
-
'
|
|
795
|
+
' Preview and apply return the Goal, currency, calculated forecastMonthKey,',
|
|
796
|
+
' effective priority, funding-account details, and forecastBasis.',
|
|
793
797
|
].join('\n');
|
|
794
798
|
}
|
|
795
799
|
export function goalsUpdateHelpText() {
|
|
@@ -808,6 +812,7 @@ export function goalsUpdateHelpText() {
|
|
|
808
812
|
' --target-month YYYY-MM Optional. Replace the target month.',
|
|
809
813
|
' --clear-target-month Optional. Remove the target month.',
|
|
810
814
|
' --type keep|spend Optional. Change how funded money is treated.',
|
|
815
|
+
' --account-ref REF Optional. Reassign to another personal Goal-funding account.',
|
|
811
816
|
' --priority POSITION Optional. Positive whole-number position; 1 is highest.',
|
|
812
817
|
' --apply Optional. Write the partial update.',
|
|
813
818
|
' --base-url URL Optional. Override the API origin.',
|
|
@@ -816,17 +821,13 @@ export function goalsUpdateHelpText() {
|
|
|
816
821
|
'',
|
|
817
822
|
'Constraints:',
|
|
818
823
|
' Provide at least one field to update.',
|
|
819
|
-
' Priority must be updated on its own.',
|
|
820
824
|
' Priority 1 is highest. The position cannot exceed the current goal count.',
|
|
821
825
|
' Moving a goal shifts the intervening goals automatically.',
|
|
822
|
-
'
|
|
823
|
-
' the Forecast screen.',
|
|
826
|
+
' Sloth recalculates the Goal roadmap before saving the update.',
|
|
824
827
|
' Set and clear target-month options are mutually exclusive.',
|
|
825
828
|
' Restore a spent goal before changing its type.',
|
|
826
|
-
'
|
|
827
|
-
'
|
|
828
|
-
' Sharing remains app-managed. Updates to an already shared goal remain visible',
|
|
829
|
-
' to the connected partner.',
|
|
829
|
+
' Sharing remains app-managed. Updates to a shared Goal remain visible to the',
|
|
830
|
+
' connected partner, but its assigned account remains private to the owner.',
|
|
830
831
|
'',
|
|
831
832
|
'Safety:',
|
|
832
833
|
' Without --apply, the command returns a dry-run preview and does not write.',
|
|
@@ -838,7 +839,8 @@ export function goalsUpdateHelpText() {
|
|
|
838
839
|
'',
|
|
839
840
|
'Output:',
|
|
840
841
|
' Preview mode returns dryRun, method, endpoint, and payload.',
|
|
841
|
-
' Apply mode returns the complete persisted
|
|
842
|
+
' Apply mode returns the complete persisted Goal, calculated forecastMonthKey,',
|
|
843
|
+
' effective priority, funding-account details, currency, and forecastBasis.',
|
|
842
844
|
].join('\n');
|
|
843
845
|
}
|
|
844
846
|
export function goalsMarkSpentHelpText() {
|
|
@@ -1207,23 +1209,6 @@ export function commandHelpText(topic) {
|
|
|
1207
1209
|
function writeJson(write, data) {
|
|
1208
1210
|
write(`${JSON.stringify(data, null, 2)}\n`);
|
|
1209
1211
|
}
|
|
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
1212
|
function redact(value, token) {
|
|
1228
1213
|
return token ? value.split(token).join('[REDACTED]') : value;
|
|
1229
1214
|
}
|
|
@@ -1404,7 +1389,11 @@ async function parseHttpResponse(response, token) {
|
|
|
1404
1389
|
&& typeof data.error === 'string')
|
|
1405
1390
|
? data.error
|
|
1406
1391
|
: `Agent API request failed with status ${response.status}`;
|
|
1407
|
-
|
|
1392
|
+
const code = (data
|
|
1393
|
+
&& typeof data === 'object'
|
|
1394
|
+
&& 'code' in data
|
|
1395
|
+
&& typeof data.code === 'string') ? data.code : undefined;
|
|
1396
|
+
throw new ApiError(redact(message, token), response.status, code);
|
|
1408
1397
|
}
|
|
1409
1398
|
return data;
|
|
1410
1399
|
}
|
|
@@ -1891,7 +1880,7 @@ export async function runCli(argv = process.argv.slice(2), options = {}) {
|
|
|
1891
1880
|
return 0;
|
|
1892
1881
|
}
|
|
1893
1882
|
if (parsed.command === 'goals-create') {
|
|
1894
|
-
const endpoint = `${baseUrl}/api/agent/v1/goals`;
|
|
1883
|
+
const endpoint = `${baseUrl}/api/agent/v1/goals${parsed.apply ? '' : '/preview'}`;
|
|
1895
1884
|
const payload = {
|
|
1896
1885
|
name: parsed.name,
|
|
1897
1886
|
targetAmount: parsed.targetAmount,
|
|
@@ -1899,16 +1888,9 @@ export async function runCli(argv = process.argv.slice(2), options = {}) {
|
|
|
1899
1888
|
? {}
|
|
1900
1889
|
: { targetMonthKey: parsed.targetMonthKey }),
|
|
1901
1890
|
goalType: parsed.goalType,
|
|
1891
|
+
fundingAccountRef: parsed.fundingAccountRef,
|
|
1892
|
+
...(parsed.priority === undefined ? {} : { priority: parsed.priority }),
|
|
1902
1893
|
};
|
|
1903
|
-
if (!parsed.apply) {
|
|
1904
|
-
writeJson(writeStdout, {
|
|
1905
|
-
dryRun: true,
|
|
1906
|
-
endpoint,
|
|
1907
|
-
method: 'POST',
|
|
1908
|
-
payload,
|
|
1909
|
-
});
|
|
1910
|
-
return 0;
|
|
1911
|
-
}
|
|
1912
1894
|
const response = await fetchImplementation(endpoint, {
|
|
1913
1895
|
method: 'POST',
|
|
1914
1896
|
headers: {
|
|
@@ -1918,7 +1900,7 @@ export async function runCli(argv = process.argv.slice(2), options = {}) {
|
|
|
1918
1900
|
body: JSON.stringify(payload),
|
|
1919
1901
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
1920
1902
|
});
|
|
1921
|
-
const data = parseApiResponse('goals-create', await parseHttpResponse(response, token));
|
|
1903
|
+
const data = parseApiResponse(parsed.apply ? 'goals-create' : 'goals-preview', await parseHttpResponse(response, token));
|
|
1922
1904
|
writeJson(writeStdout, data);
|
|
1923
1905
|
return 0;
|
|
1924
1906
|
}
|
|
@@ -1938,6 +1920,9 @@ export async function runCli(argv = process.argv.slice(2), options = {}) {
|
|
|
1938
1920
|
...(parsed.goalType === undefined
|
|
1939
1921
|
? {}
|
|
1940
1922
|
: { goalType: parsed.goalType }),
|
|
1923
|
+
...(parsed.fundingAccountRef === undefined
|
|
1924
|
+
? {}
|
|
1925
|
+
: { fundingAccountRef: parsed.fundingAccountRef }),
|
|
1941
1926
|
...(parsed.priority === undefined
|
|
1942
1927
|
? {}
|
|
1943
1928
|
: { priority: parsed.priority }),
|
|
@@ -1962,9 +1947,7 @@ export async function runCli(argv = process.argv.slice(2), options = {}) {
|
|
|
1962
1947
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
1963
1948
|
});
|
|
1964
1949
|
const data = parseApiResponse(parsed.command, await parseHttpResponse(response, token));
|
|
1965
|
-
writeJson(writeStdout,
|
|
1966
|
-
? data
|
|
1967
|
-
: withUpdatedGoalPriority(data, parsed.priority));
|
|
1950
|
+
writeJson(writeStdout, data);
|
|
1968
1951
|
return 0;
|
|
1969
1952
|
}
|
|
1970
1953
|
if (parsed.command === 'goals-delete') {
|
|
@@ -2059,7 +2042,7 @@ export async function runCli(argv = process.argv.slice(2), options = {}) {
|
|
|
2059
2042
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
2060
2043
|
});
|
|
2061
2044
|
const data = parseApiResponse('goals-list', await parseHttpResponse(response, token));
|
|
2062
|
-
writeJson(writeStdout,
|
|
2045
|
+
writeJson(writeStdout, data);
|
|
2063
2046
|
return 0;
|
|
2064
2047
|
}
|
|
2065
2048
|
if (parsed.command === 'budget' || parsed.command === 'budget-status') {
|
|
@@ -2104,7 +2087,8 @@ export async function runCli(argv = process.argv.slice(2), options = {}) {
|
|
|
2104
2087
|
catch (error) {
|
|
2105
2088
|
const exitCode = error instanceof CliError ? error.exitCode : 1;
|
|
2106
2089
|
const message = error instanceof Error ? error.message : String(error);
|
|
2107
|
-
|
|
2090
|
+
const prefix = error instanceof ApiError && error.code ? `${error.code}: ` : '';
|
|
2091
|
+
writeStderr(`${prefix}${redact(message, token)}\n`);
|
|
2108
2092
|
return exitCode;
|
|
2109
2093
|
}
|
|
2110
2094
|
}
|
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
|
-
&&
|
|
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)
|
|
@@ -696,64 +712,6 @@ function isBudgetResponse(value) {
|
|
|
696
712
|
&& Array.isArray(value.categories)
|
|
697
713
|
&& value.categories.every(isBudgetCategory));
|
|
698
714
|
}
|
|
699
|
-
function isLegacyBudgetStatusResponse(value) {
|
|
700
|
-
if (!isObject(value)
|
|
701
|
-
|| !hasOnlyFields(value, [
|
|
702
|
-
'scope',
|
|
703
|
-
'periodKey',
|
|
704
|
-
'periodStatus',
|
|
705
|
-
'currency',
|
|
706
|
-
'effectiveFromPeriodKey',
|
|
707
|
-
'funding',
|
|
708
|
-
'activity',
|
|
709
|
-
'refresh',
|
|
710
|
-
'categories',
|
|
711
|
-
]))
|
|
712
|
-
return false;
|
|
713
|
-
return ((value.scope === 'personal' || value.scope === 'joint')
|
|
714
|
-
&& typeof value.periodKey === 'string'
|
|
715
|
-
&& /^\d{4}-(0[1-9]|1[0-2])$/.test(value.periodKey)
|
|
716
|
-
&& value.periodStatus === 'current'
|
|
717
|
-
&& isCurrency(value.currency)
|
|
718
|
-
&& typeof value.effectiveFromPeriodKey === 'string'
|
|
719
|
-
&& /^\d{4}-(0[1-9]|1[0-2])$/.test(value.effectiveFromPeriodKey)
|
|
720
|
-
&& isBudgetFunding(value.funding)
|
|
721
|
-
&& isObject(value.activity)
|
|
722
|
-
&& hasOnlyFields(value.activity, [
|
|
723
|
-
'startDate',
|
|
724
|
-
'endDate',
|
|
725
|
-
'transactionCount',
|
|
726
|
-
'uncategorizedSpentPence',
|
|
727
|
-
'unmappedSpentPence',
|
|
728
|
-
])
|
|
729
|
-
&& isIsoDate(value.activity.startDate)
|
|
730
|
-
&& isIsoDate(value.activity.endDate)
|
|
731
|
-
&& value.activity.startDate <= value.activity.endDate
|
|
732
|
-
&& isNonnegativeSafeInteger(value.activity.transactionCount)
|
|
733
|
-
&& isSafeInteger(value.activity.uncategorizedSpentPence)
|
|
734
|
-
&& isSafeInteger(value.activity.unmappedSpentPence)
|
|
735
|
-
&& isRefreshStatus(value.refresh)
|
|
736
|
-
&& Array.isArray(value.categories)
|
|
737
|
-
&& value.categories.every((category) => (isObject(category)
|
|
738
|
-
&& hasOnlyFields(category, [
|
|
739
|
-
'id',
|
|
740
|
-
'name',
|
|
741
|
-
'plannedPence',
|
|
742
|
-
'assignedPence',
|
|
743
|
-
'spentPence',
|
|
744
|
-
'availablePence',
|
|
745
|
-
])
|
|
746
|
-
&& typeof category.id === 'string'
|
|
747
|
-
&& category.id.trim().length > 0
|
|
748
|
-
&& typeof category.name === 'string'
|
|
749
|
-
&& category.name.trim().length > 0
|
|
750
|
-
&& isNonnegativeSafeInteger(category.plannedPence)
|
|
751
|
-
&& isSafeInteger(category.assignedPence)
|
|
752
|
-
&& isSafeInteger(category.spentPence)
|
|
753
|
-
&& isSafeInteger(category.availablePence)
|
|
754
|
-
&& Number.isSafeInteger(category.assignedPence - category.spentPence)
|
|
755
|
-
&& category.availablePence === category.assignedPence - category.spentPence)));
|
|
756
|
-
}
|
|
757
715
|
function isActivityAmounts(value) {
|
|
758
716
|
return isObject(value)
|
|
759
717
|
&& hasOnlyFields(value, ['moneyInPence', 'moneyOutPence', 'netPence'])
|
|
@@ -827,9 +785,6 @@ function isBudgetActivityStatusResponse(value) {
|
|
|
827
785
|
? value.refresh === null
|
|
828
786
|
: isRefreshStatus(value.refresh));
|
|
829
787
|
}
|
|
830
|
-
function isBudgetStatusResponse(value) {
|
|
831
|
-
return isLegacyBudgetStatusResponse(value) || isBudgetActivityStatusResponse(value);
|
|
832
|
-
}
|
|
833
788
|
function isBudgetMovementResponse(value) {
|
|
834
789
|
return (isObject(value)
|
|
835
790
|
&& hasOnlyFields(value, [
|
|
@@ -905,7 +860,7 @@ function isAccount(value) {
|
|
|
905
860
|
'source',
|
|
906
861
|
'lastBalanceUpdatedAt',
|
|
907
862
|
'connectionState',
|
|
908
|
-
'
|
|
863
|
+
'isGoalFundingAccount',
|
|
909
864
|
])
|
|
910
865
|
&& isAccountRef(value.accountRef)
|
|
911
866
|
&& isNullableNonEmptyString(value.accountName)
|
|
@@ -924,7 +879,7 @@ function isAccount(value) {
|
|
|
924
879
|
|| value.connectionState === 'expired'
|
|
925
880
|
|| value.connectionState === 'manual'
|
|
926
881
|
|| value.connectionState === 'unknown')
|
|
927
|
-
&& typeof value.
|
|
882
|
+
&& typeof value.isGoalFundingAccount === 'boolean');
|
|
928
883
|
}
|
|
929
884
|
function isAccountsResponse(value) {
|
|
930
885
|
return (isObject(value)
|
|
@@ -990,19 +945,55 @@ function isInvestmentsResponse(value) {
|
|
|
990
945
|
&& holdings.every(isInvestmentHolding));
|
|
991
946
|
}));
|
|
992
947
|
}
|
|
948
|
+
function isForecastBasis(value) {
|
|
949
|
+
return isObject(value)
|
|
950
|
+
&& hasOnlyFields(value, [
|
|
951
|
+
'calculatedAt',
|
|
952
|
+
'activeScenarioRevision',
|
|
953
|
+
'projectionThroughMonthKey',
|
|
954
|
+
])
|
|
955
|
+
&& isIsoDateTime(value.calculatedAt)
|
|
956
|
+
&& Number.isSafeInteger(value.activeScenarioRevision)
|
|
957
|
+
&& Number(value.activeScenarioRevision) >= 0
|
|
958
|
+
&& isMonthKeyOrNull(value.projectionThroughMonthKey) === true
|
|
959
|
+
&& value.projectionThroughMonthKey !== null;
|
|
960
|
+
}
|
|
961
|
+
function isGoalPreview(value) {
|
|
962
|
+
return isObject(value)
|
|
963
|
+
&& hasOnlyFields(value, [
|
|
964
|
+
'name',
|
|
965
|
+
'targetAmount',
|
|
966
|
+
'targetMonthKey',
|
|
967
|
+
'forecastMonthKey',
|
|
968
|
+
'goalType',
|
|
969
|
+
'effectivePriority',
|
|
970
|
+
'fundingAccountRef',
|
|
971
|
+
'fundingAccountLabel',
|
|
972
|
+
])
|
|
973
|
+
&& isPlannedGoalFields(value);
|
|
974
|
+
}
|
|
993
975
|
function isGoalsResponse(value) {
|
|
994
976
|
return (isObject(value)
|
|
995
|
-
&& hasOnlyFields(value, ['currency', 'goals'])
|
|
977
|
+
&& hasOnlyFields(value, ['currency', 'forecastBasis', 'goals'])
|
|
996
978
|
&& isCurrency(value.currency)
|
|
979
|
+
&& isForecastBasis(value.forecastBasis)
|
|
997
980
|
&& Array.isArray(value.goals)
|
|
998
981
|
&& value.goals.every(isGoal));
|
|
999
982
|
}
|
|
1000
983
|
function isGoalMutationResponse(value) {
|
|
1001
984
|
return (isObject(value)
|
|
1002
|
-
&& hasOnlyFields(value, ['currency', 'goal'])
|
|
985
|
+
&& hasOnlyFields(value, ['currency', 'forecastBasis', 'goal'])
|
|
1003
986
|
&& isCurrency(value.currency)
|
|
987
|
+
&& isForecastBasis(value.forecastBasis)
|
|
1004
988
|
&& isGoal(value.goal));
|
|
1005
989
|
}
|
|
990
|
+
function isGoalPreviewResponse(value) {
|
|
991
|
+
return isObject(value)
|
|
992
|
+
&& hasOnlyFields(value, ['currency', 'forecastBasis', 'goal'])
|
|
993
|
+
&& isCurrency(value.currency)
|
|
994
|
+
&& isForecastBasis(value.forecastBasis)
|
|
995
|
+
&& isGoalPreview(value.goal);
|
|
996
|
+
}
|
|
1006
997
|
function isGoalDeleteResponse(value) {
|
|
1007
998
|
return (isObject(value)
|
|
1008
999
|
&& hasOnlyFields(value, ['deleted', 'deletedGoalId'])
|
|
@@ -1071,7 +1062,7 @@ export function parseApiResponse(command, value) {
|
|
|
1071
1062
|
: command === 'budget' || command === 'budget-update'
|
|
1072
1063
|
? isBudgetResponse(value)
|
|
1073
1064
|
: command === 'budget-status'
|
|
1074
|
-
?
|
|
1065
|
+
? isBudgetActivityStatusResponse(value)
|
|
1075
1066
|
: command === 'budget-move'
|
|
1076
1067
|
? isBudgetMovementResponse(value)
|
|
1077
1068
|
: command === 'categories'
|
|
@@ -1104,9 +1095,11 @@ export function parseApiResponse(command, value) {
|
|
|
1104
1095
|
? isPartnerResponse(value)
|
|
1105
1096
|
: command === 'goals-list'
|
|
1106
1097
|
? isGoalsResponse(value)
|
|
1107
|
-
: command === 'goals-
|
|
1108
|
-
?
|
|
1109
|
-
:
|
|
1098
|
+
: command === 'goals-preview'
|
|
1099
|
+
? isGoalPreviewResponse(value)
|
|
1100
|
+
: command === 'goals-delete'
|
|
1101
|
+
? isGoalDeleteResponse(value)
|
|
1102
|
+
: isGoalMutationResponse(value);
|
|
1110
1103
|
if (!valid) {
|
|
1111
1104
|
const label = command === 'assign' ? 'assignment' : command;
|
|
1112
1105
|
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
|
-
|
|
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
|
}
|