@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 CHANGED
@@ -1,6 +1,13 @@
1
1
  # Changelog
2
2
 
3
- ## Unreleased
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.9.1 -- sloth-agent --help
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
- --target-month 2027-06
418
+ --type keep
417
419
 
418
420
  sloth-agent goals create \
419
- --name "Emergency fund" \
420
- --target-amount 12000 \
421
+ --name "Wedding" \
422
+ --target-amount 22000 \
421
423
  --target-month 2027-06 \
424
+ --type spend \
422
425
  --apply
423
426
  ```
424
427
 
425
- Use the `id` from list or create output to update or delete goals:
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
- --achieved=false \
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-amount` or `--clear-target-month` to
444
- remove an optional value. Marking a goal achieved removes its forecast
445
- assignment. Deleting a goal also removes its forecast assignments and drift
446
- history. Goal sharing remains app-managed. Change an active shared goal's
447
- pot-tracked target amount in the Sloth Budget app, where account balances can
448
- be reallocated across goals in priority order. Goal list output includes a
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
- priority option must be used on its own, and the write persists immediately.
452
- Forecast assignments and shared pot
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 parseExplicitBoolean(value, name) {
78
- if (value !== 'true' && value !== 'false') {
79
- throw new UsageError(`${name} must be true or false`);
78
+ function parseGoalType(value) {
79
+ if (!isGoalType(value)) {
80
+ throw new UsageError('--type must be keep or spend');
80
81
  }
81
- return value === 'true';
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
- ...(targetAmount === undefined ? {} : { targetAmount }),
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 isAchieved;
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 !== '--achieved'
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
- if (targetAmount !== undefined) {
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
- isAchieved = setOnce(isAchieved, parseExplicitBoolean(value, option), option);
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
- && isAchieved === undefined
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
- || isAchieved !== undefined)) {
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
- ...(isAchieved === undefined ? {} : { isAchieved }),
637
+ ...(goalType === undefined ? {} : { goalType }),
635
638
  ...(priority === undefined ? {} : { priority }),
636
639
  apply,
637
640
  }, baseUrl);
638
641
  }
639
- if (subcommand === 'delete') {
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 delete option: ${argument}`);
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('goals delete requires --goal-id <id>');
666
+ if (!goalId) {
667
+ throw new UsageError(`goals ${subcommand} requires --goal-id <id>`);
668
+ }
663
669
  return withBaseUrl({
664
- command: 'goals-delete',
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.9.1';
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 [--target-amount AMOUNT]',
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, isAchieved, and sharedWithPartner.',
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 Optional. Positive major-unit amount with up to 2 decimals.',
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 "Emergency fund" --target-amount 12000 --apply',
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
- ' --achieved=true|false Optional. Mark the goal achieved or active.',
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 for the same field are mutually exclusive.',
680
- ' Marking a goal achieved removes its forecast assignment.',
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
- ...(parsed.targetAmount === undefined
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
- ...(parsed.name === undefined ? {} : { name: parsed.name }),
1192
- ...(parsed.targetAmount === undefined
1193
- ? {}
1194
- : { targetAmount: parsed.targetAmount }),
1195
- ...(parsed.targetMonthKey === undefined
1196
- ? {}
1197
- : { targetMonthKey: parsed.targetMonthKey }),
1198
- ...(parsed.isAchieved === undefined
1199
- ? {}
1200
- : { isAchieved: parsed.isAchieved }),
1201
- ...(parsed.priority === undefined
1202
- ? {}
1203
- : { priority: parsed.priority }),
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('goals-update', await parseHttpResponse(response, token));
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
- 'isAchieved',
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
- && typeof value.isAchieved === 'boolean'
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) {
@@ -0,0 +1,4 @@
1
+ export const GOAL_TYPES = ['keep', 'spend'];
2
+ export function isGoalType(value) {
3
+ return typeof value === 'string' && GOAL_TYPES.includes(value);
4
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@slothmoney/agent-cli",
3
- "version": "0.9.1",
3
+ "version": "0.10.0",
4
4
  "description": "Command-line access to the Sloth Money Agent API.",
5
5
  "type": "module",
6
6
  "bin": {