@slothmoney/agent-cli 0.9.1 → 0.11.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,20 @@
1
1
  # Changelog
2
2
 
3
- ## Unreleased
3
+ ## 0.11.0 - 2026-08-16
4
+
5
+ - Add preview-by-default `budget move` for atomically moving current assigned
6
+ money between categories or To Assign without changing planned budgets.
7
+ - Accept human-readable currency amounts at the CLI boundary, send integer
8
+ pence to the Agent API, and validate the returned affected balances.
9
+
10
+ ## 0.10.0 - 2026-08-15
11
+
12
+ - Require every goal create to specify a positive target amount and Keep or
13
+ Spend type, matching the breaking Agent Goals API v1 contract.
14
+ - Expose `goalType` and nullable `spentAt`, allow active goal type changes, and
15
+ remove obsolete `isAchieved` and target-amount clearing assumptions.
16
+ - Add preview-by-default `goals mark-spent` and `goals restore` actions that
17
+ send the canonical `isSpent` lifecycle update only with `--apply`.
4
18
 
5
19
  ## 0.9.1 - 2026-08-15
6
20
 
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Sloth Agent CLI
2
2
 
3
- Use your own agent to inspect accounts, investments, and budgets, manage goals, update planned amounts, and categorise transactions through the
3
+ Use your own agent to inspect accounts, investments, and budgets, manage goals, move assigned budget money, update planned amounts, and categorise transactions through the
4
4
  [Sloth Money Agent API](https://slothmoney.app/developers/).
5
5
 
6
6
  ## Install
@@ -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.11.0 -- sloth-agent --help
19
19
  ```
20
20
 
21
21
  ## Authenticate
@@ -27,7 +27,7 @@ where the CLI runs.
27
27
  New tokens are view-only. That is enough for `auth status`, `accounts`, `investments`,
28
28
  `budget`, `categories`, `transactions`, and `goals` list. Enable **Allow changes** when
29
29
  creating the token only if the CLI must apply assignments, manage categories
30
- or line items, update planned budgets, manage accounts, ask a partner for an explanation, or manage goals. Token
30
+ or line items, move assigned budget money, update planned budgets, manage accounts, ask a partner for an explanation, or manage goals. Token
31
31
  permissions cannot be changed later - revoke and reissue the token instead.
32
32
 
33
33
  ### Local computer
@@ -105,6 +105,7 @@ sloth-agent auth login --help
105
105
  sloth-agent accounts --help
106
106
  sloth-agent budget --help
107
107
  sloth-agent budget update --help
108
+ sloth-agent budget move --help
108
109
  sloth-agent categories --help
109
110
  sloth-agent categories create --help
110
111
  sloth-agent line-items create --help
@@ -112,6 +113,8 @@ sloth-agent transactions --help
112
113
  sloth-agent assign --help
113
114
  sloth-agent goals create --help
114
115
  sloth-agent goals update --help
116
+ sloth-agent goals mark-spent --help
117
+ sloth-agent goals restore --help
115
118
  sloth-agent ask-partner --help
116
119
  ```
117
120
 
@@ -283,6 +286,37 @@ period and everything after it. Earlier and historical periods remain unchanged.
283
286
  Without `--apply`, the CLI validates the file locally and does not load a token
284
287
  or contact Sloth Money. Applying requires a write-enabled token.
285
288
 
289
+ Move current assigned money between two categories, or use the reserved
290
+ `to-assign` ID to move money to or from To Assign:
291
+
292
+ ```bash
293
+ sloth-agent budget move \
294
+ --scope personal \
295
+ --from-category-id activities \
296
+ --to-category-id groceries \
297
+ --amount 52.95
298
+
299
+ sloth-agent budget move \
300
+ --scope personal \
301
+ --from-category-id activities \
302
+ --to-category-id groceries \
303
+ --amount 52.95 \
304
+ --apply
305
+ ```
306
+
307
+ Copy category IDs from `sloth-agent budget` output. `--amount` is expressed in
308
+ the budget currency and accepts up to two decimal places; the CLI converts the
309
+ decimal digits exactly and sends a positive safe-integer number of pence to the
310
+ API. Without `--apply`, the command validates and prints the exact request
311
+ without loading credentials or contacting Sloth Money.
312
+
313
+ Applying subtracts and adds the amount atomically, records the movement in the
314
+ budget history, and returns the affected assigned balances. It does not change
315
+ planned line-item amounts or future budget plans. Like the UI, it permits a
316
+ source category or To Assign to become negative; an automated workflow should
317
+ choose donors from its own available-balance policy. Historical periods cannot
318
+ be changed, and applying requires a write-enabled token.
319
+
286
320
  Create or rename a custom category. Writes are previews until `--apply` is
287
321
  present:
288
322
 
@@ -413,45 +447,64 @@ Goal writes are previews unless `--apply` is present:
413
447
  sloth-agent goals create \
414
448
  --name "Emergency fund" \
415
449
  --target-amount 12000 \
416
- --target-month 2027-06
450
+ --type keep
417
451
 
418
452
  sloth-agent goals create \
419
- --name "Emergency fund" \
420
- --target-amount 12000 \
453
+ --name "Wedding" \
454
+ --target-amount 22000 \
421
455
  --target-month 2027-06 \
456
+ --type spend \
422
457
  --apply
423
458
  ```
424
459
 
425
- Use the `id` from list or create output to update or delete goals:
460
+ Every goal is either Keep or Spend. A Keep goal continues reserving its funded
461
+ money. A Spend goal reserves money until you explicitly mark it spent. Goal
462
+ list, create, and update output includes lowercase `goalType` and nullable
463
+ `spentAt`; `spentAt` is an ISO timestamp only after a Spend goal is marked
464
+ spent.
465
+
466
+ Use the `id` from list or create output to update a goal, change its type, or
467
+ move it in the priority order:
426
468
 
427
469
  ```bash
428
470
  sloth-agent goals update \
429
471
  --goal-id goal-id \
430
- --clear-target-amount \
431
472
  --target-month 2027-12 \
432
- --achieved=false \
473
+ --type spend \
433
474
  --apply
434
475
 
435
476
  sloth-agent goals update \
436
477
  --goal-id house-goal-id \
437
478
  --priority 2 \
438
479
  --apply
480
+ ```
481
+
482
+ Marking spent and restoring are also previews by default:
483
+
484
+ ```bash
485
+ sloth-agent goals mark-spent --goal-id goal-id
486
+ sloth-agent goals mark-spent --goal-id goal-id --apply
487
+
488
+ sloth-agent goals restore --goal-id goal-id
489
+ sloth-agent goals restore --goal-id goal-id --apply
439
490
 
440
491
  sloth-agent goals delete --goal-id goal-id --apply
441
492
  ```
442
493
 
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
494
+ Updates are partial. Use `--clear-target-month` to remove the optional month.
495
+ A Keep goal cannot be marked spent. A spent goal must be restored before its
496
+ type can change; the API returns these lifecycle conflicts without hiding the
497
+ required recovery action. Restoring clears `spentAt` and returns the goal to
498
+ allocation at its saved priority. Deleting a goal also removes its forecast
499
+ assignments and drift history.
500
+
501
+ Goal sharing remains app-managed. Change an active shared goal's pot-tracked
502
+ target amount in the Sloth Budget app, where account balances can be
503
+ reallocated across goals in priority order. Goal list output includes a
449
504
  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.
505
+ goals between its old and new positions. The priority option must be used on
506
+ its own. Forecast assignments and shared pot progress are browser-owned
507
+ derived state and refresh when the owner next opens the Forecast screen.
455
508
 
456
509
  Read uncategorised contributions to the joint budget:
457
510
 
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) {
@@ -48,16 +49,33 @@ function requireNonEmpty(value, name) {
48
49
  throw new UsageError(`${name} requires a value`);
49
50
  return value;
50
51
  }
51
- function parseGoalAmount(value, name) {
52
+ function validatePositiveDecimalAmount(value, name) {
52
53
  if (!/^\d+(?:\.\d{1,2})?$/.test(value)) {
53
54
  throw new UsageError(`${name} must be a positive amount with at most two decimal places`);
54
55
  }
56
+ const digits = value.replace('.', '');
57
+ if (!/[1-9]/.test(digits)) {
58
+ throw new UsageError(`${name} must be a positive amount with at most two decimal places`);
59
+ }
60
+ }
61
+ function parsePositiveDecimalAmount(value, name) {
62
+ validatePositiveDecimalAmount(value, name);
55
63
  const amount = Number(value);
56
64
  if (!Number.isFinite(amount) || amount <= 0) {
57
65
  throw new UsageError(`${name} must be a positive amount with at most two decimal places`);
58
66
  }
59
67
  return amount;
60
68
  }
69
+ function parsePositiveAmountPence(value, name) {
70
+ validatePositiveDecimalAmount(value, name);
71
+ const [wholePounds, fractionalPounds = ''] = value.split('.');
72
+ const amountPence = BigInt(wholePounds) * 100n
73
+ + BigInt(fractionalPounds.padEnd(2, '0'));
74
+ if (amountPence > BigInt(Number.MAX_SAFE_INTEGER)) {
75
+ throw new UsageError(`${name} must be a positive amount with at most two decimal places`);
76
+ }
77
+ return Number(amountPence);
78
+ }
61
79
  function parseGoalMonthKey(value, name) {
62
80
  if (!/^\d{4}-(0[1-9]|1[0-2])$/.test(value)) {
63
81
  throw new UsageError(`${name} must be a valid YYYY-MM month`);
@@ -74,11 +92,11 @@ function parseGoalPriority(value) {
74
92
  }
75
93
  return priority;
76
94
  }
77
- function parseExplicitBoolean(value, name) {
78
- if (value !== 'true' && value !== 'false') {
79
- throw new UsageError(`${name} must be true or false`);
95
+ function parseGoalType(value) {
96
+ if (!isGoalType(value)) {
97
+ throw new UsageError('--type must be keep or spend');
80
98
  }
81
- return value === 'true';
99
+ return value;
82
100
  }
83
101
  function parseGoalName(value) {
84
102
  const name = value.trim();
@@ -108,7 +126,7 @@ function parseResourceId(value, option) {
108
126
  const codePoint = character.codePointAt(0);
109
127
  return codePoint !== undefined && (codePoint < 32 || codePoint === 127);
110
128
  })) {
111
- const resource = option === '--category-id' ? 'category' : 'line-item';
129
+ const resource = option === '--line-item-id' ? 'line-item' : 'category';
112
130
  throw new UsageError(`${option} must be a valid ${resource} document ID`);
113
131
  }
114
132
  return id;
@@ -354,13 +372,18 @@ function parseInvestments(args, baseUrl) {
354
372
  return withBaseUrl({ command: 'investments', ...(accountRef ? { accountRef } : {}) }, baseUrl);
355
373
  }
356
374
  function parseBudget(args, baseUrl) {
357
- const update = args[0] === 'update';
358
- if (update)
359
- args.shift();
360
- const { values, apply } = parseNamedOptions(args, update ? 'budget update' : 'budget', new Set(update ? ['--scope', '--period', '--input'] : ['--scope', '--period']));
361
- if (!update && apply)
375
+ const subcommand = args[0] === 'update' || args[0] === 'move' ? args.shift() : undefined;
376
+ const update = subcommand === 'update';
377
+ const move = subcommand === 'move';
378
+ const commandLabel = update ? 'budget update' : move ? 'budget move' : 'budget';
379
+ const { values, apply } = parseNamedOptions(args, commandLabel, new Set(update
380
+ ? ['--scope', '--period', '--input']
381
+ : move
382
+ ? ['--scope', '--period', '--from-category-id', '--to-category-id', '--amount']
383
+ : ['--scope', '--period']));
384
+ if (!update && !move && apply)
362
385
  throw new UsageError('Unknown budget option: --apply');
363
- const scope = requiredOption(values, '--scope', update ? 'budget update' : 'budget');
386
+ const scope = requiredOption(values, '--scope', commandLabel);
364
387
  if (scope !== 'personal' && scope !== 'joint') {
365
388
  throw new UsageError('--scope must be personal or joint');
366
389
  }
@@ -369,8 +392,23 @@ function parseBudget(args, baseUrl) {
369
392
  scope: scope,
370
393
  ...(period === undefined ? {} : { periodKey: parseGoalMonthKey(period, '--period') }),
371
394
  };
372
- if (!update)
395
+ if (!update && !move)
373
396
  return withBaseUrl({ command: 'budget', ...common }, baseUrl);
397
+ if (move) {
398
+ const fromCategoryId = parseResourceId(requiredOption(values, '--from-category-id', commandLabel), '--from-category-id');
399
+ const toCategoryId = parseResourceId(requiredOption(values, '--to-category-id', commandLabel), '--to-category-id');
400
+ if (fromCategoryId === toCategoryId) {
401
+ throw new UsageError('--from-category-id and --to-category-id must differ');
402
+ }
403
+ return withBaseUrl({
404
+ command: 'budget-move',
405
+ ...common,
406
+ fromCategoryId,
407
+ toCategoryId,
408
+ amountPence: parsePositiveAmountPence(requiredOption(values, '--amount', commandLabel), '--amount'),
409
+ apply,
410
+ }, baseUrl);
411
+ }
374
412
  return withBaseUrl({
375
413
  command: 'budget-update',
376
414
  ...common,
@@ -500,6 +538,7 @@ function parseGoals(args, baseUrl) {
500
538
  let name;
501
539
  let targetAmount;
502
540
  let targetMonthKey;
541
+ let goalType;
503
542
  let apply = false;
504
543
  for (let index = 0; index < args.length; index += 1) {
505
544
  const argument = args[index];
@@ -514,7 +553,8 @@ function parseGoals(args, baseUrl) {
514
553
  : [argument, undefined];
515
554
  if (option !== '--name'
516
555
  && option !== '--target-amount'
517
- && option !== '--target-month') {
556
+ && option !== '--target-month'
557
+ && option !== '--type') {
518
558
  throw new UsageError(`Unknown goals create option: ${argument}`);
519
559
  }
520
560
  const value = requireNonEmpty(inlineValue ?? readOptionValue(args, index, option), option);
@@ -524,19 +564,29 @@ function parseGoals(args, baseUrl) {
524
564
  name = setOnce(name, parseGoalName(value), option);
525
565
  }
526
566
  else if (option === '--target-amount') {
527
- targetAmount = setOnce(targetAmount, parseGoalAmount(value, option), option);
567
+ targetAmount = setOnce(targetAmount, parsePositiveDecimalAmount(value, option), option);
528
568
  }
529
- else {
569
+ else if (option === '--target-month') {
530
570
  targetMonthKey = setOnce(targetMonthKey, parseGoalMonthKey(value, option), option);
531
571
  }
572
+ else {
573
+ goalType = setOnce(goalType, parseGoalType(value), option);
574
+ }
532
575
  }
533
576
  if (!name)
534
577
  throw new UsageError('goals create requires --name <name>');
578
+ if (targetAmount === undefined) {
579
+ throw new UsageError('goals create requires --target-amount <amount>');
580
+ }
581
+ if (goalType === undefined) {
582
+ throw new UsageError('goals create requires --type <keep|spend>');
583
+ }
535
584
  return withBaseUrl({
536
585
  command: 'goals-create',
537
586
  name,
538
- ...(targetAmount === undefined ? {} : { targetAmount }),
587
+ targetAmount,
539
588
  ...(targetMonthKey === undefined ? {} : { targetMonthKey }),
589
+ goalType,
540
590
  apply,
541
591
  }, baseUrl);
542
592
  }
@@ -545,7 +595,7 @@ function parseGoals(args, baseUrl) {
545
595
  let name;
546
596
  let targetAmount;
547
597
  let targetMonthKey;
548
- let isAchieved;
598
+ let goalType;
549
599
  let priority;
550
600
  let apply = false;
551
601
  for (let index = 0; index < args.length; index += 1) {
@@ -556,13 +606,6 @@ function parseGoals(args, baseUrl) {
556
606
  apply = true;
557
607
  continue;
558
608
  }
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
609
  if (argument === '--clear-target-month') {
567
610
  if (targetMonthKey !== undefined) {
568
611
  throw new UsageError('--target-month and --clear-target-month are mutually exclusive');
@@ -577,7 +620,7 @@ function parseGoals(args, baseUrl) {
577
620
  && option !== '--name'
578
621
  && option !== '--target-amount'
579
622
  && option !== '--target-month'
580
- && option !== '--achieved'
623
+ && option !== '--type'
581
624
  && option !== '--priority') {
582
625
  throw new UsageError(`Unknown goals update option: ${argument}`);
583
626
  }
@@ -591,10 +634,7 @@ function parseGoals(args, baseUrl) {
591
634
  name = setOnce(name, parseGoalName(value), option);
592
635
  }
593
636
  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);
637
+ targetAmount = setOnce(targetAmount, parsePositiveDecimalAmount(value, option), option);
598
638
  }
599
639
  else if (option === '--target-month') {
600
640
  if (targetMonthKey !== undefined) {
@@ -606,7 +646,7 @@ function parseGoals(args, baseUrl) {
606
646
  priority = setOnce(priority, parseGoalPriority(value), option);
607
647
  }
608
648
  else {
609
- isAchieved = setOnce(isAchieved, parseExplicitBoolean(value, option), option);
649
+ goalType = setOnce(goalType, parseGoalType(value), option);
610
650
  }
611
651
  }
612
652
  if (!goalId)
@@ -614,7 +654,7 @@ function parseGoals(args, baseUrl) {
614
654
  if (name === undefined
615
655
  && targetAmount === undefined
616
656
  && targetMonthKey === undefined
617
- && isAchieved === undefined
657
+ && goalType === undefined
618
658
  && priority === undefined) {
619
659
  throw new UsageError('goals update requires at least one field to update');
620
660
  }
@@ -622,7 +662,7 @@ function parseGoals(args, baseUrl) {
622
662
  && (name !== undefined
623
663
  || targetAmount !== undefined
624
664
  || targetMonthKey !== undefined
625
- || isAchieved !== undefined)) {
665
+ || goalType !== undefined)) {
626
666
  throw new UsageError('--priority must be used on its own');
627
667
  }
628
668
  return withBaseUrl({
@@ -631,12 +671,14 @@ function parseGoals(args, baseUrl) {
631
671
  ...(name === undefined ? {} : { name }),
632
672
  ...(targetAmount === undefined ? {} : { targetAmount }),
633
673
  ...(targetMonthKey === undefined ? {} : { targetMonthKey }),
634
- ...(isAchieved === undefined ? {} : { isAchieved }),
674
+ ...(goalType === undefined ? {} : { goalType }),
635
675
  ...(priority === undefined ? {} : { priority }),
636
676
  apply,
637
677
  }, baseUrl);
638
678
  }
639
- if (subcommand === 'delete') {
679
+ if (subcommand === 'mark-spent'
680
+ || subcommand === 'restore'
681
+ || subcommand === 'delete') {
640
682
  let goalId;
641
683
  let apply = false;
642
684
  for (let index = 0; index < args.length; index += 1) {
@@ -651,17 +693,22 @@ function parseGoals(args, baseUrl) {
651
693
  ? argument.split(/=(.*)/s, 2)
652
694
  : [argument, undefined];
653
695
  if (option !== '--goal-id') {
654
- throw new UsageError(`Unknown goals delete option: ${argument}`);
696
+ throw new UsageError(`Unknown goals ${subcommand} option: ${argument}`);
655
697
  }
656
698
  const value = requireNonEmpty(inlineValue ?? readOptionValue(args, index, option), option);
657
699
  if (inlineValue === undefined)
658
700
  index += 1;
659
701
  goalId = setOnce(goalId, parseGoalId(value), option);
660
702
  }
661
- if (!goalId)
662
- throw new UsageError('goals delete requires --goal-id <id>');
703
+ if (!goalId) {
704
+ throw new UsageError(`goals ${subcommand} requires --goal-id <id>`);
705
+ }
663
706
  return withBaseUrl({
664
- command: 'goals-delete',
707
+ command: subcommand === 'mark-spent'
708
+ ? 'goals-mark-spent'
709
+ : subcommand === 'restore'
710
+ ? 'goals-restore'
711
+ : 'goals-delete',
665
712
  goalId,
666
713
  apply,
667
714
  }, baseUrl);
@@ -699,6 +746,10 @@ function helpTopic(argv) {
699
746
  return 'goals-create';
700
747
  if (subcommand === 'update')
701
748
  return 'goals-update';
749
+ if (subcommand === 'mark-spent')
750
+ return 'goals-mark-spent';
751
+ if (subcommand === 'restore')
752
+ return 'goals-restore';
702
753
  if (subcommand === 'delete')
703
754
  return 'goals-delete';
704
755
  return 'goals';
@@ -718,7 +769,11 @@ function helpTopic(argv) {
718
769
  return undefined;
719
770
  }
720
771
  if (command === 'budget') {
721
- return subcommand === 'update' ? 'budget-update' : 'budget';
772
+ if (subcommand === 'update')
773
+ return 'budget-update';
774
+ if (subcommand === 'move')
775
+ return 'budget-move';
776
+ return 'budget';
722
777
  }
723
778
  if (command === 'accounts'
724
779
  || command === 'transactions'
package/dist/cli.js CHANGED
@@ -1,10 +1,10 @@
1
1
  import fs from 'node:fs';
2
2
  import { parseArgs, resolveBaseUrl, } from './args.js';
3
3
  import { ICON_KEYS } from './category-metadata.js';
4
- import { parseApiResponse, validateAssignmentPayload, validateBudgetUpdatePayload, } from './contracts.js';
4
+ import { parseApiResponse, validateAssignmentPayload, validateBudgetMovementResponse, 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.11.0';
8
8
  const REQUEST_TIMEOUT_MS = 60_000;
9
9
  const API_ORIGIN_HELP_LINES = [
10
10
  '',
@@ -32,6 +32,8 @@ export function usageText() {
32
32
  ' sloth-agent budget --scope personal|joint [--period YYYY-MM] [--base-url URL]',
33
33
  ' sloth-agent budget update --scope personal|joint [--period YYYY-MM]',
34
34
  ' --input budget.json [--apply] [--base-url URL]',
35
+ ' sloth-agent budget move --scope personal|joint [--period YYYY-MM]',
36
+ ' --from-category-id ID --to-category-id ID --amount AMOUNT [--apply]',
35
37
  ' sloth-agent categories [list] [--base-url URL]',
36
38
  ' sloth-agent categories create --name NAME --icon-key KEY --type TYPE [--apply]',
37
39
  ' sloth-agent categories rename --category-id ID --name NAME [--apply]',
@@ -43,9 +45,11 @@ export function usageText() {
43
45
  ' [--cursor CURSOR] [--base-url URL]',
44
46
  ' sloth-agent assign --input assignments.json [--apply] [--base-url URL]',
45
47
  ' 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]',
48
+ ' sloth-agent goals create --name NAME --target-amount AMOUNT',
49
+ ' --type keep|spend [--target-month YYYY-MM] [--apply] [--base-url URL]',
48
50
  ' sloth-agent goals update --goal-id ID [fields] [--apply] [--base-url URL]',
51
+ ' sloth-agent goals mark-spent --goal-id ID [--apply] [--base-url URL]',
52
+ ' sloth-agent goals restore --goal-id ID [--apply] [--base-url URL]',
49
53
  ' sloth-agent goals delete --goal-id ID [--apply] [--base-url URL]',
50
54
  ' sloth-agent ask-partner --transaction-ref REF [--base-url URL]',
51
55
  '',
@@ -455,6 +459,47 @@ export function budgetUpdateHelpText() {
455
459
  ' Apply mode returns the complete persisted budget response.',
456
460
  ].join('\n');
457
461
  }
462
+ export function budgetMoveHelpText() {
463
+ return [
464
+ 'Sloth Agent CLI — budget move',
465
+ '',
466
+ 'Preview or move assigned money between categories or To Assign.',
467
+ '',
468
+ 'Usage:',
469
+ ' sloth-agent budget move --scope personal|joint [--period YYYY-MM] --from-category-id ID --to-category-id ID --amount AMOUNT [--apply] [--base-url URL]',
470
+ '',
471
+ 'Required inputs:',
472
+ ' --scope personal|joint Budget ownership scope.',
473
+ ' --from-category-id ID Source category ID, or to-assign.',
474
+ ' --to-category-id ID Destination category ID, or to-assign.',
475
+ ' --amount AMOUNT Positive amount in the budget currency, with up to two decimals.',
476
+ ' The integer-pence value must be at most 9,007,199,254,740,991.',
477
+ '',
478
+ 'Optional inputs:',
479
+ ' --period YYYY-MM Defaults to the current Sloth budget period.',
480
+ ' --apply Send the movement. Without it, only validate and preview.',
481
+ ' --base-url URL Override the API origin.',
482
+ ' -h, --help Show this help.',
483
+ ...API_ORIGIN_HELP_LINES,
484
+ '',
485
+ 'Write behavior:',
486
+ ' Without --apply, returns JSON locally without loading credentials or contacting Sloth Money.',
487
+ ' With --apply, atomically subtracts from the source and adds to the destination.',
488
+ ' Use the reserved ID to-assign to move money to or from To Assign.',
489
+ ' The move changes current assigned balances and records budget movement history.',
490
+ ' The source category or To Assign may become negative, so choose the source deliberately.',
491
+ ' It does not change planned amounts or future budget plans.',
492
+ ' Historical periods cannot be changed. Applying requires agent:write.',
493
+ '',
494
+ 'Output:',
495
+ ' Preview mode returns dryRun, endpoint, method, and the amountPence payload.',
496
+ ' Apply mode returns the period, currency, movement, To Assign balance, and affected category balances.',
497
+ '',
498
+ 'Examples:',
499
+ ' sloth-agent budget move --scope personal --from-category-id activities --to-category-id groceries --amount 52.95',
500
+ ' sloth-agent budget move --scope personal --from-category-id activities --to-category-id groceries --amount 52.95 --apply',
501
+ ].join('\n');
502
+ }
458
503
  export function transactionsHelpText() {
459
504
  return [
460
505
  'Sloth Agent CLI — transactions',
@@ -578,12 +623,14 @@ export function goalsHelpText() {
578
623
  return [
579
624
  'Sloth Agent CLI — goals',
580
625
  '',
581
- 'List, create, update, or delete your savings goals.',
626
+ 'List, create, update, mark spent, restore, or delete your savings goals.',
582
627
  '',
583
628
  'Commands:',
584
629
  ' sloth-agent goals list List goals; "sloth-agent goals" is equivalent.',
585
630
  ' sloth-agent goals create Preview or create a goal.',
586
631
  ' sloth-agent goals update Preview or update selected goal fields.',
632
+ ' sloth-agent goals mark-spent Preview or mark a Spend goal spent.',
633
+ ' sloth-agent goals restore Preview or restore a spent goal.',
587
634
  ' sloth-agent goals delete Preview or permanently delete a goal.',
588
635
  '',
589
636
  'Help:',
@@ -611,7 +658,8 @@ export function goalsListHelpText() {
611
658
  '',
612
659
  'Output:',
613
660
  ' JSON containing currency and goals. Each goal contains id, name, priority,',
614
- ' targetAmount, targetMonthKey, isAchieved, and sharedWithPartner.',
661
+ ' targetAmount, targetMonthKey, goalType, nullable spentAt, and',
662
+ ' sharedWithPartner.',
615
663
  ].join('\n');
616
664
  }
617
665
  export function goalsCreateHelpText() {
@@ -621,11 +669,12 @@ export function goalsCreateHelpText() {
621
669
  'Preview or create a goal.',
622
670
  '',
623
671
  'Usage:',
624
- ' sloth-agent goals create --name NAME [options]',
672
+ ' sloth-agent goals create --name NAME --target-amount AMOUNT --type keep|spend [options]',
625
673
  '',
626
674
  'Options:',
627
675
  ' --name NAME Required. Goal name, 1 to 200 characters.',
628
- ' --target-amount AMOUNT Optional. Positive major-unit amount with up to 2 decimals.',
676
+ ' --target-amount AMOUNT Required. Positive major-unit amount with up to 2 decimals.',
677
+ ' --type keep|spend Required. Keep reserves funded money; Spend is spent later.',
629
678
  ' --target-month YYYY-MM Optional. Target calendar month.',
630
679
  ' --apply Optional. Create the goal in Sloth Money.',
631
680
  ' --base-url URL Optional. Override the API origin.',
@@ -638,8 +687,8 @@ export function goalsCreateHelpText() {
638
687
  ' New goals are private to the owner and appended to the existing goal order.',
639
688
  '',
640
689
  'Example:',
641
- ' sloth-agent goals create --name "Emergency fund" --target-amount 12000',
642
- ' sloth-agent goals create --name "Emergency fund" --target-amount 12000 --apply',
690
+ ' sloth-agent goals create --name "Emergency fund" --target-amount 12000 --type keep',
691
+ ' sloth-agent goals create --name "Wedding" --target-amount 22000 --type spend --target-month 2027-06 --apply',
643
692
  '',
644
693
  'Output:',
645
694
  ' Preview mode returns dryRun, method, endpoint, and payload.',
@@ -659,10 +708,9 @@ export function goalsUpdateHelpText() {
659
708
  ' --goal-id ID Required. Goal ID from goals list or create output.',
660
709
  ' --name NAME Optional. Replacement name, 1 to 200 characters.',
661
710
  ' --target-amount AMOUNT Optional. Positive amount with up to 2 decimals.',
662
- ' --clear-target-amount Optional. Remove the target amount.',
663
711
  ' --target-month YYYY-MM Optional. Replace the target month.',
664
712
  ' --clear-target-month Optional. Remove the target month.',
665
- ' --achieved=true|false Optional. Mark the goal achieved or active.',
713
+ ' --type keep|spend Optional. Change how funded money is treated.',
666
714
  ' --priority POSITION Optional. Positive whole-number position; 1 is highest.',
667
715
  ' --apply Optional. Write the partial update.',
668
716
  ' --base-url URL Optional. Override the API origin.',
@@ -676,9 +724,8 @@ export function goalsUpdateHelpText() {
676
724
  ' Moving a goal shifts the intervening goals automatically.',
677
725
  ' Forecast assignments and shared progress refresh when the owner next opens',
678
726
  ' 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.',
727
+ ' Set and clear target-month options are mutually exclusive.',
728
+ ' Restore a spent goal before changing its type.',
682
729
  ' Change active shared pot target amounts in the Sloth Budget app, where',
683
730
  ' account balances can be reconciled across goals in priority order.',
684
731
  ' Sharing remains app-managed. Updates to an already shared goal remain visible',
@@ -689,6 +736,7 @@ export function goalsUpdateHelpText() {
689
736
  ' Applying requires a write-enabled token created with Allow changes.',
690
737
  '',
691
738
  'Example:',
739
+ ' sloth-agent goals update --goal-id wedding --type spend --apply',
692
740
  ' sloth-agent goals update --goal-id goal-3 --priority 2 --apply',
693
741
  '',
694
742
  'Output:',
@@ -696,6 +744,63 @@ export function goalsUpdateHelpText() {
696
744
  ' Apply mode returns the complete persisted goal and currency.',
697
745
  ].join('\n');
698
746
  }
747
+ export function goalsMarkSpentHelpText() {
748
+ return [
749
+ 'Sloth Agent CLI — goals mark-spent',
750
+ '',
751
+ 'Preview or mark a Spend goal spent.',
752
+ '',
753
+ 'Usage:',
754
+ ' sloth-agent goals mark-spent --goal-id ID [--apply] [--base-url URL]',
755
+ '',
756
+ 'Options:',
757
+ ' --goal-id ID Required. Spend goal ID from goals list or create output.',
758
+ ' --apply Optional. Mark the goal spent in Sloth Money.',
759
+ ' --base-url URL Optional. Override the API origin.',
760
+ ' -h, --help Show this help.',
761
+ ...API_ORIGIN_HELP_LINES,
762
+ '',
763
+ 'Safety and lifecycle:',
764
+ ' Without --apply, the command previews PATCH {"isSpent":true} and does not write.',
765
+ ' Keep goals cannot be marked spent. Change an active goal to Spend first.',
766
+ ' A spent goal is excluded from future goal allocation until restored.',
767
+ '',
768
+ 'Example:',
769
+ ' sloth-agent goals mark-spent --goal-id wedding --apply',
770
+ '',
771
+ 'Output:',
772
+ ' Preview mode returns dryRun, method, endpoint, and payload.',
773
+ ' Apply mode returns the complete persisted goal and currency.',
774
+ ].join('\n');
775
+ }
776
+ export function goalsRestoreHelpText() {
777
+ return [
778
+ 'Sloth Agent CLI — goals restore',
779
+ '',
780
+ 'Preview or restore a spent Spend goal.',
781
+ '',
782
+ 'Usage:',
783
+ ' sloth-agent goals restore --goal-id ID [--apply] [--base-url URL]',
784
+ '',
785
+ 'Options:',
786
+ ' --goal-id ID Required. Spent goal ID from goals list output.',
787
+ ' --apply Optional. Restore the goal in Sloth Money.',
788
+ ' --base-url URL Optional. Override the API origin.',
789
+ ' -h, --help Show this help.',
790
+ ...API_ORIGIN_HELP_LINES,
791
+ '',
792
+ 'Safety and lifecycle:',
793
+ ' Without --apply, the command previews PATCH {"isSpent":false} and does not write.',
794
+ ' Restoring clears spentAt and returns the goal to allocation at its saved priority.',
795
+ '',
796
+ 'Example:',
797
+ ' sloth-agent goals restore --goal-id wedding --apply',
798
+ '',
799
+ 'Output:',
800
+ ' Preview mode returns dryRun, method, endpoint, and payload.',
801
+ ' Apply mode returns the complete persisted goal and currency.',
802
+ ].join('\n');
803
+ }
699
804
  export function goalsDeleteHelpText() {
700
805
  return [
701
806
  'Sloth Agent CLI — goals delete',
@@ -762,6 +867,7 @@ export function commandHelpText(topic) {
762
867
  'accounts-remove': accountsRemoveHelpText,
763
868
  investments: investmentsHelpText,
764
869
  budget: budgetHelpText,
870
+ 'budget-move': budgetMoveHelpText,
765
871
  'budget-update': budgetUpdateHelpText,
766
872
  categories: categoriesHelpText,
767
873
  'categories-create': categoriesCreateHelpText,
@@ -774,6 +880,8 @@ export function commandHelpText(topic) {
774
880
  'goals-list': goalsListHelpText,
775
881
  'goals-create': goalsCreateHelpText,
776
882
  'goals-update': goalsUpdateHelpText,
883
+ 'goals-mark-spent': goalsMarkSpentHelpText,
884
+ 'goals-restore': goalsRestoreHelpText,
777
885
  'goals-delete': goalsDeleteHelpText,
778
886
  'ask-partner': askPartnerHelpText,
779
887
  };
@@ -1076,6 +1184,24 @@ export async function runCli(argv = process.argv.slice(2), options = {}) {
1076
1184
  });
1077
1185
  return 0;
1078
1186
  }
1187
+ const budgetMovementPayload = parsed.command === 'budget-move'
1188
+ ? {
1189
+ scope: parsed.scope,
1190
+ ...(parsed.periodKey === undefined ? {} : { periodKey: parsed.periodKey }),
1191
+ fromCategoryId: parsed.fromCategoryId,
1192
+ toCategoryId: parsed.toCategoryId,
1193
+ amountPence: parsed.amountPence,
1194
+ }
1195
+ : undefined;
1196
+ if (parsed.command === 'budget-move' && !parsed.apply) {
1197
+ writeJson(writeStdout, {
1198
+ dryRun: true,
1199
+ endpoint: `${baseUrl}/api/agent/v1/budget-movements`,
1200
+ method: 'POST',
1201
+ payload: budgetMovementPayload,
1202
+ });
1203
+ return 0;
1204
+ }
1079
1205
  const credential = await resolveCredential(environment, baseUrl, getCredentialStore);
1080
1206
  token = credential.token;
1081
1207
  const headers = requestHeaders(token);
@@ -1118,6 +1244,17 @@ export async function runCli(argv = process.argv.slice(2), options = {}) {
1118
1244
  writeJson(writeStdout, data);
1119
1245
  return 0;
1120
1246
  }
1247
+ if (parsed.command === 'budget-move') {
1248
+ const response = await fetchImplementation(`${baseUrl}/api/agent/v1/budget-movements`, {
1249
+ method: 'POST',
1250
+ headers: { ...headers, 'Content-Type': 'application/json' },
1251
+ body: JSON.stringify(budgetMovementPayload),
1252
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
1253
+ });
1254
+ const data = validateBudgetMovementResponse(await parseHttpResponse(response, token), budgetMovementPayload);
1255
+ writeJson(writeStdout, data);
1256
+ return 0;
1257
+ }
1121
1258
  if (parsed.command === 'categories-create'
1122
1259
  || parsed.command === 'categories-rename'
1123
1260
  || parsed.command === 'line-items-create'
@@ -1156,12 +1293,11 @@ export async function runCli(argv = process.argv.slice(2), options = {}) {
1156
1293
  const endpoint = `${baseUrl}/api/agent/v1/goals`;
1157
1294
  const payload = {
1158
1295
  name: parsed.name,
1159
- ...(parsed.targetAmount === undefined
1160
- ? {}
1161
- : { targetAmount: parsed.targetAmount }),
1296
+ targetAmount: parsed.targetAmount,
1162
1297
  ...(parsed.targetMonthKey === undefined
1163
1298
  ? {}
1164
1299
  : { targetMonthKey: parsed.targetMonthKey }),
1300
+ goalType: parsed.goalType,
1165
1301
  };
1166
1302
  if (!parsed.apply) {
1167
1303
  writeJson(writeStdout, {
@@ -1185,23 +1321,27 @@ export async function runCli(argv = process.argv.slice(2), options = {}) {
1185
1321
  writeJson(writeStdout, data);
1186
1322
  return 0;
1187
1323
  }
1188
- if (parsed.command === 'goals-update') {
1324
+ if (parsed.command === 'goals-update'
1325
+ || parsed.command === 'goals-mark-spent'
1326
+ || parsed.command === 'goals-restore') {
1189
1327
  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
- };
1328
+ const payload = parsed.command === 'goals-update'
1329
+ ? {
1330
+ ...(parsed.name === undefined ? {} : { name: parsed.name }),
1331
+ ...(parsed.targetAmount === undefined
1332
+ ? {}
1333
+ : { targetAmount: parsed.targetAmount }),
1334
+ ...(parsed.targetMonthKey === undefined
1335
+ ? {}
1336
+ : { targetMonthKey: parsed.targetMonthKey }),
1337
+ ...(parsed.goalType === undefined
1338
+ ? {}
1339
+ : { goalType: parsed.goalType }),
1340
+ ...(parsed.priority === undefined
1341
+ ? {}
1342
+ : { priority: parsed.priority }),
1343
+ }
1344
+ : { isSpent: parsed.command === 'goals-mark-spent' };
1205
1345
  if (!parsed.apply) {
1206
1346
  writeJson(writeStdout, {
1207
1347
  dryRun: true,
@@ -1220,8 +1360,8 @@ export async function runCli(argv = process.argv.slice(2), options = {}) {
1220
1360
  body: JSON.stringify(payload),
1221
1361
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
1222
1362
  });
1223
- const data = parseApiResponse('goals-update', await parseHttpResponse(response, token));
1224
- writeJson(writeStdout, parsed.priority === undefined
1363
+ const data = parseApiResponse(parsed.command, await parseHttpResponse(response, token));
1364
+ writeJson(writeStdout, parsed.command !== 'goals-update' || parsed.priority === undefined
1225
1365
  ? data
1226
1366
  : withUpdatedGoalPriority(data, parsed.priority));
1227
1367
  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) {
@@ -370,6 +375,63 @@ function isBudgetResponse(value) {
370
375
  && Array.isArray(value.categories)
371
376
  && value.categories.every(isBudgetCategory));
372
377
  }
378
+ function isBudgetMovementResponse(value) {
379
+ return (isObject(value)
380
+ && hasOnlyFields(value, [
381
+ 'moved',
382
+ 'scope',
383
+ 'periodKey',
384
+ 'currency',
385
+ 'fromCategoryId',
386
+ 'toCategoryId',
387
+ 'amountPence',
388
+ 'toAssignPence',
389
+ 'categoryBalances',
390
+ ])
391
+ && value.moved === true
392
+ && (value.scope === 'personal' || value.scope === 'joint')
393
+ && typeof value.periodKey === 'string'
394
+ && /^\d{4}-(0[1-9]|1[0-2])$/.test(value.periodKey)
395
+ && isCurrency(value.currency)
396
+ && typeof value.fromCategoryId === 'string'
397
+ && value.fromCategoryId.trim().length > 0
398
+ && typeof value.toCategoryId === 'string'
399
+ && value.toCategoryId.trim().length > 0
400
+ && value.fromCategoryId !== value.toCategoryId
401
+ && isNonnegativeSafeInteger(value.amountPence)
402
+ && value.amountPence > 0
403
+ && isSafeInteger(value.toAssignPence)
404
+ && Array.isArray(value.categoryBalances)
405
+ && value.categoryBalances.length >= 1
406
+ && value.categoryBalances.length <= 2
407
+ && value.categoryBalances.every((balance) => (isObject(balance)
408
+ && hasOnlyFields(balance, ['categoryId', 'assignedPence'])
409
+ && typeof balance.categoryId === 'string'
410
+ && balance.categoryId.trim().length > 0
411
+ && isSafeInteger(balance.assignedPence))));
412
+ }
413
+ export function validateBudgetMovementResponse(value, expected) {
414
+ if (!isBudgetMovementResponse(value)) {
415
+ throw new ApiError('Invalid budget-move response from the Agent API');
416
+ }
417
+ const expectedCategoryIds = [expected.fromCategoryId, expected.toCategoryId]
418
+ .filter(categoryId => categoryId !== 'to-assign')
419
+ .sort();
420
+ const actualCategoryIds = value.categoryBalances
421
+ .map(balance => balance.categoryId)
422
+ .sort();
423
+ const matchesRequest = value.scope === expected.scope
424
+ && (expected.periodKey === undefined || value.periodKey === expected.periodKey)
425
+ && value.fromCategoryId === expected.fromCategoryId
426
+ && value.toCategoryId === expected.toCategoryId
427
+ && value.amountPence === expected.amountPence;
428
+ const matchesAffectedCategories = actualCategoryIds.length === expectedCategoryIds.length
429
+ && actualCategoryIds.every((categoryId, index) => categoryId === expectedCategoryIds[index]);
430
+ if (!matchesRequest || !matchesAffectedCategories) {
431
+ throw new ApiError('Invalid budget-move response from the Agent API');
432
+ }
433
+ return value;
434
+ }
373
435
  function isNullableNonEmptyString(value) {
374
436
  return value === null || (typeof value === 'string'
375
437
  && value.length > 0
@@ -506,23 +568,25 @@ export function parseApiResponse(command, value) {
506
568
  ? isInvestmentsResponse(value)
507
569
  : command === 'budget' || command === 'budget-update'
508
570
  ? isBudgetResponse(value)
509
- : command === 'categories'
510
- ? isCategoryResponse(value)
511
- : command === 'categories-create' || command === 'categories-rename'
512
- ? isCategoryMutationResponse(value)
513
- : command === 'line-items-create' || command === 'line-items-rename'
514
- ? isLineItemMutationResponse(value)
515
- : command === 'transactions'
516
- ? isTransactionsResponse(value)
517
- : command === 'assign'
518
- ? isAssignmentResponse(value)
519
- : command === 'ask-partner'
520
- ? isPartnerResponse(value)
521
- : command === 'goals-list'
522
- ? isGoalsResponse(value)
523
- : command === 'goals-delete'
524
- ? isGoalDeleteResponse(value)
525
- : isGoalMutationResponse(value);
571
+ : command === 'budget-move'
572
+ ? isBudgetMovementResponse(value)
573
+ : command === 'categories'
574
+ ? isCategoryResponse(value)
575
+ : command === 'categories-create' || command === 'categories-rename'
576
+ ? isCategoryMutationResponse(value)
577
+ : command === 'line-items-create' || command === 'line-items-rename'
578
+ ? isLineItemMutationResponse(value)
579
+ : command === 'transactions'
580
+ ? isTransactionsResponse(value)
581
+ : command === 'assign'
582
+ ? isAssignmentResponse(value)
583
+ : command === 'ask-partner'
584
+ ? isPartnerResponse(value)
585
+ : command === 'goals-list'
586
+ ? isGoalsResponse(value)
587
+ : command === 'goals-delete'
588
+ ? isGoalDeleteResponse(value)
589
+ : isGoalMutationResponse(value);
526
590
  if (!valid) {
527
591
  const label = command === 'assign' ? 'assignment' : command;
528
592
  throw new ApiError(`Invalid ${label} response from the Agent API`);
@@ -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.11.0",
4
4
  "description": "Command-line access to the Sloth Money Agent API.",
5
5
  "type": "module",
6
6
  "bin": {