@slothmoney/agent-cli 0.9.0 → 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 +17 -1
- package/README.md +100 -29
- package/dist/args.js +43 -29
- package/dist/cli.js +120 -40
- 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,22 @@
|
|
|
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`.
|
|
11
|
+
|
|
12
|
+
## 0.9.1 - 2026-08-15
|
|
13
|
+
|
|
14
|
+
- Clarify how personal and joint category assignments appear in transaction
|
|
15
|
+
results and how assignment scope affects uncategorised filters.
|
|
16
|
+
- Document complete category and line-item assignments, including the
|
|
17
|
+
category-specific `Other` fallback when no more specific line item fits.
|
|
18
|
+
- Extend command-help and clean-installed package checks for the categorisation
|
|
19
|
+
workflow.
|
|
4
20
|
|
|
5
21
|
## 0.9.0 - 2026-08-12
|
|
6
22
|
|
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,9 +112,39 @@ 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
|
|
|
120
|
+
### How transaction categorisation is represented
|
|
121
|
+
|
|
122
|
+
Personal and joint category assignments are separate. A personal assignment
|
|
123
|
+
uses the transaction's top-level `categoryId`, `lineItemId`, and
|
|
124
|
+
`categorySplits`. A joint-budget assignment uses the corresponding fields under
|
|
125
|
+
`jointBudgetContribution`.
|
|
126
|
+
|
|
127
|
+
A transaction can have no personal category while its joint-budget contribution
|
|
128
|
+
already has a category and line item. To assess a transaction's categorisation,
|
|
129
|
+
inspect both locations. An included joint contribution with a category and line
|
|
130
|
+
item is already categorised for the joint budget. For example:
|
|
131
|
+
|
|
132
|
+
```json
|
|
133
|
+
{
|
|
134
|
+
"categoryId": null,
|
|
135
|
+
"lineItemId": null,
|
|
136
|
+
"jointBudgetContribution": {
|
|
137
|
+
"included": true,
|
|
138
|
+
"categoryId": "groceries",
|
|
139
|
+
"lineItemId": "joint-groceries"
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
This transaction is uncategorised personally but categorised as Groceries for
|
|
145
|
+
the joint budget. The `--uncategorized` filter applies to the selected
|
|
146
|
+
assignment scope; personal is used when `--assignment-scope` is omitted.
|
|
147
|
+
|
|
118
148
|
### Categorise a transaction end to end
|
|
119
149
|
|
|
120
150
|
1. Read categories and available budget line items:
|
|
@@ -127,23 +157,43 @@ A category is the broader parent. A line item is a child within one category.
|
|
|
127
157
|
Line-item names such as `Other` may repeat, so preserve the full choice as
|
|
128
158
|
`(scope, categoryId, lineItemId)`. Use the personal or joint line-item map that
|
|
129
159
|
matches the transaction scope. For example, `Bills → Other` and `Subscriptions
|
|
130
|
-
→ Other` are different choices.
|
|
160
|
+
→ Other` are different choices. Choose the most specific suitable line item;
|
|
161
|
+
if none fits, use that category's `Other` line item. Historical assignments
|
|
162
|
+
without a line item should not be treated as a recommendation to omit one.
|
|
131
163
|
|
|
132
164
|
2. Read uncategorised transactions:
|
|
133
165
|
|
|
134
166
|
```bash
|
|
135
|
-
sloth-agent transactions
|
|
167
|
+
sloth-agent transactions \
|
|
168
|
+
--assignment-scope personal \
|
|
169
|
+
--uncategorized \
|
|
170
|
+
--limit 50
|
|
136
171
|
```
|
|
137
172
|
|
|
138
|
-
|
|
139
|
-
|
|
173
|
+
To read uncategorised joint-budget contributions instead:
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
sloth-agent transactions \
|
|
177
|
+
--assignment-scope joint \
|
|
178
|
+
--uncategorized \
|
|
179
|
+
--limit 50
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
The remaining example continues with a personal assignment. For a joint
|
|
183
|
+
assignment, set `"assignmentScope": "joint"` in the assignment payload and use
|
|
184
|
+
`--assignment-scope joint` when checking the result.
|
|
185
|
+
|
|
186
|
+
3. Copy the exact `transactionRef`, `categoryId`, and `lineItemId` from the
|
|
187
|
+
earlier outputs into `assignments.json`:
|
|
140
188
|
|
|
141
189
|
```json
|
|
142
190
|
{
|
|
143
191
|
"assignments": [
|
|
144
192
|
{
|
|
145
193
|
"transactionRef": "PASTE_THE_EXACT_TRANSACTION_REF_HERE",
|
|
146
|
-
"
|
|
194
|
+
"assignmentScope": "personal",
|
|
195
|
+
"categoryId": "PASTE_A_CATEGORY_ID_HERE",
|
|
196
|
+
"lineItemId": "PASTE_A_LINE_ITEM_ID_HERE"
|
|
147
197
|
}
|
|
148
198
|
]
|
|
149
199
|
}
|
|
@@ -172,17 +222,19 @@ This step requires a token created with **Allow changes**.
|
|
|
172
222
|
|
|
173
223
|
Inspect every item in the returned `succeeded` and `failed` arrays.
|
|
174
224
|
|
|
175
|
-
6. Check the result
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
`--uncategorized` and inspect
|
|
225
|
+
6. Check the result in the same assignment scope that you changed. Successful
|
|
226
|
+
assignments update the category and optional budget line item on the
|
|
227
|
+
original transaction. See the result in **Sloth Money → Transactions**, or
|
|
228
|
+
re-run the original transaction query without `--uncategorized` and inspect
|
|
229
|
+
both the personal and joint category fields:
|
|
179
230
|
|
|
180
231
|
```bash
|
|
181
|
-
sloth-agent transactions --limit 50
|
|
232
|
+
sloth-agent transactions --assignment-scope personal --limit 50
|
|
182
233
|
```
|
|
183
234
|
|
|
184
235
|
The transaction should also disappear from the matching `--uncategorized`
|
|
185
|
-
query.
|
|
236
|
+
query. Confirm that an existing assignment in the other scope was not changed.
|
|
237
|
+
Assignments do not create a separate list.
|
|
186
238
|
|
|
187
239
|
### Other workflows
|
|
188
240
|
|
|
@@ -363,45 +415,64 @@ Goal writes are previews unless `--apply` is present:
|
|
|
363
415
|
sloth-agent goals create \
|
|
364
416
|
--name "Emergency fund" \
|
|
365
417
|
--target-amount 12000 \
|
|
366
|
-
--
|
|
418
|
+
--type keep
|
|
367
419
|
|
|
368
420
|
sloth-agent goals create \
|
|
369
|
-
--name "
|
|
370
|
-
--target-amount
|
|
421
|
+
--name "Wedding" \
|
|
422
|
+
--target-amount 22000 \
|
|
371
423
|
--target-month 2027-06 \
|
|
424
|
+
--type spend \
|
|
372
425
|
--apply
|
|
373
426
|
```
|
|
374
427
|
|
|
375
|
-
|
|
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:
|
|
376
436
|
|
|
377
437
|
```bash
|
|
378
438
|
sloth-agent goals update \
|
|
379
439
|
--goal-id goal-id \
|
|
380
|
-
--clear-target-amount \
|
|
381
440
|
--target-month 2027-12 \
|
|
382
|
-
--
|
|
441
|
+
--type spend \
|
|
383
442
|
--apply
|
|
384
443
|
|
|
385
444
|
sloth-agent goals update \
|
|
386
445
|
--goal-id house-goal-id \
|
|
387
446
|
--priority 2 \
|
|
388
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
|
|
389
458
|
|
|
390
459
|
sloth-agent goals delete --goal-id goal-id --apply
|
|
391
460
|
```
|
|
392
461
|
|
|
393
|
-
Updates are partial. Use `--clear-target-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
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
|
|
399
472
|
one-based `priority`; `1` is highest. Moving one goal automatically shifts the
|
|
400
|
-
goals between its old and new positions. The
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
progress are browser-owned derived state and refresh when the owner next opens
|
|
404
|
-
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.
|
|
405
476
|
|
|
406
477
|
Read uncategorised contributions to the joint budget:
|
|
407
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
|
'',
|
|
@@ -185,6 +187,8 @@ export function categoriesHelpText() {
|
|
|
185
187
|
' Line-item names such as "Other" may repeat. Preserve the full choice as',
|
|
186
188
|
' (scope, categoryId, lineItemId), using the personal or joint line-item',
|
|
187
189
|
' map matching the transaction scope.',
|
|
190
|
+
' Choose the most specific suitable line item. If none fits, use that',
|
|
191
|
+
' category\'s "Other" line item.',
|
|
188
192
|
'',
|
|
189
193
|
'Examples:',
|
|
190
194
|
' sloth-agent categories',
|
|
@@ -463,7 +467,8 @@ export function transactionsHelpText() {
|
|
|
463
467
|
' sloth-agent transactions [options]',
|
|
464
468
|
'',
|
|
465
469
|
'Options:',
|
|
466
|
-
' --uncategorized[=true|false] Optional. Filter
|
|
470
|
+
' --uncategorized[=true|false] Optional. Filter the selected assignment scope by state;',
|
|
471
|
+
' with no value, use true.',
|
|
467
472
|
' --limit N Optional. Integer from 1 to 200; omit for API default.',
|
|
468
473
|
' --start-date YYYY-MM-DD Optional. Include transactions on or after this date.',
|
|
469
474
|
' --end-date YYYY-MM-DD Optional. Include transactions on or before this date.',
|
|
@@ -472,6 +477,7 @@ export function transactionsHelpText() {
|
|
|
472
477
|
' --category-id ID Optional. Filter by category ID.',
|
|
473
478
|
' --line-item-id ID Optional. Filter primary or split assignments by line-item ID.',
|
|
474
479
|
' --assignment-scope SCOPE Optional. Filter assignments by personal or joint.',
|
|
480
|
+
' Personal is used when omitted.',
|
|
475
481
|
' --cursor CURSOR Optional. Continue from a previous nextCursor.',
|
|
476
482
|
' --base-url URL Optional. Override the API origin.',
|
|
477
483
|
' -h, --help Show this help.',
|
|
@@ -487,6 +493,10 @@ export function transactionsHelpText() {
|
|
|
487
493
|
'Output:',
|
|
488
494
|
' JSON containing transactions, nextCursor, and structured refresh status.',
|
|
489
495
|
' Refresh failures do not hide readable cached transactions.',
|
|
496
|
+
' Personal assignments use the top-level categoryId, lineItemId, and categorySplits.',
|
|
497
|
+
' Joint assignments appear under jointBudgetContribution.',
|
|
498
|
+
' A transaction can be uncategorised personally while its joint-budget contribution',
|
|
499
|
+
' is already categorised. To assess its categorisation, inspect both locations.',
|
|
490
500
|
' Use nextCursor with --cursor',
|
|
491
501
|
' to request the next page. A null nextCursor means there are no more pages.',
|
|
492
502
|
'',
|
|
@@ -529,29 +539,34 @@ export function assignHelpText() {
|
|
|
529
539
|
' categories output. The example values below are placeholders.',
|
|
530
540
|
' Set categoryId to null to clear an assignment.',
|
|
531
541
|
' lineItemId is optional and accepts a non-empty string or null.',
|
|
542
|
+
' When the category has line items, choose the most specific suitable line item.',
|
|
543
|
+
' If none fits, use that category\'s Other line item from the matching scope.',
|
|
532
544
|
' categorySplits is optional and accepts a non-empty array or null.',
|
|
533
545
|
' Each split requires categoryId and a positive integer amountPence;',
|
|
534
546
|
' a split lineItemId is optional.',
|
|
535
547
|
' incomeSubtype is optional and accepts "pay", "interest", or null.',
|
|
536
548
|
' assignmentScope is optional and accepts "personal" or "joint".',
|
|
549
|
+
' Personal is used when assignmentScope is omitted.',
|
|
537
550
|
'',
|
|
538
551
|
'Workflow:',
|
|
539
552
|
' sloth-agent categories',
|
|
540
|
-
' sloth-agent transactions --uncategorized --limit 50',
|
|
553
|
+
' sloth-agent transactions --assignment-scope personal --uncategorized --limit 50',
|
|
541
554
|
' sloth-agent assign --input assignments.json Preview only',
|
|
542
555
|
' sloth-agent assign --input assignments.json --apply Write assignments',
|
|
543
|
-
' sloth-agent transactions --limit 50
|
|
556
|
+
' sloth-agent transactions --assignment-scope personal --limit 50 Read back',
|
|
544
557
|
'',
|
|
545
558
|
'Example:',
|
|
546
559
|
' {',
|
|
547
560
|
' "assignments": [',
|
|
548
561
|
' {',
|
|
549
562
|
' "transactionRef": "PASTE_THE_EXACT_TRANSACTION_REF_HERE",',
|
|
550
|
-
' "
|
|
563
|
+
' "assignmentScope": "personal",',
|
|
564
|
+
' "categoryId": "PASTE_A_CATEGORY_ID_HERE",',
|
|
565
|
+
' "lineItemId": "PASTE_A_LINE_ITEM_ID_HERE"',
|
|
551
566
|
' }',
|
|
552
567
|
' ]',
|
|
553
568
|
' }',
|
|
554
|
-
' These are placeholders. Replace
|
|
569
|
+
' These are placeholders. Replace all three values with exact IDs from CLI output.',
|
|
555
570
|
'',
|
|
556
571
|
'Output:',
|
|
557
572
|
' Preview mode returns dryRun, endpoint, and the validated payload.',
|
|
@@ -565,12 +580,14 @@ export function goalsHelpText() {
|
|
|
565
580
|
return [
|
|
566
581
|
'Sloth Agent CLI — goals',
|
|
567
582
|
'',
|
|
568
|
-
'List, create, update, or delete your savings goals.',
|
|
583
|
+
'List, create, update, mark spent, restore, or delete your savings goals.',
|
|
569
584
|
'',
|
|
570
585
|
'Commands:',
|
|
571
586
|
' sloth-agent goals list List goals; "sloth-agent goals" is equivalent.',
|
|
572
587
|
' sloth-agent goals create Preview or create a goal.',
|
|
573
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.',
|
|
574
591
|
' sloth-agent goals delete Preview or permanently delete a goal.',
|
|
575
592
|
'',
|
|
576
593
|
'Help:',
|
|
@@ -598,7 +615,8 @@ export function goalsListHelpText() {
|
|
|
598
615
|
'',
|
|
599
616
|
'Output:',
|
|
600
617
|
' JSON containing currency and goals. Each goal contains id, name, priority,',
|
|
601
|
-
' targetAmount, targetMonthKey,
|
|
618
|
+
' targetAmount, targetMonthKey, goalType, nullable spentAt, and',
|
|
619
|
+
' sharedWithPartner.',
|
|
602
620
|
].join('\n');
|
|
603
621
|
}
|
|
604
622
|
export function goalsCreateHelpText() {
|
|
@@ -608,11 +626,12 @@ export function goalsCreateHelpText() {
|
|
|
608
626
|
'Preview or create a goal.',
|
|
609
627
|
'',
|
|
610
628
|
'Usage:',
|
|
611
|
-
' sloth-agent goals create --name NAME [options]',
|
|
629
|
+
' sloth-agent goals create --name NAME --target-amount AMOUNT --type keep|spend [options]',
|
|
612
630
|
'',
|
|
613
631
|
'Options:',
|
|
614
632
|
' --name NAME Required. Goal name, 1 to 200 characters.',
|
|
615
|
-
' --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.',
|
|
616
635
|
' --target-month YYYY-MM Optional. Target calendar month.',
|
|
617
636
|
' --apply Optional. Create the goal in Sloth Money.',
|
|
618
637
|
' --base-url URL Optional. Override the API origin.',
|
|
@@ -625,8 +644,8 @@ export function goalsCreateHelpText() {
|
|
|
625
644
|
' New goals are private to the owner and appended to the existing goal order.',
|
|
626
645
|
'',
|
|
627
646
|
'Example:',
|
|
628
|
-
' sloth-agent goals create --name "Emergency fund" --target-amount 12000',
|
|
629
|
-
' 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',
|
|
630
649
|
'',
|
|
631
650
|
'Output:',
|
|
632
651
|
' Preview mode returns dryRun, method, endpoint, and payload.',
|
|
@@ -646,10 +665,9 @@ export function goalsUpdateHelpText() {
|
|
|
646
665
|
' --goal-id ID Required. Goal ID from goals list or create output.',
|
|
647
666
|
' --name NAME Optional. Replacement name, 1 to 200 characters.',
|
|
648
667
|
' --target-amount AMOUNT Optional. Positive amount with up to 2 decimals.',
|
|
649
|
-
' --clear-target-amount Optional. Remove the target amount.',
|
|
650
668
|
' --target-month YYYY-MM Optional. Replace the target month.',
|
|
651
669
|
' --clear-target-month Optional. Remove the target month.',
|
|
652
|
-
' --
|
|
670
|
+
' --type keep|spend Optional. Change how funded money is treated.',
|
|
653
671
|
' --priority POSITION Optional. Positive whole-number position; 1 is highest.',
|
|
654
672
|
' --apply Optional. Write the partial update.',
|
|
655
673
|
' --base-url URL Optional. Override the API origin.',
|
|
@@ -663,9 +681,8 @@ export function goalsUpdateHelpText() {
|
|
|
663
681
|
' Moving a goal shifts the intervening goals automatically.',
|
|
664
682
|
' Forecast assignments and shared progress refresh when the owner next opens',
|
|
665
683
|
' the Forecast screen.',
|
|
666
|
-
' Set and clear options
|
|
667
|
-
'
|
|
668
|
-
' 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.',
|
|
669
686
|
' Change active shared pot target amounts in the Sloth Budget app, where',
|
|
670
687
|
' account balances can be reconciled across goals in priority order.',
|
|
671
688
|
' Sharing remains app-managed. Updates to an already shared goal remain visible',
|
|
@@ -676,6 +693,7 @@ export function goalsUpdateHelpText() {
|
|
|
676
693
|
' Applying requires a write-enabled token created with Allow changes.',
|
|
677
694
|
'',
|
|
678
695
|
'Example:',
|
|
696
|
+
' sloth-agent goals update --goal-id wedding --type spend --apply',
|
|
679
697
|
' sloth-agent goals update --goal-id goal-3 --priority 2 --apply',
|
|
680
698
|
'',
|
|
681
699
|
'Output:',
|
|
@@ -683,6 +701,63 @@ export function goalsUpdateHelpText() {
|
|
|
683
701
|
' Apply mode returns the complete persisted goal and currency.',
|
|
684
702
|
].join('\n');
|
|
685
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
|
+
}
|
|
686
761
|
export function goalsDeleteHelpText() {
|
|
687
762
|
return [
|
|
688
763
|
'Sloth Agent CLI — goals delete',
|
|
@@ -761,6 +836,8 @@ export function commandHelpText(topic) {
|
|
|
761
836
|
'goals-list': goalsListHelpText,
|
|
762
837
|
'goals-create': goalsCreateHelpText,
|
|
763
838
|
'goals-update': goalsUpdateHelpText,
|
|
839
|
+
'goals-mark-spent': goalsMarkSpentHelpText,
|
|
840
|
+
'goals-restore': goalsRestoreHelpText,
|
|
764
841
|
'goals-delete': goalsDeleteHelpText,
|
|
765
842
|
'ask-partner': askPartnerHelpText,
|
|
766
843
|
};
|
|
@@ -1143,12 +1220,11 @@ export async function runCli(argv = process.argv.slice(2), options = {}) {
|
|
|
1143
1220
|
const endpoint = `${baseUrl}/api/agent/v1/goals`;
|
|
1144
1221
|
const payload = {
|
|
1145
1222
|
name: parsed.name,
|
|
1146
|
-
|
|
1147
|
-
? {}
|
|
1148
|
-
: { targetAmount: parsed.targetAmount }),
|
|
1223
|
+
targetAmount: parsed.targetAmount,
|
|
1149
1224
|
...(parsed.targetMonthKey === undefined
|
|
1150
1225
|
? {}
|
|
1151
1226
|
: { targetMonthKey: parsed.targetMonthKey }),
|
|
1227
|
+
goalType: parsed.goalType,
|
|
1152
1228
|
};
|
|
1153
1229
|
if (!parsed.apply) {
|
|
1154
1230
|
writeJson(writeStdout, {
|
|
@@ -1172,23 +1248,27 @@ export async function runCli(argv = process.argv.slice(2), options = {}) {
|
|
|
1172
1248
|
writeJson(writeStdout, data);
|
|
1173
1249
|
return 0;
|
|
1174
1250
|
}
|
|
1175
|
-
if (parsed.command === 'goals-update'
|
|
1251
|
+
if (parsed.command === 'goals-update'
|
|
1252
|
+
|| parsed.command === 'goals-mark-spent'
|
|
1253
|
+
|| parsed.command === 'goals-restore') {
|
|
1176
1254
|
const endpoint = `${baseUrl}/api/agent/v1/goals/${encodeURIComponent(parsed.goalId)}`;
|
|
1177
|
-
const payload =
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
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' };
|
|
1192
1272
|
if (!parsed.apply) {
|
|
1193
1273
|
writeJson(writeStdout, {
|
|
1194
1274
|
dryRun: true,
|
|
@@ -1207,8 +1287,8 @@ export async function runCli(argv = process.argv.slice(2), options = {}) {
|
|
|
1207
1287
|
body: JSON.stringify(payload),
|
|
1208
1288
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
1209
1289
|
});
|
|
1210
|
-
const data = parseApiResponse(
|
|
1211
|
-
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
|
|
1212
1292
|
? data
|
|
1213
1293
|
: withUpdatedGoalPriority(data, parsed.priority));
|
|
1214
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) {
|