@slothmoney/agent-cli 0.9.1 → 0.10.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 +8 -1
- package/README.md +39 -18
- package/dist/args.js +43 -29
- package/dist/cli.js +102 -35
- package/dist/contracts.js +7 -2
- package/dist/goal-metadata.js +4 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## 0.10.0 - 2026-08-15
|
|
4
|
+
|
|
5
|
+
- Require every goal create to specify a positive target amount and Keep or
|
|
6
|
+
Spend type, matching the breaking Agent Goals API v1 contract.
|
|
7
|
+
- Expose `goalType` and nullable `spentAt`, allow active goal type changes, and
|
|
8
|
+
remove obsolete `isAchieved` and target-amount clearing assumptions.
|
|
9
|
+
- Add preview-by-default `goals mark-spent` and `goals restore` actions that
|
|
10
|
+
send the canonical `isSpent` lifecycle update only with `--apply`.
|
|
4
11
|
|
|
5
12
|
## 0.9.1 - 2026-08-15
|
|
6
13
|
|
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ sloth-agent --version
|
|
|
15
15
|
For a one-off pinned run:
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
|
-
npm exec --yes --package=@slothmoney/agent-cli@0.
|
|
18
|
+
npm exec --yes --package=@slothmoney/agent-cli@0.10.0 -- sloth-agent --help
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
## Authenticate
|
|
@@ -112,6 +112,8 @@ sloth-agent transactions --help
|
|
|
112
112
|
sloth-agent assign --help
|
|
113
113
|
sloth-agent goals create --help
|
|
114
114
|
sloth-agent goals update --help
|
|
115
|
+
sloth-agent goals mark-spent --help
|
|
116
|
+
sloth-agent goals restore --help
|
|
115
117
|
sloth-agent ask-partner --help
|
|
116
118
|
```
|
|
117
119
|
|
|
@@ -413,45 +415,64 @@ Goal writes are previews unless `--apply` is present:
|
|
|
413
415
|
sloth-agent goals create \
|
|
414
416
|
--name "Emergency fund" \
|
|
415
417
|
--target-amount 12000 \
|
|
416
|
-
--
|
|
418
|
+
--type keep
|
|
417
419
|
|
|
418
420
|
sloth-agent goals create \
|
|
419
|
-
--name "
|
|
420
|
-
--target-amount
|
|
421
|
+
--name "Wedding" \
|
|
422
|
+
--target-amount 22000 \
|
|
421
423
|
--target-month 2027-06 \
|
|
424
|
+
--type spend \
|
|
422
425
|
--apply
|
|
423
426
|
```
|
|
424
427
|
|
|
425
|
-
|
|
428
|
+
Every goal is either Keep or Spend. A Keep goal continues reserving its funded
|
|
429
|
+
money. A Spend goal reserves money until you explicitly mark it spent. Goal
|
|
430
|
+
list, create, and update output includes lowercase `goalType` and nullable
|
|
431
|
+
`spentAt`; `spentAt` is an ISO timestamp only after a Spend goal is marked
|
|
432
|
+
spent.
|
|
433
|
+
|
|
434
|
+
Use the `id` from list or create output to update a goal, change its type, or
|
|
435
|
+
move it in the priority order:
|
|
426
436
|
|
|
427
437
|
```bash
|
|
428
438
|
sloth-agent goals update \
|
|
429
439
|
--goal-id goal-id \
|
|
430
|
-
--clear-target-amount \
|
|
431
440
|
--target-month 2027-12 \
|
|
432
|
-
--
|
|
441
|
+
--type spend \
|
|
433
442
|
--apply
|
|
434
443
|
|
|
435
444
|
sloth-agent goals update \
|
|
436
445
|
--goal-id house-goal-id \
|
|
437
446
|
--priority 2 \
|
|
438
447
|
--apply
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
Marking spent and restoring are also previews by default:
|
|
451
|
+
|
|
452
|
+
```bash
|
|
453
|
+
sloth-agent goals mark-spent --goal-id goal-id
|
|
454
|
+
sloth-agent goals mark-spent --goal-id goal-id --apply
|
|
455
|
+
|
|
456
|
+
sloth-agent goals restore --goal-id goal-id
|
|
457
|
+
sloth-agent goals restore --goal-id goal-id --apply
|
|
439
458
|
|
|
440
459
|
sloth-agent goals delete --goal-id goal-id --apply
|
|
441
460
|
```
|
|
442
461
|
|
|
443
|
-
Updates are partial. Use `--clear-target-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
462
|
+
Updates are partial. Use `--clear-target-month` to remove the optional month.
|
|
463
|
+
A Keep goal cannot be marked spent. A spent goal must be restored before its
|
|
464
|
+
type can change; the API returns these lifecycle conflicts without hiding the
|
|
465
|
+
required recovery action. Restoring clears `spentAt` and returns the goal to
|
|
466
|
+
allocation at its saved priority. Deleting a goal also removes its forecast
|
|
467
|
+
assignments and drift history.
|
|
468
|
+
|
|
469
|
+
Goal sharing remains app-managed. Change an active shared goal's pot-tracked
|
|
470
|
+
target amount in the Sloth Budget app, where account balances can be
|
|
471
|
+
reallocated across goals in priority order. Goal list output includes a
|
|
449
472
|
one-based `priority`; `1` is highest. Moving one goal automatically shifts the
|
|
450
|
-
goals between its old and new positions. The
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
progress are browser-owned derived state and refresh when the owner next opens
|
|
454
|
-
the Forecast screen.
|
|
473
|
+
goals between its old and new positions. The priority option must be used on
|
|
474
|
+
its own. Forecast assignments and shared pot progress are browser-owned
|
|
475
|
+
derived state and refresh when the owner next opens the Forecast screen.
|
|
455
476
|
|
|
456
477
|
Read uncategorised contributions to the joint budget:
|
|
457
478
|
|
package/dist/args.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { UsageError } from './errors.js';
|
|
2
2
|
import { CATEGORY_TYPES, ICON_KEYS, } from './category-metadata.js';
|
|
3
|
+
import { isGoalType, } from './goal-metadata.js';
|
|
3
4
|
const PRODUCTION_BASE_URL = 'https://budget.slothmoney.app';
|
|
4
5
|
const LOCAL_HOSTS = new Set(['localhost', '127.0.0.1', '[::1]']);
|
|
5
6
|
function readOptionValue(args, index, name) {
|
|
@@ -74,11 +75,11 @@ function parseGoalPriority(value) {
|
|
|
74
75
|
}
|
|
75
76
|
return priority;
|
|
76
77
|
}
|
|
77
|
-
function
|
|
78
|
-
if (value
|
|
79
|
-
throw new UsageError(
|
|
78
|
+
function parseGoalType(value) {
|
|
79
|
+
if (!isGoalType(value)) {
|
|
80
|
+
throw new UsageError('--type must be keep or spend');
|
|
80
81
|
}
|
|
81
|
-
return value
|
|
82
|
+
return value;
|
|
82
83
|
}
|
|
83
84
|
function parseGoalName(value) {
|
|
84
85
|
const name = value.trim();
|
|
@@ -500,6 +501,7 @@ function parseGoals(args, baseUrl) {
|
|
|
500
501
|
let name;
|
|
501
502
|
let targetAmount;
|
|
502
503
|
let targetMonthKey;
|
|
504
|
+
let goalType;
|
|
503
505
|
let apply = false;
|
|
504
506
|
for (let index = 0; index < args.length; index += 1) {
|
|
505
507
|
const argument = args[index];
|
|
@@ -514,7 +516,8 @@ function parseGoals(args, baseUrl) {
|
|
|
514
516
|
: [argument, undefined];
|
|
515
517
|
if (option !== '--name'
|
|
516
518
|
&& option !== '--target-amount'
|
|
517
|
-
&& option !== '--target-month'
|
|
519
|
+
&& option !== '--target-month'
|
|
520
|
+
&& option !== '--type') {
|
|
518
521
|
throw new UsageError(`Unknown goals create option: ${argument}`);
|
|
519
522
|
}
|
|
520
523
|
const value = requireNonEmpty(inlineValue ?? readOptionValue(args, index, option), option);
|
|
@@ -526,17 +529,27 @@ function parseGoals(args, baseUrl) {
|
|
|
526
529
|
else if (option === '--target-amount') {
|
|
527
530
|
targetAmount = setOnce(targetAmount, parseGoalAmount(value, option), option);
|
|
528
531
|
}
|
|
529
|
-
else {
|
|
532
|
+
else if (option === '--target-month') {
|
|
530
533
|
targetMonthKey = setOnce(targetMonthKey, parseGoalMonthKey(value, option), option);
|
|
531
534
|
}
|
|
535
|
+
else {
|
|
536
|
+
goalType = setOnce(goalType, parseGoalType(value), option);
|
|
537
|
+
}
|
|
532
538
|
}
|
|
533
539
|
if (!name)
|
|
534
540
|
throw new UsageError('goals create requires --name <name>');
|
|
541
|
+
if (targetAmount === undefined) {
|
|
542
|
+
throw new UsageError('goals create requires --target-amount <amount>');
|
|
543
|
+
}
|
|
544
|
+
if (goalType === undefined) {
|
|
545
|
+
throw new UsageError('goals create requires --type <keep|spend>');
|
|
546
|
+
}
|
|
535
547
|
return withBaseUrl({
|
|
536
548
|
command: 'goals-create',
|
|
537
549
|
name,
|
|
538
|
-
|
|
550
|
+
targetAmount,
|
|
539
551
|
...(targetMonthKey === undefined ? {} : { targetMonthKey }),
|
|
552
|
+
goalType,
|
|
540
553
|
apply,
|
|
541
554
|
}, baseUrl);
|
|
542
555
|
}
|
|
@@ -545,7 +558,7 @@ function parseGoals(args, baseUrl) {
|
|
|
545
558
|
let name;
|
|
546
559
|
let targetAmount;
|
|
547
560
|
let targetMonthKey;
|
|
548
|
-
let
|
|
561
|
+
let goalType;
|
|
549
562
|
let priority;
|
|
550
563
|
let apply = false;
|
|
551
564
|
for (let index = 0; index < args.length; index += 1) {
|
|
@@ -556,13 +569,6 @@ function parseGoals(args, baseUrl) {
|
|
|
556
569
|
apply = true;
|
|
557
570
|
continue;
|
|
558
571
|
}
|
|
559
|
-
if (argument === '--clear-target-amount') {
|
|
560
|
-
if (targetAmount !== undefined) {
|
|
561
|
-
throw new UsageError('--target-amount and --clear-target-amount are mutually exclusive');
|
|
562
|
-
}
|
|
563
|
-
targetAmount = null;
|
|
564
|
-
continue;
|
|
565
|
-
}
|
|
566
572
|
if (argument === '--clear-target-month') {
|
|
567
573
|
if (targetMonthKey !== undefined) {
|
|
568
574
|
throw new UsageError('--target-month and --clear-target-month are mutually exclusive');
|
|
@@ -577,7 +583,7 @@ function parseGoals(args, baseUrl) {
|
|
|
577
583
|
&& option !== '--name'
|
|
578
584
|
&& option !== '--target-amount'
|
|
579
585
|
&& option !== '--target-month'
|
|
580
|
-
&& option !== '--
|
|
586
|
+
&& option !== '--type'
|
|
581
587
|
&& option !== '--priority') {
|
|
582
588
|
throw new UsageError(`Unknown goals update option: ${argument}`);
|
|
583
589
|
}
|
|
@@ -591,10 +597,7 @@ function parseGoals(args, baseUrl) {
|
|
|
591
597
|
name = setOnce(name, parseGoalName(value), option);
|
|
592
598
|
}
|
|
593
599
|
else if (option === '--target-amount') {
|
|
594
|
-
|
|
595
|
-
throw new UsageError('--target-amount and --clear-target-amount are mutually exclusive');
|
|
596
|
-
}
|
|
597
|
-
targetAmount = parseGoalAmount(value, option);
|
|
600
|
+
targetAmount = setOnce(targetAmount, parseGoalAmount(value, option), option);
|
|
598
601
|
}
|
|
599
602
|
else if (option === '--target-month') {
|
|
600
603
|
if (targetMonthKey !== undefined) {
|
|
@@ -606,7 +609,7 @@ function parseGoals(args, baseUrl) {
|
|
|
606
609
|
priority = setOnce(priority, parseGoalPriority(value), option);
|
|
607
610
|
}
|
|
608
611
|
else {
|
|
609
|
-
|
|
612
|
+
goalType = setOnce(goalType, parseGoalType(value), option);
|
|
610
613
|
}
|
|
611
614
|
}
|
|
612
615
|
if (!goalId)
|
|
@@ -614,7 +617,7 @@ function parseGoals(args, baseUrl) {
|
|
|
614
617
|
if (name === undefined
|
|
615
618
|
&& targetAmount === undefined
|
|
616
619
|
&& targetMonthKey === undefined
|
|
617
|
-
&&
|
|
620
|
+
&& goalType === undefined
|
|
618
621
|
&& priority === undefined) {
|
|
619
622
|
throw new UsageError('goals update requires at least one field to update');
|
|
620
623
|
}
|
|
@@ -622,7 +625,7 @@ function parseGoals(args, baseUrl) {
|
|
|
622
625
|
&& (name !== undefined
|
|
623
626
|
|| targetAmount !== undefined
|
|
624
627
|
|| targetMonthKey !== undefined
|
|
625
|
-
||
|
|
628
|
+
|| goalType !== undefined)) {
|
|
626
629
|
throw new UsageError('--priority must be used on its own');
|
|
627
630
|
}
|
|
628
631
|
return withBaseUrl({
|
|
@@ -631,12 +634,14 @@ function parseGoals(args, baseUrl) {
|
|
|
631
634
|
...(name === undefined ? {} : { name }),
|
|
632
635
|
...(targetAmount === undefined ? {} : { targetAmount }),
|
|
633
636
|
...(targetMonthKey === undefined ? {} : { targetMonthKey }),
|
|
634
|
-
...(
|
|
637
|
+
...(goalType === undefined ? {} : { goalType }),
|
|
635
638
|
...(priority === undefined ? {} : { priority }),
|
|
636
639
|
apply,
|
|
637
640
|
}, baseUrl);
|
|
638
641
|
}
|
|
639
|
-
if (subcommand === '
|
|
642
|
+
if (subcommand === 'mark-spent'
|
|
643
|
+
|| subcommand === 'restore'
|
|
644
|
+
|| subcommand === 'delete') {
|
|
640
645
|
let goalId;
|
|
641
646
|
let apply = false;
|
|
642
647
|
for (let index = 0; index < args.length; index += 1) {
|
|
@@ -651,17 +656,22 @@ function parseGoals(args, baseUrl) {
|
|
|
651
656
|
? argument.split(/=(.*)/s, 2)
|
|
652
657
|
: [argument, undefined];
|
|
653
658
|
if (option !== '--goal-id') {
|
|
654
|
-
throw new UsageError(`Unknown goals
|
|
659
|
+
throw new UsageError(`Unknown goals ${subcommand} option: ${argument}`);
|
|
655
660
|
}
|
|
656
661
|
const value = requireNonEmpty(inlineValue ?? readOptionValue(args, index, option), option);
|
|
657
662
|
if (inlineValue === undefined)
|
|
658
663
|
index += 1;
|
|
659
664
|
goalId = setOnce(goalId, parseGoalId(value), option);
|
|
660
665
|
}
|
|
661
|
-
if (!goalId)
|
|
662
|
-
throw new UsageError(
|
|
666
|
+
if (!goalId) {
|
|
667
|
+
throw new UsageError(`goals ${subcommand} requires --goal-id <id>`);
|
|
668
|
+
}
|
|
663
669
|
return withBaseUrl({
|
|
664
|
-
command: '
|
|
670
|
+
command: subcommand === 'mark-spent'
|
|
671
|
+
? 'goals-mark-spent'
|
|
672
|
+
: subcommand === 'restore'
|
|
673
|
+
? 'goals-restore'
|
|
674
|
+
: 'goals-delete',
|
|
665
675
|
goalId,
|
|
666
676
|
apply,
|
|
667
677
|
}, baseUrl);
|
|
@@ -699,6 +709,10 @@ function helpTopic(argv) {
|
|
|
699
709
|
return 'goals-create';
|
|
700
710
|
if (subcommand === 'update')
|
|
701
711
|
return 'goals-update';
|
|
712
|
+
if (subcommand === 'mark-spent')
|
|
713
|
+
return 'goals-mark-spent';
|
|
714
|
+
if (subcommand === 'restore')
|
|
715
|
+
return 'goals-restore';
|
|
702
716
|
if (subcommand === 'delete')
|
|
703
717
|
return 'goals-delete';
|
|
704
718
|
return 'goals';
|
package/dist/cli.js
CHANGED
|
@@ -4,7 +4,7 @@ import { ICON_KEYS } from './category-metadata.js';
|
|
|
4
4
|
import { parseApiResponse, validateAssignmentPayload, validateBudgetUpdatePayload, } from './contracts.js';
|
|
5
5
|
import { createSystemCredentialStore, secureStorageUnavailableError, } from './credential-store.js';
|
|
6
6
|
import { ApiError, CliError, ConfigError, UsageError, } from './errors.js';
|
|
7
|
-
export const CLI_VERSION = '0.
|
|
7
|
+
export const CLI_VERSION = '0.10.0';
|
|
8
8
|
const REQUEST_TIMEOUT_MS = 60_000;
|
|
9
9
|
const API_ORIGIN_HELP_LINES = [
|
|
10
10
|
'',
|
|
@@ -43,9 +43,11 @@ export function usageText() {
|
|
|
43
43
|
' [--cursor CURSOR] [--base-url URL]',
|
|
44
44
|
' sloth-agent assign --input assignments.json [--apply] [--base-url URL]',
|
|
45
45
|
' sloth-agent goals [list] [--base-url URL]',
|
|
46
|
-
' sloth-agent goals create --name NAME
|
|
47
|
-
' [--target-month YYYY-MM] [--apply] [--base-url URL]',
|
|
46
|
+
' sloth-agent goals create --name NAME --target-amount AMOUNT',
|
|
47
|
+
' --type keep|spend [--target-month YYYY-MM] [--apply] [--base-url URL]',
|
|
48
48
|
' sloth-agent goals update --goal-id ID [fields] [--apply] [--base-url URL]',
|
|
49
|
+
' sloth-agent goals mark-spent --goal-id ID [--apply] [--base-url URL]',
|
|
50
|
+
' sloth-agent goals restore --goal-id ID [--apply] [--base-url URL]',
|
|
49
51
|
' sloth-agent goals delete --goal-id ID [--apply] [--base-url URL]',
|
|
50
52
|
' sloth-agent ask-partner --transaction-ref REF [--base-url URL]',
|
|
51
53
|
'',
|
|
@@ -578,12 +580,14 @@ export function goalsHelpText() {
|
|
|
578
580
|
return [
|
|
579
581
|
'Sloth Agent CLI — goals',
|
|
580
582
|
'',
|
|
581
|
-
'List, create, update, or delete your savings goals.',
|
|
583
|
+
'List, create, update, mark spent, restore, or delete your savings goals.',
|
|
582
584
|
'',
|
|
583
585
|
'Commands:',
|
|
584
586
|
' sloth-agent goals list List goals; "sloth-agent goals" is equivalent.',
|
|
585
587
|
' sloth-agent goals create Preview or create a goal.',
|
|
586
588
|
' sloth-agent goals update Preview or update selected goal fields.',
|
|
589
|
+
' sloth-agent goals mark-spent Preview or mark a Spend goal spent.',
|
|
590
|
+
' sloth-agent goals restore Preview or restore a spent goal.',
|
|
587
591
|
' sloth-agent goals delete Preview or permanently delete a goal.',
|
|
588
592
|
'',
|
|
589
593
|
'Help:',
|
|
@@ -611,7 +615,8 @@ export function goalsListHelpText() {
|
|
|
611
615
|
'',
|
|
612
616
|
'Output:',
|
|
613
617
|
' JSON containing currency and goals. Each goal contains id, name, priority,',
|
|
614
|
-
' targetAmount, targetMonthKey,
|
|
618
|
+
' targetAmount, targetMonthKey, goalType, nullable spentAt, and',
|
|
619
|
+
' sharedWithPartner.',
|
|
615
620
|
].join('\n');
|
|
616
621
|
}
|
|
617
622
|
export function goalsCreateHelpText() {
|
|
@@ -621,11 +626,12 @@ export function goalsCreateHelpText() {
|
|
|
621
626
|
'Preview or create a goal.',
|
|
622
627
|
'',
|
|
623
628
|
'Usage:',
|
|
624
|
-
' sloth-agent goals create --name NAME [options]',
|
|
629
|
+
' sloth-agent goals create --name NAME --target-amount AMOUNT --type keep|spend [options]',
|
|
625
630
|
'',
|
|
626
631
|
'Options:',
|
|
627
632
|
' --name NAME Required. Goal name, 1 to 200 characters.',
|
|
628
|
-
' --target-amount AMOUNT
|
|
633
|
+
' --target-amount AMOUNT Required. Positive major-unit amount with up to 2 decimals.',
|
|
634
|
+
' --type keep|spend Required. Keep reserves funded money; Spend is spent later.',
|
|
629
635
|
' --target-month YYYY-MM Optional. Target calendar month.',
|
|
630
636
|
' --apply Optional. Create the goal in Sloth Money.',
|
|
631
637
|
' --base-url URL Optional. Override the API origin.',
|
|
@@ -638,8 +644,8 @@ export function goalsCreateHelpText() {
|
|
|
638
644
|
' New goals are private to the owner and appended to the existing goal order.',
|
|
639
645
|
'',
|
|
640
646
|
'Example:',
|
|
641
|
-
' sloth-agent goals create --name "Emergency fund" --target-amount 12000',
|
|
642
|
-
' sloth-agent goals create --name "
|
|
647
|
+
' sloth-agent goals create --name "Emergency fund" --target-amount 12000 --type keep',
|
|
648
|
+
' sloth-agent goals create --name "Wedding" --target-amount 22000 --type spend --target-month 2027-06 --apply',
|
|
643
649
|
'',
|
|
644
650
|
'Output:',
|
|
645
651
|
' Preview mode returns dryRun, method, endpoint, and payload.',
|
|
@@ -659,10 +665,9 @@ export function goalsUpdateHelpText() {
|
|
|
659
665
|
' --goal-id ID Required. Goal ID from goals list or create output.',
|
|
660
666
|
' --name NAME Optional. Replacement name, 1 to 200 characters.',
|
|
661
667
|
' --target-amount AMOUNT Optional. Positive amount with up to 2 decimals.',
|
|
662
|
-
' --clear-target-amount Optional. Remove the target amount.',
|
|
663
668
|
' --target-month YYYY-MM Optional. Replace the target month.',
|
|
664
669
|
' --clear-target-month Optional. Remove the target month.',
|
|
665
|
-
' --
|
|
670
|
+
' --type keep|spend Optional. Change how funded money is treated.',
|
|
666
671
|
' --priority POSITION Optional. Positive whole-number position; 1 is highest.',
|
|
667
672
|
' --apply Optional. Write the partial update.',
|
|
668
673
|
' --base-url URL Optional. Override the API origin.',
|
|
@@ -676,9 +681,8 @@ export function goalsUpdateHelpText() {
|
|
|
676
681
|
' Moving a goal shifts the intervening goals automatically.',
|
|
677
682
|
' Forecast assignments and shared progress refresh when the owner next opens',
|
|
678
683
|
' the Forecast screen.',
|
|
679
|
-
' Set and clear options
|
|
680
|
-
'
|
|
681
|
-
' Marking it active again does not restore the previous assignment.',
|
|
684
|
+
' Set and clear target-month options are mutually exclusive.',
|
|
685
|
+
' Restore a spent goal before changing its type.',
|
|
682
686
|
' Change active shared pot target amounts in the Sloth Budget app, where',
|
|
683
687
|
' account balances can be reconciled across goals in priority order.',
|
|
684
688
|
' Sharing remains app-managed. Updates to an already shared goal remain visible',
|
|
@@ -689,6 +693,7 @@ export function goalsUpdateHelpText() {
|
|
|
689
693
|
' Applying requires a write-enabled token created with Allow changes.',
|
|
690
694
|
'',
|
|
691
695
|
'Example:',
|
|
696
|
+
' sloth-agent goals update --goal-id wedding --type spend --apply',
|
|
692
697
|
' sloth-agent goals update --goal-id goal-3 --priority 2 --apply',
|
|
693
698
|
'',
|
|
694
699
|
'Output:',
|
|
@@ -696,6 +701,63 @@ export function goalsUpdateHelpText() {
|
|
|
696
701
|
' Apply mode returns the complete persisted goal and currency.',
|
|
697
702
|
].join('\n');
|
|
698
703
|
}
|
|
704
|
+
export function goalsMarkSpentHelpText() {
|
|
705
|
+
return [
|
|
706
|
+
'Sloth Agent CLI — goals mark-spent',
|
|
707
|
+
'',
|
|
708
|
+
'Preview or mark a Spend goal spent.',
|
|
709
|
+
'',
|
|
710
|
+
'Usage:',
|
|
711
|
+
' sloth-agent goals mark-spent --goal-id ID [--apply] [--base-url URL]',
|
|
712
|
+
'',
|
|
713
|
+
'Options:',
|
|
714
|
+
' --goal-id ID Required. Spend goal ID from goals list or create output.',
|
|
715
|
+
' --apply Optional. Mark the goal spent in Sloth Money.',
|
|
716
|
+
' --base-url URL Optional. Override the API origin.',
|
|
717
|
+
' -h, --help Show this help.',
|
|
718
|
+
...API_ORIGIN_HELP_LINES,
|
|
719
|
+
'',
|
|
720
|
+
'Safety and lifecycle:',
|
|
721
|
+
' Without --apply, the command previews PATCH {"isSpent":true} and does not write.',
|
|
722
|
+
' Keep goals cannot be marked spent. Change an active goal to Spend first.',
|
|
723
|
+
' A spent goal is excluded from future goal allocation until restored.',
|
|
724
|
+
'',
|
|
725
|
+
'Example:',
|
|
726
|
+
' sloth-agent goals mark-spent --goal-id wedding --apply',
|
|
727
|
+
'',
|
|
728
|
+
'Output:',
|
|
729
|
+
' Preview mode returns dryRun, method, endpoint, and payload.',
|
|
730
|
+
' Apply mode returns the complete persisted goal and currency.',
|
|
731
|
+
].join('\n');
|
|
732
|
+
}
|
|
733
|
+
export function goalsRestoreHelpText() {
|
|
734
|
+
return [
|
|
735
|
+
'Sloth Agent CLI — goals restore',
|
|
736
|
+
'',
|
|
737
|
+
'Preview or restore a spent Spend goal.',
|
|
738
|
+
'',
|
|
739
|
+
'Usage:',
|
|
740
|
+
' sloth-agent goals restore --goal-id ID [--apply] [--base-url URL]',
|
|
741
|
+
'',
|
|
742
|
+
'Options:',
|
|
743
|
+
' --goal-id ID Required. Spent goal ID from goals list output.',
|
|
744
|
+
' --apply Optional. Restore the goal in Sloth Money.',
|
|
745
|
+
' --base-url URL Optional. Override the API origin.',
|
|
746
|
+
' -h, --help Show this help.',
|
|
747
|
+
...API_ORIGIN_HELP_LINES,
|
|
748
|
+
'',
|
|
749
|
+
'Safety and lifecycle:',
|
|
750
|
+
' Without --apply, the command previews PATCH {"isSpent":false} and does not write.',
|
|
751
|
+
' Restoring clears spentAt and returns the goal to allocation at its saved priority.',
|
|
752
|
+
'',
|
|
753
|
+
'Example:',
|
|
754
|
+
' sloth-agent goals restore --goal-id wedding --apply',
|
|
755
|
+
'',
|
|
756
|
+
'Output:',
|
|
757
|
+
' Preview mode returns dryRun, method, endpoint, and payload.',
|
|
758
|
+
' Apply mode returns the complete persisted goal and currency.',
|
|
759
|
+
].join('\n');
|
|
760
|
+
}
|
|
699
761
|
export function goalsDeleteHelpText() {
|
|
700
762
|
return [
|
|
701
763
|
'Sloth Agent CLI — goals delete',
|
|
@@ -774,6 +836,8 @@ export function commandHelpText(topic) {
|
|
|
774
836
|
'goals-list': goalsListHelpText,
|
|
775
837
|
'goals-create': goalsCreateHelpText,
|
|
776
838
|
'goals-update': goalsUpdateHelpText,
|
|
839
|
+
'goals-mark-spent': goalsMarkSpentHelpText,
|
|
840
|
+
'goals-restore': goalsRestoreHelpText,
|
|
777
841
|
'goals-delete': goalsDeleteHelpText,
|
|
778
842
|
'ask-partner': askPartnerHelpText,
|
|
779
843
|
};
|
|
@@ -1156,12 +1220,11 @@ export async function runCli(argv = process.argv.slice(2), options = {}) {
|
|
|
1156
1220
|
const endpoint = `${baseUrl}/api/agent/v1/goals`;
|
|
1157
1221
|
const payload = {
|
|
1158
1222
|
name: parsed.name,
|
|
1159
|
-
|
|
1160
|
-
? {}
|
|
1161
|
-
: { targetAmount: parsed.targetAmount }),
|
|
1223
|
+
targetAmount: parsed.targetAmount,
|
|
1162
1224
|
...(parsed.targetMonthKey === undefined
|
|
1163
1225
|
? {}
|
|
1164
1226
|
: { targetMonthKey: parsed.targetMonthKey }),
|
|
1227
|
+
goalType: parsed.goalType,
|
|
1165
1228
|
};
|
|
1166
1229
|
if (!parsed.apply) {
|
|
1167
1230
|
writeJson(writeStdout, {
|
|
@@ -1185,23 +1248,27 @@ export async function runCli(argv = process.argv.slice(2), options = {}) {
|
|
|
1185
1248
|
writeJson(writeStdout, data);
|
|
1186
1249
|
return 0;
|
|
1187
1250
|
}
|
|
1188
|
-
if (parsed.command === 'goals-update'
|
|
1251
|
+
if (parsed.command === 'goals-update'
|
|
1252
|
+
|| parsed.command === 'goals-mark-spent'
|
|
1253
|
+
|| parsed.command === 'goals-restore') {
|
|
1189
1254
|
const endpoint = `${baseUrl}/api/agent/v1/goals/${encodeURIComponent(parsed.goalId)}`;
|
|
1190
|
-
const payload =
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1255
|
+
const payload = parsed.command === 'goals-update'
|
|
1256
|
+
? {
|
|
1257
|
+
...(parsed.name === undefined ? {} : { name: parsed.name }),
|
|
1258
|
+
...(parsed.targetAmount === undefined
|
|
1259
|
+
? {}
|
|
1260
|
+
: { targetAmount: parsed.targetAmount }),
|
|
1261
|
+
...(parsed.targetMonthKey === undefined
|
|
1262
|
+
? {}
|
|
1263
|
+
: { targetMonthKey: parsed.targetMonthKey }),
|
|
1264
|
+
...(parsed.goalType === undefined
|
|
1265
|
+
? {}
|
|
1266
|
+
: { goalType: parsed.goalType }),
|
|
1267
|
+
...(parsed.priority === undefined
|
|
1268
|
+
? {}
|
|
1269
|
+
: { priority: parsed.priority }),
|
|
1270
|
+
}
|
|
1271
|
+
: { isSpent: parsed.command === 'goals-mark-spent' };
|
|
1205
1272
|
if (!parsed.apply) {
|
|
1206
1273
|
writeJson(writeStdout, {
|
|
1207
1274
|
dryRun: true,
|
|
@@ -1220,8 +1287,8 @@ export async function runCli(argv = process.argv.slice(2), options = {}) {
|
|
|
1220
1287
|
body: JSON.stringify(payload),
|
|
1221
1288
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
1222
1289
|
});
|
|
1223
|
-
const data = parseApiResponse(
|
|
1224
|
-
writeJson(writeStdout, parsed.priority === undefined
|
|
1290
|
+
const data = parseApiResponse(parsed.command, await parseHttpResponse(response, token));
|
|
1291
|
+
writeJson(writeStdout, parsed.command !== 'goals-update' || parsed.priority === undefined
|
|
1225
1292
|
? data
|
|
1226
1293
|
: withUpdatedGoalPriority(data, parsed.priority));
|
|
1227
1294
|
return 0;
|
package/dist/contracts.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ApiError, UsageError, } from './errors.js';
|
|
2
2
|
import { CATEGORY_TYPES, ICON_KEYS } from './category-metadata.js';
|
|
3
|
+
import { isGoalType } from './goal-metadata.js';
|
|
3
4
|
function isObject(value) {
|
|
4
5
|
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
5
6
|
}
|
|
@@ -297,7 +298,8 @@ function isGoal(value) {
|
|
|
297
298
|
'name',
|
|
298
299
|
'targetAmount',
|
|
299
300
|
'targetMonthKey',
|
|
300
|
-
'
|
|
301
|
+
'goalType',
|
|
302
|
+
'spentAt',
|
|
301
303
|
'sharedWithPartner',
|
|
302
304
|
])
|
|
303
305
|
&& typeof value.id === 'string'
|
|
@@ -309,7 +311,10 @@ function isGoal(value) {
|
|
|
309
311
|
&& (value.targetMonthKey === null
|
|
310
312
|
|| (typeof value.targetMonthKey === 'string'
|
|
311
313
|
&& /^\d{4}-(0[1-9]|1[0-2])$/.test(value.targetMonthKey)))
|
|
312
|
-
&&
|
|
314
|
+
&& isGoalType(value.goalType)
|
|
315
|
+
&& (value.goalType === 'spend'
|
|
316
|
+
? value.spentAt === null || isIsoDateTime(value.spentAt)
|
|
317
|
+
: value.spentAt === null)
|
|
313
318
|
&& typeof value.sharedWithPartner === 'boolean');
|
|
314
319
|
}
|
|
315
320
|
function isCurrency(value) {
|