@slothmoney/agent-cli 0.6.0 → 0.8.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 +12 -0
- package/README.md +66 -6
- package/dist/args.js +57 -2
- package/dist/cli.js +158 -4
- package/dist/contracts.js +104 -17
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.8.0 - 2026-08-12
|
|
6
|
+
|
|
7
|
+
- Read each goal's one-based priority and move one goal to a new position with
|
|
8
|
+
automatic shifting of the intervening goals.
|
|
9
|
+
|
|
10
|
+
## 0.7.0 - 2026-08-09
|
|
11
|
+
|
|
12
|
+
- Read personal or joint budget periods with categories, line items, funding,
|
|
13
|
+
and planned amounts.
|
|
14
|
+
- Preview or apply planned line-item updates that overwrite the selected period
|
|
15
|
+
and all explicit future plans.
|
|
16
|
+
|
|
5
17
|
## 0.6.0 - 2026-08-08
|
|
6
18
|
|
|
7
19
|
- Expose goal-savings membership on account inventory rows and preview or apply
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Sloth Agent CLI
|
|
2
2
|
|
|
3
|
-
Use your own agent to inspect accounts and
|
|
3
|
+
Use your own agent to inspect accounts, investments, and budgets, manage goals, 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.
|
|
18
|
+
npm exec --yes --package=@slothmoney/agent-cli@0.8.0 -- sloth-agent --help
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
## Authenticate
|
|
@@ -25,9 +25,9 @@ Create a personal access token in Sloth Money under
|
|
|
25
25
|
where the CLI runs.
|
|
26
26
|
|
|
27
27
|
New tokens are view-only. That is enough for `auth status`, `accounts`, `investments`,
|
|
28
|
-
`categories`, `transactions`, and `goals` list. Enable **Allow changes** when
|
|
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, change goal-savings account membership, ask a partner for an explanation, or manage goals. Token
|
|
30
|
+
or line items, update planned budgets, change goal-savings account membership, 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
|
|
@@ -103,6 +103,8 @@ options, output, and examples:
|
|
|
103
103
|
```bash
|
|
104
104
|
sloth-agent auth login --help
|
|
105
105
|
sloth-agent accounts --help
|
|
106
|
+
sloth-agent budget --help
|
|
107
|
+
sloth-agent budget update --help
|
|
106
108
|
sloth-agent categories --help
|
|
107
109
|
sloth-agent categories create --help
|
|
108
110
|
sloth-agent line-items create --help
|
|
@@ -184,6 +186,53 @@ query. Assignments do not create a separate list.
|
|
|
184
186
|
|
|
185
187
|
### Other workflows
|
|
186
188
|
|
|
189
|
+
Read a personal or joint budget. Omit `--period` to use Sloth's current budget period:
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
sloth-agent budget --scope personal --period 2026-08
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
The result includes the budget period and status, currency, the effective plan,
|
|
196
|
+
stored funding amounts when available, categories, line items, and planned
|
|
197
|
+
amounts in pence.
|
|
198
|
+
|
|
199
|
+
Update selected line-item amounts by creating `budget.json`:
|
|
200
|
+
|
|
201
|
+
```json
|
|
202
|
+
{
|
|
203
|
+
"allocations": [
|
|
204
|
+
{
|
|
205
|
+
"categoryId": "groceries",
|
|
206
|
+
"lineItemId": "weekly",
|
|
207
|
+
"plannedPence": 45000
|
|
208
|
+
}
|
|
209
|
+
]
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Preview locally, then apply the same file:
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
sloth-agent budget update \
|
|
217
|
+
--scope personal \
|
|
218
|
+
--period 2026-08 \
|
|
219
|
+
--input budget.json
|
|
220
|
+
|
|
221
|
+
sloth-agent budget update \
|
|
222
|
+
--scope personal \
|
|
223
|
+
--period 2026-08 \
|
|
224
|
+
--input budget.json \
|
|
225
|
+
--apply
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
The update starts from the complete selected-period budget, changes the listed
|
|
229
|
+
line items, then overwrites the selected period and every explicit future plan
|
|
230
|
+
with that complete result. A later update from another period overwrites that
|
|
231
|
+
period and everything after it. Earlier and historical periods remain unchanged.
|
|
232
|
+
|
|
233
|
+
Without `--apply`, the CLI validates the file locally and does not load a token
|
|
234
|
+
or contact Sloth Money. Applying requires a write-enabled token.
|
|
235
|
+
|
|
187
236
|
Create or rename a custom category. Writes are previews until `--apply` is
|
|
188
237
|
present:
|
|
189
238
|
|
|
@@ -306,7 +355,7 @@ sloth-agent goals create \
|
|
|
306
355
|
--apply
|
|
307
356
|
```
|
|
308
357
|
|
|
309
|
-
Use the `id` from list or create output to update or delete
|
|
358
|
+
Use the `id` from list or create output to update or delete goals:
|
|
310
359
|
|
|
311
360
|
```bash
|
|
312
361
|
sloth-agent goals update \
|
|
@@ -316,6 +365,11 @@ sloth-agent goals update \
|
|
|
316
365
|
--achieved=false \
|
|
317
366
|
--apply
|
|
318
367
|
|
|
368
|
+
sloth-agent goals update \
|
|
369
|
+
--goal-id house-goal-id \
|
|
370
|
+
--priority 2 \
|
|
371
|
+
--apply
|
|
372
|
+
|
|
319
373
|
sloth-agent goals delete --goal-id goal-id --apply
|
|
320
374
|
```
|
|
321
375
|
|
|
@@ -324,7 +378,13 @@ remove an optional value. Marking a goal achieved removes its forecast
|
|
|
324
378
|
assignment. Deleting a goal also removes its forecast assignments and drift
|
|
325
379
|
history. Goal sharing remains app-managed. Change an active shared goal's
|
|
326
380
|
pot-tracked target amount in the Sloth Budget app, where account balances can
|
|
327
|
-
be reallocated across goals in priority order.
|
|
381
|
+
be reallocated across goals in priority order. Goal list output includes a
|
|
382
|
+
one-based `priority`; `1` is highest. Moving one goal automatically shifts the
|
|
383
|
+
goals between its old and new positions. The
|
|
384
|
+
priority option must be used on its own, and the write persists immediately.
|
|
385
|
+
Forecast assignments and shared pot
|
|
386
|
+
progress are browser-owned derived state and refresh when the owner next opens
|
|
387
|
+
the Forecast screen.
|
|
328
388
|
|
|
329
389
|
Read uncategorised contributions to the joint budget:
|
|
330
390
|
|
package/dist/args.js
CHANGED
|
@@ -64,6 +64,16 @@ function parseGoalMonthKey(value, name) {
|
|
|
64
64
|
}
|
|
65
65
|
return value;
|
|
66
66
|
}
|
|
67
|
+
function parseGoalPriority(value) {
|
|
68
|
+
if (!/^[1-9]\d*$/.test(value)) {
|
|
69
|
+
throw new UsageError('--priority must be a positive whole-number position');
|
|
70
|
+
}
|
|
71
|
+
const priority = Number(value);
|
|
72
|
+
if (!Number.isSafeInteger(priority)) {
|
|
73
|
+
throw new UsageError('--priority must be a positive whole-number position');
|
|
74
|
+
}
|
|
75
|
+
return priority;
|
|
76
|
+
}
|
|
67
77
|
function parseExplicitBoolean(value, name) {
|
|
68
78
|
if (value !== 'true' && value !== 'false') {
|
|
69
79
|
throw new UsageError(`${name} must be true or false`);
|
|
@@ -274,6 +284,31 @@ function parseInvestments(args, baseUrl) {
|
|
|
274
284
|
}
|
|
275
285
|
return withBaseUrl({ command: 'investments', ...(accountRef ? { accountRef } : {}) }, baseUrl);
|
|
276
286
|
}
|
|
287
|
+
function parseBudget(args, baseUrl) {
|
|
288
|
+
const update = args[0] === 'update';
|
|
289
|
+
if (update)
|
|
290
|
+
args.shift();
|
|
291
|
+
const { values, apply } = parseNamedOptions(args, update ? 'budget update' : 'budget', new Set(update ? ['--scope', '--period', '--input'] : ['--scope', '--period']));
|
|
292
|
+
if (!update && apply)
|
|
293
|
+
throw new UsageError('Unknown budget option: --apply');
|
|
294
|
+
const scope = requiredOption(values, '--scope', update ? 'budget update' : 'budget');
|
|
295
|
+
if (scope !== 'personal' && scope !== 'joint') {
|
|
296
|
+
throw new UsageError('--scope must be personal or joint');
|
|
297
|
+
}
|
|
298
|
+
const period = values.get('--period');
|
|
299
|
+
const common = {
|
|
300
|
+
scope: scope,
|
|
301
|
+
...(period === undefined ? {} : { periodKey: parseGoalMonthKey(period, '--period') }),
|
|
302
|
+
};
|
|
303
|
+
if (!update)
|
|
304
|
+
return withBaseUrl({ command: 'budget', ...common }, baseUrl);
|
|
305
|
+
return withBaseUrl({
|
|
306
|
+
command: 'budget-update',
|
|
307
|
+
...common,
|
|
308
|
+
input: requiredOption(values, '--input', 'budget update'),
|
|
309
|
+
apply,
|
|
310
|
+
}, baseUrl);
|
|
311
|
+
}
|
|
277
312
|
function parseCategories(args, baseUrl) {
|
|
278
313
|
const subcommand = args.shift();
|
|
279
314
|
if (subcommand === undefined || subcommand === 'list') {
|
|
@@ -442,6 +477,7 @@ function parseGoals(args, baseUrl) {
|
|
|
442
477
|
let targetAmount;
|
|
443
478
|
let targetMonthKey;
|
|
444
479
|
let isAchieved;
|
|
480
|
+
let priority;
|
|
445
481
|
let apply = false;
|
|
446
482
|
for (let index = 0; index < args.length; index += 1) {
|
|
447
483
|
const argument = args[index];
|
|
@@ -472,7 +508,8 @@ function parseGoals(args, baseUrl) {
|
|
|
472
508
|
&& option !== '--name'
|
|
473
509
|
&& option !== '--target-amount'
|
|
474
510
|
&& option !== '--target-month'
|
|
475
|
-
&& option !== '--achieved'
|
|
511
|
+
&& option !== '--achieved'
|
|
512
|
+
&& option !== '--priority') {
|
|
476
513
|
throw new UsageError(`Unknown goals update option: ${argument}`);
|
|
477
514
|
}
|
|
478
515
|
const value = requireNonEmpty(inlineValue ?? readOptionValue(args, index, option), option);
|
|
@@ -496,6 +533,9 @@ function parseGoals(args, baseUrl) {
|
|
|
496
533
|
}
|
|
497
534
|
targetMonthKey = parseGoalMonthKey(value, option);
|
|
498
535
|
}
|
|
536
|
+
else if (option === '--priority') {
|
|
537
|
+
priority = setOnce(priority, parseGoalPriority(value), option);
|
|
538
|
+
}
|
|
499
539
|
else {
|
|
500
540
|
isAchieved = setOnce(isAchieved, parseExplicitBoolean(value, option), option);
|
|
501
541
|
}
|
|
@@ -505,9 +545,17 @@ function parseGoals(args, baseUrl) {
|
|
|
505
545
|
if (name === undefined
|
|
506
546
|
&& targetAmount === undefined
|
|
507
547
|
&& targetMonthKey === undefined
|
|
508
|
-
&& isAchieved === undefined
|
|
548
|
+
&& isAchieved === undefined
|
|
549
|
+
&& priority === undefined) {
|
|
509
550
|
throw new UsageError('goals update requires at least one field to update');
|
|
510
551
|
}
|
|
552
|
+
if (priority !== undefined
|
|
553
|
+
&& (name !== undefined
|
|
554
|
+
|| targetAmount !== undefined
|
|
555
|
+
|| targetMonthKey !== undefined
|
|
556
|
+
|| isAchieved !== undefined)) {
|
|
557
|
+
throw new UsageError('--priority must be used on its own');
|
|
558
|
+
}
|
|
511
559
|
return withBaseUrl({
|
|
512
560
|
command: 'goals-update',
|
|
513
561
|
goalId,
|
|
@@ -515,6 +563,7 @@ function parseGoals(args, baseUrl) {
|
|
|
515
563
|
...(targetAmount === undefined ? {} : { targetAmount }),
|
|
516
564
|
...(targetMonthKey === undefined ? {} : { targetMonthKey }),
|
|
517
565
|
...(isAchieved === undefined ? {} : { isAchieved }),
|
|
566
|
+
...(priority === undefined ? {} : { priority }),
|
|
518
567
|
apply,
|
|
519
568
|
}, baseUrl);
|
|
520
569
|
}
|
|
@@ -599,6 +648,9 @@ function helpTopic(argv) {
|
|
|
599
648
|
return 'line-items-rename';
|
|
600
649
|
return undefined;
|
|
601
650
|
}
|
|
651
|
+
if (command === 'budget') {
|
|
652
|
+
return subcommand === 'update' ? 'budget-update' : 'budget';
|
|
653
|
+
}
|
|
602
654
|
if (command === 'accounts'
|
|
603
655
|
|| command === 'transactions'
|
|
604
656
|
|| command === 'assign'
|
|
@@ -640,6 +692,9 @@ export function parseArgs(argv) {
|
|
|
640
692
|
if (command === 'investments') {
|
|
641
693
|
return parseInvestments(args, baseUrl);
|
|
642
694
|
}
|
|
695
|
+
if (command === 'budget') {
|
|
696
|
+
return parseBudget(args, baseUrl);
|
|
697
|
+
}
|
|
643
698
|
if (command === 'transactions') {
|
|
644
699
|
return withBaseUrl({ command, filters: parseTransactions(args) }, baseUrl);
|
|
645
700
|
}
|
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, } from './contracts.js';
|
|
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.8.0';
|
|
8
8
|
const REQUEST_TIMEOUT_MS = 60_000;
|
|
9
9
|
const API_ORIGIN_HELP_LINES = [
|
|
10
10
|
'',
|
|
@@ -28,6 +28,9 @@ export function usageText() {
|
|
|
28
28
|
' sloth-agent accounts [list] [--base-url URL]',
|
|
29
29
|
' sloth-agent accounts update --account-ref REF --goal-savings-source true|false [--apply]',
|
|
30
30
|
' sloth-agent investments [--account-ref REF] [--base-url URL]',
|
|
31
|
+
' sloth-agent budget --scope personal|joint [--period YYYY-MM] [--base-url URL]',
|
|
32
|
+
' sloth-agent budget update --scope personal|joint [--period YYYY-MM]',
|
|
33
|
+
' --input budget.json [--apply] [--base-url URL]',
|
|
31
34
|
' sloth-agent categories [list] [--base-url URL]',
|
|
32
35
|
' sloth-agent categories create --name NAME --icon-key KEY --type TYPE [--apply]',
|
|
33
36
|
' sloth-agent categories rename --category-id ID --name NAME [--apply]',
|
|
@@ -351,6 +354,70 @@ export function investmentsHelpText() {
|
|
|
351
354
|
' provider-native and are not converted or guaranteed to reconcile to totals.',
|
|
352
355
|
].join('\n');
|
|
353
356
|
}
|
|
357
|
+
export function budgetHelpText() {
|
|
358
|
+
return [
|
|
359
|
+
'Sloth Agent CLI — budget',
|
|
360
|
+
'',
|
|
361
|
+
'Read one personal or joint budget period.',
|
|
362
|
+
'',
|
|
363
|
+
'Usage:',
|
|
364
|
+
' sloth-agent budget --scope personal|joint [--period YYYY-MM] [--base-url URL]',
|
|
365
|
+
'',
|
|
366
|
+
'Options:',
|
|
367
|
+
' --scope personal|joint Required. Budget ownership scope.',
|
|
368
|
+
' --period YYYY-MM Optional. Defaults to the current Sloth budget period.',
|
|
369
|
+
' --base-url URL Optional. Override the API origin.',
|
|
370
|
+
' -h, --help Show this help.',
|
|
371
|
+
...API_ORIGIN_HELP_LINES,
|
|
372
|
+
'',
|
|
373
|
+
'Access:',
|
|
374
|
+
' This command is read-only and requires agent:read.',
|
|
375
|
+
'',
|
|
376
|
+
'Output:',
|
|
377
|
+
' JSON containing scope, periodKey, periodStatus, currency, and effectiveFromPeriodKey.',
|
|
378
|
+
' funding contains current stored to-assign and reserve amounts when that period exists.',
|
|
379
|
+
' categories[].lineItems contains line-item IDs, names, and planned amounts in pence.',
|
|
380
|
+
' Categories also include plannedPence and assignedPence.',
|
|
381
|
+
].join('\n');
|
|
382
|
+
}
|
|
383
|
+
export function budgetUpdateHelpText() {
|
|
384
|
+
return [
|
|
385
|
+
'Sloth Agent CLI — budget update',
|
|
386
|
+
'',
|
|
387
|
+
'Preview or update planned line-item amounts for one budget scope.',
|
|
388
|
+
'',
|
|
389
|
+
'Usage:',
|
|
390
|
+
' sloth-agent budget update --scope personal|joint [--period YYYY-MM] --input FILE [--apply] [--base-url URL]',
|
|
391
|
+
'',
|
|
392
|
+
'Required inputs:',
|
|
393
|
+
' --scope personal|joint Budget ownership scope.',
|
|
394
|
+
' --input FILE JSON file containing allocations.',
|
|
395
|
+
'',
|
|
396
|
+
'Optional inputs:',
|
|
397
|
+
' --period YYYY-MM Defaults to the current Sloth budget period.',
|
|
398
|
+
' --apply Send the update. Without it, only validate and preview.',
|
|
399
|
+
' --base-url URL Override the API origin.',
|
|
400
|
+
' -h, --help Show this help.',
|
|
401
|
+
...API_ORIGIN_HELP_LINES,
|
|
402
|
+
'',
|
|
403
|
+
'Input format:',
|
|
404
|
+
' {"allocations":[{"categoryId":"groceries","lineItemId":"weekly","plannedPence":45000}]}',
|
|
405
|
+
' Provide 1 to 100 unique categoryId and lineItemId pairs.',
|
|
406
|
+
' plannedPence must be a nonnegative whole number of pence.',
|
|
407
|
+
'',
|
|
408
|
+
'Write behavior:',
|
|
409
|
+
' Without --apply, returns JSON after local validation and does not load credentials',
|
|
410
|
+
' or contact Sloth Money. A successful preview does not guarantee the remote write.',
|
|
411
|
+
' With --apply, each supplied amount patches a complete selected-period budget.',
|
|
412
|
+
' The resulting complete budget overwrites the selected period and every explicit future plan.',
|
|
413
|
+
' A later update from another period overwrites that period and everything after it.',
|
|
414
|
+
' Historical periods cannot be changed. Applying requires agent:write.',
|
|
415
|
+
'',
|
|
416
|
+
'Output:',
|
|
417
|
+
' Preview mode returns dryRun, endpoint, method, and the validated payload.',
|
|
418
|
+
' Apply mode returns the complete persisted budget response.',
|
|
419
|
+
].join('\n');
|
|
420
|
+
}
|
|
354
421
|
export function transactionsHelpText() {
|
|
355
422
|
return [
|
|
356
423
|
'Sloth Agent CLI — transactions',
|
|
@@ -495,7 +562,7 @@ export function goalsListHelpText() {
|
|
|
495
562
|
' This command is read-only.',
|
|
496
563
|
'',
|
|
497
564
|
'Output:',
|
|
498
|
-
' JSON containing currency and goals. Each goal contains id, name,',
|
|
565
|
+
' JSON containing currency and goals. Each goal contains id, name, priority,',
|
|
499
566
|
' targetAmount, targetMonthKey, isAchieved, and sharedWithPartner.',
|
|
500
567
|
].join('\n');
|
|
501
568
|
}
|
|
@@ -548,6 +615,7 @@ export function goalsUpdateHelpText() {
|
|
|
548
615
|
' --target-month YYYY-MM Optional. Replace the target month.',
|
|
549
616
|
' --clear-target-month Optional. Remove the target month.',
|
|
550
617
|
' --achieved=true|false Optional. Mark the goal achieved or active.',
|
|
618
|
+
' --priority POSITION Optional. Positive whole-number position; 1 is highest.',
|
|
551
619
|
' --apply Optional. Write the partial update.',
|
|
552
620
|
' --base-url URL Optional. Override the API origin.',
|
|
553
621
|
' -h, --help Show this help.',
|
|
@@ -555,6 +623,11 @@ export function goalsUpdateHelpText() {
|
|
|
555
623
|
'',
|
|
556
624
|
'Constraints:',
|
|
557
625
|
' Provide at least one field to update.',
|
|
626
|
+
' Priority must be updated on its own.',
|
|
627
|
+
' Priority 1 is highest. The position cannot exceed the current goal count.',
|
|
628
|
+
' Moving a goal shifts the intervening goals automatically.',
|
|
629
|
+
' Forecast assignments and shared progress refresh when the owner next opens',
|
|
630
|
+
' the Forecast screen.',
|
|
558
631
|
' Set and clear options for the same field are mutually exclusive.',
|
|
559
632
|
' Marking a goal achieved removes its forecast assignment.',
|
|
560
633
|
' Marking it active again does not restore the previous assignment.',
|
|
@@ -567,6 +640,9 @@ export function goalsUpdateHelpText() {
|
|
|
567
640
|
' Without --apply, the command returns a dry-run preview and does not write.',
|
|
568
641
|
' Applying requires a write-enabled token created with Allow changes.',
|
|
569
642
|
'',
|
|
643
|
+
'Example:',
|
|
644
|
+
' sloth-agent goals update --goal-id goal-3 --priority 2 --apply',
|
|
645
|
+
'',
|
|
570
646
|
'Output:',
|
|
571
647
|
' Preview mode returns dryRun, method, endpoint, and payload.',
|
|
572
648
|
' Apply mode returns the complete persisted goal and currency.',
|
|
@@ -636,6 +712,8 @@ export function commandHelpText(topic) {
|
|
|
636
712
|
accounts: accountsHelpText,
|
|
637
713
|
'accounts-update': accountsUpdateHelpText,
|
|
638
714
|
investments: investmentsHelpText,
|
|
715
|
+
budget: budgetHelpText,
|
|
716
|
+
'budget-update': budgetUpdateHelpText,
|
|
639
717
|
categories: categoriesHelpText,
|
|
640
718
|
'categories-create': categoriesCreateHelpText,
|
|
641
719
|
'categories-rename': categoriesRenameHelpText,
|
|
@@ -655,6 +733,23 @@ export function commandHelpText(topic) {
|
|
|
655
733
|
function writeJson(write, data) {
|
|
656
734
|
write(`${JSON.stringify(data, null, 2)}\n`);
|
|
657
735
|
}
|
|
736
|
+
function withListedGoalPriorities(value) {
|
|
737
|
+
const response = value;
|
|
738
|
+
return {
|
|
739
|
+
...response,
|
|
740
|
+
goals: response.goals.map((goal, index) => ({
|
|
741
|
+
...goal,
|
|
742
|
+
priority: index + 1,
|
|
743
|
+
})),
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
function withUpdatedGoalPriority(value, priority) {
|
|
747
|
+
const response = value;
|
|
748
|
+
return {
|
|
749
|
+
...response,
|
|
750
|
+
goal: { ...response.goal, priority },
|
|
751
|
+
};
|
|
752
|
+
}
|
|
658
753
|
function redact(value, token) {
|
|
659
754
|
return token ? value.split(token).join('[REDACTED]') : value;
|
|
660
755
|
}
|
|
@@ -721,6 +816,15 @@ function readAssignmentFile(filePath) {
|
|
|
721
816
|
throw new UsageError(`Failed to read assignment JSON: ${message}`);
|
|
722
817
|
}
|
|
723
818
|
}
|
|
819
|
+
function readBudgetFile(filePath) {
|
|
820
|
+
try {
|
|
821
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
822
|
+
}
|
|
823
|
+
catch (error) {
|
|
824
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
825
|
+
throw new UsageError(`Failed to read budget JSON: ${message}`);
|
|
826
|
+
}
|
|
827
|
+
}
|
|
724
828
|
function buildTransactionsQuery(filters) {
|
|
725
829
|
const params = new URLSearchParams();
|
|
726
830
|
if (filters.uncategorized !== undefined) {
|
|
@@ -898,6 +1002,22 @@ export async function runCli(argv = process.argv.slice(2), options = {}) {
|
|
|
898
1002
|
});
|
|
899
1003
|
return 0;
|
|
900
1004
|
}
|
|
1005
|
+
const budgetUpdatePayload = parsed.command === 'budget-update'
|
|
1006
|
+
? validateBudgetUpdatePayload(readBudgetFile(parsed.input))
|
|
1007
|
+
: undefined;
|
|
1008
|
+
if (parsed.command === 'budget-update' && !parsed.apply) {
|
|
1009
|
+
writeJson(writeStdout, {
|
|
1010
|
+
dryRun: true,
|
|
1011
|
+
endpoint: `${baseUrl}/api/agent/v1/budgets`,
|
|
1012
|
+
method: 'PATCH',
|
|
1013
|
+
payload: {
|
|
1014
|
+
scope: parsed.scope,
|
|
1015
|
+
...(parsed.periodKey === undefined ? {} : { periodKey: parsed.periodKey }),
|
|
1016
|
+
...budgetUpdatePayload,
|
|
1017
|
+
},
|
|
1018
|
+
});
|
|
1019
|
+
return 0;
|
|
1020
|
+
}
|
|
901
1021
|
const credential = await resolveCredential(environment, baseUrl, getCredentialStore);
|
|
902
1022
|
token = credential.token;
|
|
903
1023
|
const headers = requestHeaders(token);
|
|
@@ -914,6 +1034,22 @@ export async function runCli(argv = process.argv.slice(2), options = {}) {
|
|
|
914
1034
|
writeJson(writeStdout, data);
|
|
915
1035
|
return 0;
|
|
916
1036
|
}
|
|
1037
|
+
if (parsed.command === 'budget-update') {
|
|
1038
|
+
const payload = {
|
|
1039
|
+
scope: parsed.scope,
|
|
1040
|
+
...(parsed.periodKey === undefined ? {} : { periodKey: parsed.periodKey }),
|
|
1041
|
+
...budgetUpdatePayload,
|
|
1042
|
+
};
|
|
1043
|
+
const response = await fetchImplementation(`${baseUrl}/api/agent/v1/budgets`, {
|
|
1044
|
+
method: 'PATCH',
|
|
1045
|
+
headers: { ...headers, 'Content-Type': 'application/json' },
|
|
1046
|
+
body: JSON.stringify(payload),
|
|
1047
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
1048
|
+
});
|
|
1049
|
+
const data = parseApiResponse(parsed.command, await parseHttpResponse(response, token));
|
|
1050
|
+
writeJson(writeStdout, data);
|
|
1051
|
+
return 0;
|
|
1052
|
+
}
|
|
917
1053
|
if (parsed.command === 'categories-create'
|
|
918
1054
|
|| parsed.command === 'categories-rename'
|
|
919
1055
|
|| parsed.command === 'line-items-create'
|
|
@@ -994,6 +1130,9 @@ export async function runCli(argv = process.argv.slice(2), options = {}) {
|
|
|
994
1130
|
...(parsed.isAchieved === undefined
|
|
995
1131
|
? {}
|
|
996
1132
|
: { isAchieved: parsed.isAchieved }),
|
|
1133
|
+
...(parsed.priority === undefined
|
|
1134
|
+
? {}
|
|
1135
|
+
: { priority: parsed.priority }),
|
|
997
1136
|
};
|
|
998
1137
|
if (!parsed.apply) {
|
|
999
1138
|
writeJson(writeStdout, {
|
|
@@ -1014,7 +1153,9 @@ export async function runCli(argv = process.argv.slice(2), options = {}) {
|
|
|
1014
1153
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
1015
1154
|
});
|
|
1016
1155
|
const data = parseApiResponse('goals-update', await parseHttpResponse(response, token));
|
|
1017
|
-
writeJson(writeStdout,
|
|
1156
|
+
writeJson(writeStdout, parsed.priority === undefined
|
|
1157
|
+
? data
|
|
1158
|
+
: withUpdatedGoalPriority(data, parsed.priority));
|
|
1018
1159
|
return 0;
|
|
1019
1160
|
}
|
|
1020
1161
|
if (parsed.command === 'goals-delete') {
|
|
@@ -1077,6 +1218,19 @@ export async function runCli(argv = process.argv.slice(2), options = {}) {
|
|
|
1077
1218
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
1078
1219
|
});
|
|
1079
1220
|
const data = parseApiResponse('goals-list', await parseHttpResponse(response, token));
|
|
1221
|
+
writeJson(writeStdout, withListedGoalPriorities(data));
|
|
1222
|
+
return 0;
|
|
1223
|
+
}
|
|
1224
|
+
if (parsed.command === 'budget') {
|
|
1225
|
+
const query = new URLSearchParams({ scope: parsed.scope });
|
|
1226
|
+
if (parsed.periodKey !== undefined)
|
|
1227
|
+
query.set('periodKey', parsed.periodKey);
|
|
1228
|
+
const response = await fetchImplementation(`${baseUrl}/api/agent/v1/budgets?${query.toString()}`, {
|
|
1229
|
+
method: 'GET',
|
|
1230
|
+
headers,
|
|
1231
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
1232
|
+
});
|
|
1233
|
+
const data = parseApiResponse('budget', await parseHttpResponse(response, token));
|
|
1080
1234
|
writeJson(writeStdout, data);
|
|
1081
1235
|
return 0;
|
|
1082
1236
|
}
|
package/dist/contracts.js
CHANGED
|
@@ -109,6 +109,36 @@ export function validateAssignmentPayload(value) {
|
|
|
109
109
|
}
|
|
110
110
|
return { assignments: payload.assignments.map(validateAssignment) };
|
|
111
111
|
}
|
|
112
|
+
export function validateBudgetUpdatePayload(value) {
|
|
113
|
+
const payload = requireObject(value, 'budget update payload');
|
|
114
|
+
rejectUnknownFields(payload, new Set(['allocations']), 'budget update payload');
|
|
115
|
+
if (!Array.isArray(payload.allocations)) {
|
|
116
|
+
throw new UsageError('allocations array is required');
|
|
117
|
+
}
|
|
118
|
+
if (payload.allocations.length < 1 || payload.allocations.length > 100) {
|
|
119
|
+
throw new UsageError('allocations must contain between 1 and 100 items');
|
|
120
|
+
}
|
|
121
|
+
const seen = new Set();
|
|
122
|
+
const allocations = payload.allocations.map((value, index) => {
|
|
123
|
+
const label = `allocations[${index}]`;
|
|
124
|
+
const allocation = requireObject(value, label);
|
|
125
|
+
rejectUnknownFields(allocation, new Set(['categoryId', 'lineItemId', 'plannedPence']), label);
|
|
126
|
+
const categoryId = requireString(allocation.categoryId, `${label}.categoryId`);
|
|
127
|
+
const lineItemId = requireString(allocation.lineItemId, `${label}.lineItemId`);
|
|
128
|
+
if (typeof allocation.plannedPence !== 'number'
|
|
129
|
+
|| !Number.isSafeInteger(allocation.plannedPence)
|
|
130
|
+
|| allocation.plannedPence < 0) {
|
|
131
|
+
throw new UsageError(`${label}.plannedPence must be a nonnegative safe integer`);
|
|
132
|
+
}
|
|
133
|
+
const key = `${categoryId}\u0000${lineItemId}`;
|
|
134
|
+
if (seen.has(key)) {
|
|
135
|
+
throw new UsageError(`${label} duplicates a categoryId and lineItemId pair`);
|
|
136
|
+
}
|
|
137
|
+
seen.add(key);
|
|
138
|
+
return { categoryId, lineItemId, plannedPence: allocation.plannedPence };
|
|
139
|
+
});
|
|
140
|
+
return { allocations };
|
|
141
|
+
}
|
|
112
142
|
function isLineItemMap(value) {
|
|
113
143
|
if (!isObject(value))
|
|
114
144
|
return false;
|
|
@@ -285,6 +315,61 @@ function isGoal(value) {
|
|
|
285
315
|
function isCurrency(value) {
|
|
286
316
|
return typeof value === 'string' && /^[A-Z]{3}$/.test(value);
|
|
287
317
|
}
|
|
318
|
+
function isNonnegativeSafeInteger(value) {
|
|
319
|
+
return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0;
|
|
320
|
+
}
|
|
321
|
+
function isSafeInteger(value) {
|
|
322
|
+
return typeof value === 'number' && Number.isSafeInteger(value);
|
|
323
|
+
}
|
|
324
|
+
function isBudgetLineItem(value) {
|
|
325
|
+
return (isObject(value)
|
|
326
|
+
&& hasOnlyFields(value, ['id', 'name', 'plannedPence'])
|
|
327
|
+
&& typeof value.id === 'string'
|
|
328
|
+
&& value.id.trim().length > 0
|
|
329
|
+
&& typeof value.name === 'string'
|
|
330
|
+
&& value.name.trim().length > 0
|
|
331
|
+
&& isNonnegativeSafeInteger(value.plannedPence));
|
|
332
|
+
}
|
|
333
|
+
function isBudgetCategory(value) {
|
|
334
|
+
return (isObject(value)
|
|
335
|
+
&& hasOnlyFields(value, ['id', 'name', 'plannedPence', 'assignedPence', 'lineItems'])
|
|
336
|
+
&& typeof value.id === 'string'
|
|
337
|
+
&& value.id.trim().length > 0
|
|
338
|
+
&& typeof value.name === 'string'
|
|
339
|
+
&& value.name.trim().length > 0
|
|
340
|
+
&& isNonnegativeSafeInteger(value.plannedPence)
|
|
341
|
+
&& (value.assignedPence === null || isSafeInteger(value.assignedPence))
|
|
342
|
+
&& Array.isArray(value.lineItems)
|
|
343
|
+
&& value.lineItems.every(isBudgetLineItem));
|
|
344
|
+
}
|
|
345
|
+
function isBudgetResponse(value) {
|
|
346
|
+
return (isObject(value)
|
|
347
|
+
&& hasOnlyFields(value, [
|
|
348
|
+
'scope',
|
|
349
|
+
'periodKey',
|
|
350
|
+
'periodStatus',
|
|
351
|
+
'currency',
|
|
352
|
+
'effectiveFromPeriodKey',
|
|
353
|
+
'funding',
|
|
354
|
+
'categories',
|
|
355
|
+
])
|
|
356
|
+
&& (value.scope === 'personal' || value.scope === 'joint')
|
|
357
|
+
&& typeof value.periodKey === 'string'
|
|
358
|
+
&& /^\d{4}-(0[1-9]|1[0-2])$/.test(value.periodKey)
|
|
359
|
+
&& (value.periodStatus === 'historical'
|
|
360
|
+
|| value.periodStatus === 'current'
|
|
361
|
+
|| value.periodStatus === 'future')
|
|
362
|
+
&& isCurrency(value.currency)
|
|
363
|
+
&& typeof value.effectiveFromPeriodKey === 'string'
|
|
364
|
+
&& /^\d{4}-(0[1-9]|1[0-2])$/.test(value.effectiveFromPeriodKey)
|
|
365
|
+
&& (value.funding === null
|
|
366
|
+
|| (isObject(value.funding)
|
|
367
|
+
&& hasOnlyFields(value.funding, ['toAssignPence', 'nextPeriodReservePence'])
|
|
368
|
+
&& isSafeInteger(value.funding.toAssignPence)
|
|
369
|
+
&& isSafeInteger(value.funding.nextPeriodReservePence)))
|
|
370
|
+
&& Array.isArray(value.categories)
|
|
371
|
+
&& value.categories.every(isBudgetCategory));
|
|
372
|
+
}
|
|
288
373
|
function isNullableNonEmptyString(value) {
|
|
289
374
|
return value === null || (typeof value === 'string'
|
|
290
375
|
&& value.length > 0
|
|
@@ -409,23 +494,25 @@ export function parseApiResponse(command, value) {
|
|
|
409
494
|
? isAccountMutationResponse(value)
|
|
410
495
|
: command === 'investments'
|
|
411
496
|
? isInvestmentsResponse(value)
|
|
412
|
-
: command === '
|
|
413
|
-
?
|
|
414
|
-
: command === 'categories
|
|
415
|
-
?
|
|
416
|
-
: command === '
|
|
417
|
-
?
|
|
418
|
-
: command === '
|
|
419
|
-
?
|
|
420
|
-
: command === '
|
|
421
|
-
?
|
|
422
|
-
: command === '
|
|
423
|
-
?
|
|
424
|
-
: command === '
|
|
425
|
-
?
|
|
426
|
-
: command === 'goals-
|
|
427
|
-
?
|
|
428
|
-
:
|
|
497
|
+
: command === 'budget' || command === 'budget-update'
|
|
498
|
+
? isBudgetResponse(value)
|
|
499
|
+
: command === 'categories'
|
|
500
|
+
? isCategoryResponse(value)
|
|
501
|
+
: command === 'categories-create' || command === 'categories-rename'
|
|
502
|
+
? isCategoryMutationResponse(value)
|
|
503
|
+
: command === 'line-items-create' || command === 'line-items-rename'
|
|
504
|
+
? isLineItemMutationResponse(value)
|
|
505
|
+
: command === 'transactions'
|
|
506
|
+
? isTransactionsResponse(value)
|
|
507
|
+
: command === 'assign'
|
|
508
|
+
? isAssignmentResponse(value)
|
|
509
|
+
: command === 'ask-partner'
|
|
510
|
+
? isPartnerResponse(value)
|
|
511
|
+
: command === 'goals-list'
|
|
512
|
+
? isGoalsResponse(value)
|
|
513
|
+
: command === 'goals-delete'
|
|
514
|
+
? isGoalDeleteResponse(value)
|
|
515
|
+
: isGoalMutationResponse(value);
|
|
429
516
|
if (!valid) {
|
|
430
517
|
const label = command === 'assign' ? 'assignment' : command;
|
|
431
518
|
throw new ApiError(`Invalid ${label} response from the Agent API`);
|