@slothmoney/agent-cli 0.7.0 → 0.9.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 -0
- package/README.md +37 -9
- package/dist/args.js +101 -6
- package/dist/cli.js +99 -13
- package/dist/contracts.js +31 -21
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.9.0 - 2026-08-12
|
|
6
|
+
|
|
7
|
+
- Manage owned manual balance and manual transaction accounts through partial
|
|
8
|
+
account updates, including metadata, ownership, balance-only settings, and
|
|
9
|
+
goal-savings membership where supported.
|
|
10
|
+
- Preview or apply idempotent manual account archival while retaining the
|
|
11
|
+
underlying account, transaction, import, balance, and categorisation records.
|
|
12
|
+
- Keep account writes local-only by default and require `--apply` before any
|
|
13
|
+
authenticated PATCH or DELETE request is sent.
|
|
14
|
+
|
|
15
|
+
## 0.8.0 - 2026-08-12
|
|
16
|
+
|
|
17
|
+
- Read each goal's one-based priority and move one goal to a new position with
|
|
18
|
+
automatic shifting of the intervening goals.
|
|
19
|
+
|
|
20
|
+
## 0.7.0 - 2026-08-09
|
|
21
|
+
|
|
5
22
|
- Read personal or joint budget periods with categories, line items, funding,
|
|
6
23
|
and planned amounts.
|
|
7
24
|
- Preview or apply planned line-item updates that overwrite the selected period
|
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.9.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,
|
|
30
|
+
or line items, 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
|
|
@@ -305,21 +305,38 @@ Missing values are JSON
|
|
|
305
305
|
are excluded, while enabled shared joint accounts follow Sloth's existing
|
|
306
306
|
visibility rules.
|
|
307
307
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
308
|
+
Account changes are previews unless `--apply` is present. Connected accounts
|
|
309
|
+
support only goal-savings membership. Manual current accounts support their
|
|
310
|
+
institution, name, currency, and ownership. Manual balance accounts also
|
|
311
|
+
support balance, Savings/Investments type, and goal-savings membership.
|
|
312
|
+
Partner-owned shared accounts return an explanatory error.
|
|
311
313
|
|
|
312
314
|
```bash
|
|
313
315
|
sloth-agent accounts update \
|
|
314
316
|
--account-ref sloth_account_v1_... \
|
|
315
|
-
--
|
|
317
|
+
--institution-name "Hargreaves Lansdown" \
|
|
318
|
+
--account-name "Stocks & Shares ISA" \
|
|
319
|
+
--currency GBP \
|
|
320
|
+
--ownership individual \
|
|
321
|
+
--balance-amount 12500.75 \
|
|
322
|
+
--account-type investments \
|
|
323
|
+
--goal-savings-source false
|
|
316
324
|
|
|
317
325
|
sloth-agent accounts update \
|
|
318
326
|
--account-ref sloth_account_v1_... \
|
|
319
|
-
--goal-savings-source
|
|
327
|
+
--goal-savings-source false \
|
|
320
328
|
--apply
|
|
321
329
|
```
|
|
322
330
|
|
|
331
|
+
Archive an owned manual account. The account disappears from active Sloth
|
|
332
|
+
surfaces, but its underlying records are retained. Repeating an applied removal
|
|
333
|
+
is safe and returns `changed: false`.
|
|
334
|
+
|
|
335
|
+
```bash
|
|
336
|
+
sloth-agent accounts remove --account-ref sloth_account_v1_...
|
|
337
|
+
sloth-agent accounts remove --account-ref sloth_account_v1_... --apply
|
|
338
|
+
```
|
|
339
|
+
|
|
323
340
|
Read linked investment accounts and their cached holdings:
|
|
324
341
|
|
|
325
342
|
```bash
|
|
@@ -355,7 +372,7 @@ sloth-agent goals create \
|
|
|
355
372
|
--apply
|
|
356
373
|
```
|
|
357
374
|
|
|
358
|
-
Use the `id` from list or create output to update or delete
|
|
375
|
+
Use the `id` from list or create output to update or delete goals:
|
|
359
376
|
|
|
360
377
|
```bash
|
|
361
378
|
sloth-agent goals update \
|
|
@@ -365,6 +382,11 @@ sloth-agent goals update \
|
|
|
365
382
|
--achieved=false \
|
|
366
383
|
--apply
|
|
367
384
|
|
|
385
|
+
sloth-agent goals update \
|
|
386
|
+
--goal-id house-goal-id \
|
|
387
|
+
--priority 2 \
|
|
388
|
+
--apply
|
|
389
|
+
|
|
368
390
|
sloth-agent goals delete --goal-id goal-id --apply
|
|
369
391
|
```
|
|
370
392
|
|
|
@@ -373,7 +395,13 @@ remove an optional value. Marking a goal achieved removes its forecast
|
|
|
373
395
|
assignment. Deleting a goal also removes its forecast assignments and drift
|
|
374
396
|
history. Goal sharing remains app-managed. Change an active shared goal's
|
|
375
397
|
pot-tracked target amount in the Sloth Budget app, where account balances can
|
|
376
|
-
be reallocated across goals in priority order.
|
|
398
|
+
be reallocated across goals in priority order. Goal list output includes a
|
|
399
|
+
one-based `priority`; `1` is highest. Moving one goal automatically shifts the
|
|
400
|
+
goals between its old and new positions. The
|
|
401
|
+
priority option must be used on its own, and the write persists immediately.
|
|
402
|
+
Forecast assignments and shared pot
|
|
403
|
+
progress are browser-owned derived state and refresh when the owner next opens
|
|
404
|
+
the Forecast screen.
|
|
377
405
|
|
|
378
406
|
Read uncategorised contributions to the joint budget:
|
|
379
407
|
|
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`);
|
|
@@ -232,6 +242,19 @@ function parseAccountRef(value) {
|
|
|
232
242
|
}
|
|
233
243
|
return value;
|
|
234
244
|
}
|
|
245
|
+
function parseAccountName(value, option) {
|
|
246
|
+
const name = value.trim();
|
|
247
|
+
if (name.length > 300)
|
|
248
|
+
throw new UsageError(`${option} must be at most 300 characters`);
|
|
249
|
+
return name;
|
|
250
|
+
}
|
|
251
|
+
function parseAccountBalance(value) {
|
|
252
|
+
const amount = Number(value);
|
|
253
|
+
if (!/^\d+(?:\.\d+)?$/.test(value) || !Number.isFinite(amount) || amount < 0) {
|
|
254
|
+
throw new UsageError('--balance-amount must be a nonnegative amount');
|
|
255
|
+
}
|
|
256
|
+
return amount;
|
|
257
|
+
}
|
|
235
258
|
function parseAccounts(args, baseUrl) {
|
|
236
259
|
const subcommand = args.shift();
|
|
237
260
|
if (subcommand === undefined || subcommand === 'list') {
|
|
@@ -240,15 +263,71 @@ function parseAccounts(args, baseUrl) {
|
|
|
240
263
|
return withBaseUrl({ command: 'accounts' }, baseUrl);
|
|
241
264
|
}
|
|
242
265
|
if (subcommand === 'update') {
|
|
243
|
-
const { values, apply } = parseNamedOptions(args, 'accounts update', new Set([
|
|
244
|
-
|
|
245
|
-
|
|
266
|
+
const { values, apply } = parseNamedOptions(args, 'accounts update', new Set([
|
|
267
|
+
'--account-ref',
|
|
268
|
+
'--institution-name',
|
|
269
|
+
'--account-name',
|
|
270
|
+
'--currency',
|
|
271
|
+
'--ownership',
|
|
272
|
+
'--balance-amount',
|
|
273
|
+
'--account-type',
|
|
274
|
+
'--goal-savings-source',
|
|
275
|
+
]));
|
|
276
|
+
const institutionName = values.get('--institution-name');
|
|
277
|
+
const accountName = values.get('--account-name');
|
|
278
|
+
const currencyValue = values.get('--currency');
|
|
279
|
+
const ownershipValue = values.get('--ownership');
|
|
280
|
+
const balanceValue = values.get('--balance-amount');
|
|
281
|
+
const accountTypeValue = values.get('--account-type');
|
|
282
|
+
const sourceValue = values.get('--goal-savings-source');
|
|
283
|
+
if (currencyValue !== undefined && !/^[A-Za-z]{3}$/.test(currencyValue)) {
|
|
284
|
+
throw new UsageError('--currency must be a three-letter currency code');
|
|
285
|
+
}
|
|
286
|
+
if (ownershipValue !== undefined
|
|
287
|
+
&& ownershipValue !== 'individual'
|
|
288
|
+
&& ownershipValue !== 'joint') {
|
|
289
|
+
throw new UsageError('--ownership must be individual or joint');
|
|
290
|
+
}
|
|
291
|
+
if (accountTypeValue !== undefined
|
|
292
|
+
&& accountTypeValue !== 'savings'
|
|
293
|
+
&& accountTypeValue !== 'investments') {
|
|
294
|
+
throw new UsageError('--account-type must be savings or investments');
|
|
295
|
+
}
|
|
296
|
+
if (sourceValue !== undefined && sourceValue !== 'true' && sourceValue !== 'false') {
|
|
246
297
|
throw new UsageError('--goal-savings-source must be true or false');
|
|
247
298
|
}
|
|
299
|
+
const update = {
|
|
300
|
+
...(institutionName === undefined
|
|
301
|
+
? {}
|
|
302
|
+
: { institutionName: parseAccountName(institutionName, '--institution-name') }),
|
|
303
|
+
...(accountName === undefined
|
|
304
|
+
? {}
|
|
305
|
+
: { accountName: parseAccountName(accountName, '--account-name') }),
|
|
306
|
+
...(currencyValue === undefined ? {} : { currency: currencyValue.toUpperCase() }),
|
|
307
|
+
...(ownershipValue === undefined
|
|
308
|
+
? {}
|
|
309
|
+
: { ownership: ownershipValue === 'individual' ? 'personal' : 'joint' }),
|
|
310
|
+
...(balanceValue === undefined ? {} : { balanceAmount: parseAccountBalance(balanceValue) }),
|
|
311
|
+
...(accountTypeValue === undefined
|
|
312
|
+
? {}
|
|
313
|
+
: { accountType: accountTypeValue }),
|
|
314
|
+
...(sourceValue === undefined ? {} : { isGoalSavingsSource: sourceValue === 'true' }),
|
|
315
|
+
};
|
|
316
|
+
if (Object.keys(update).length === 0) {
|
|
317
|
+
throw new UsageError('accounts update requires at least one field to update');
|
|
318
|
+
}
|
|
248
319
|
return withBaseUrl({
|
|
249
320
|
command: 'accounts-update',
|
|
250
321
|
accountRef: parseAccountRef(requiredOption(values, '--account-ref', 'accounts update')),
|
|
251
|
-
|
|
322
|
+
update,
|
|
323
|
+
apply,
|
|
324
|
+
}, baseUrl);
|
|
325
|
+
}
|
|
326
|
+
if (subcommand === 'remove') {
|
|
327
|
+
const { values, apply } = parseNamedOptions(args, 'accounts remove', new Set(['--account-ref']));
|
|
328
|
+
return withBaseUrl({
|
|
329
|
+
command: 'accounts-remove',
|
|
330
|
+
accountRef: parseAccountRef(requiredOption(values, '--account-ref', 'accounts remove')),
|
|
252
331
|
apply,
|
|
253
332
|
}, baseUrl);
|
|
254
333
|
}
|
|
@@ -467,6 +546,7 @@ function parseGoals(args, baseUrl) {
|
|
|
467
546
|
let targetAmount;
|
|
468
547
|
let targetMonthKey;
|
|
469
548
|
let isAchieved;
|
|
549
|
+
let priority;
|
|
470
550
|
let apply = false;
|
|
471
551
|
for (let index = 0; index < args.length; index += 1) {
|
|
472
552
|
const argument = args[index];
|
|
@@ -497,7 +577,8 @@ function parseGoals(args, baseUrl) {
|
|
|
497
577
|
&& option !== '--name'
|
|
498
578
|
&& option !== '--target-amount'
|
|
499
579
|
&& option !== '--target-month'
|
|
500
|
-
&& option !== '--achieved'
|
|
580
|
+
&& option !== '--achieved'
|
|
581
|
+
&& option !== '--priority') {
|
|
501
582
|
throw new UsageError(`Unknown goals update option: ${argument}`);
|
|
502
583
|
}
|
|
503
584
|
const value = requireNonEmpty(inlineValue ?? readOptionValue(args, index, option), option);
|
|
@@ -521,6 +602,9 @@ function parseGoals(args, baseUrl) {
|
|
|
521
602
|
}
|
|
522
603
|
targetMonthKey = parseGoalMonthKey(value, option);
|
|
523
604
|
}
|
|
605
|
+
else if (option === '--priority') {
|
|
606
|
+
priority = setOnce(priority, parseGoalPriority(value), option);
|
|
607
|
+
}
|
|
524
608
|
else {
|
|
525
609
|
isAchieved = setOnce(isAchieved, parseExplicitBoolean(value, option), option);
|
|
526
610
|
}
|
|
@@ -530,9 +614,17 @@ function parseGoals(args, baseUrl) {
|
|
|
530
614
|
if (name === undefined
|
|
531
615
|
&& targetAmount === undefined
|
|
532
616
|
&& targetMonthKey === undefined
|
|
533
|
-
&& isAchieved === undefined
|
|
617
|
+
&& isAchieved === undefined
|
|
618
|
+
&& priority === undefined) {
|
|
534
619
|
throw new UsageError('goals update requires at least one field to update');
|
|
535
620
|
}
|
|
621
|
+
if (priority !== undefined
|
|
622
|
+
&& (name !== undefined
|
|
623
|
+
|| targetAmount !== undefined
|
|
624
|
+
|| targetMonthKey !== undefined
|
|
625
|
+
|| isAchieved !== undefined)) {
|
|
626
|
+
throw new UsageError('--priority must be used on its own');
|
|
627
|
+
}
|
|
536
628
|
return withBaseUrl({
|
|
537
629
|
command: 'goals-update',
|
|
538
630
|
goalId,
|
|
@@ -540,6 +632,7 @@ function parseGoals(args, baseUrl) {
|
|
|
540
632
|
...(targetAmount === undefined ? {} : { targetAmount }),
|
|
541
633
|
...(targetMonthKey === undefined ? {} : { targetMonthKey }),
|
|
542
634
|
...(isAchieved === undefined ? {} : { isAchieved }),
|
|
635
|
+
...(priority === undefined ? {} : { priority }),
|
|
543
636
|
apply,
|
|
544
637
|
}, baseUrl);
|
|
545
638
|
}
|
|
@@ -633,6 +726,8 @@ function helpTopic(argv) {
|
|
|
633
726
|
|| command === 'ask-partner') {
|
|
634
727
|
if (command === 'accounts' && subcommand === 'update')
|
|
635
728
|
return 'accounts-update';
|
|
729
|
+
if (command === 'accounts' && subcommand === 'remove')
|
|
730
|
+
return 'accounts-remove';
|
|
636
731
|
return command;
|
|
637
732
|
}
|
|
638
733
|
if (command === 'investments')
|
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.9.0';
|
|
8
8
|
const REQUEST_TIMEOUT_MS = 60_000;
|
|
9
9
|
const API_ORIGIN_HELP_LINES = [
|
|
10
10
|
'',
|
|
@@ -26,7 +26,8 @@ export function usageText() {
|
|
|
26
26
|
' sloth-agent auth status [--base-url URL]',
|
|
27
27
|
' sloth-agent auth logout [--base-url URL]',
|
|
28
28
|
' sloth-agent accounts [list] [--base-url URL]',
|
|
29
|
-
' sloth-agent accounts update --account-ref REF
|
|
29
|
+
' sloth-agent accounts update --account-ref REF [fields] [--apply]',
|
|
30
|
+
' sloth-agent accounts remove --account-ref REF [--apply]',
|
|
30
31
|
' sloth-agent investments [--account-ref REF] [--base-url URL]',
|
|
31
32
|
' sloth-agent budget --scope personal|joint [--period YYYY-MM] [--base-url URL]',
|
|
32
33
|
' sloth-agent budget update --scope personal|joint [--period YYYY-MM]',
|
|
@@ -308,19 +309,29 @@ export function accountsUpdateHelpText() {
|
|
|
308
309
|
return [
|
|
309
310
|
'Sloth Agent CLI — accounts update',
|
|
310
311
|
'',
|
|
311
|
-
'Preview or update
|
|
312
|
+
'Preview or update an owned account. Manual accounts support their editable fields.',
|
|
312
313
|
'',
|
|
313
314
|
'Usage:',
|
|
314
|
-
' sloth-agent accounts update --account-ref REF
|
|
315
|
+
' sloth-agent accounts update --account-ref REF [fields] [--apply] [--base-url URL]',
|
|
315
316
|
'',
|
|
316
|
-
'Required
|
|
317
|
+
'Required input:',
|
|
317
318
|
' --account-ref REF Opaque accountRef from sloth-agent accounts.',
|
|
318
|
-
'
|
|
319
|
+
'',
|
|
320
|
+
'Update fields (at least one):',
|
|
321
|
+
' --institution-name NAME Manual account institution.',
|
|
322
|
+
' --account-name NAME Manual account name.',
|
|
323
|
+
' --currency CODE Three-letter currency code.',
|
|
324
|
+
' --ownership individual|joint Manual account ownership.',
|
|
325
|
+
' --balance-amount AMOUNT Balance-only account balance.',
|
|
326
|
+
' --account-type savings|investments Balance-only account type.',
|
|
327
|
+
' --goal-savings-source true|false Goal-savings membership.',
|
|
319
328
|
'',
|
|
320
329
|
'Write behavior:',
|
|
321
330
|
' Without --apply, returns a JSON preview without credentials or a network request.',
|
|
322
331
|
' With --apply, requires agent:write on a write-enabled token and updates saved Sloth metadata.',
|
|
323
|
-
'
|
|
332
|
+
' Connected accounts support only --goal-savings-source.',
|
|
333
|
+
' Manual current accounts cannot change type, balance, or goal-savings membership.',
|
|
334
|
+
' Partner-owned shared accounts cannot be changed.',
|
|
324
335
|
' Unknown, disconnected, or inaccessible references return Account not found.',
|
|
325
336
|
...API_ORIGIN_HELP_LINES,
|
|
326
337
|
'',
|
|
@@ -329,6 +340,30 @@ export function accountsUpdateHelpText() {
|
|
|
329
340
|
' Apply mode returns changed and the complete persisted account.',
|
|
330
341
|
].join('\n');
|
|
331
342
|
}
|
|
343
|
+
export function accountsRemoveHelpText() {
|
|
344
|
+
return [
|
|
345
|
+
'Sloth Agent CLI — accounts remove',
|
|
346
|
+
'',
|
|
347
|
+
'Preview or archive an owned manual account while retaining its underlying records.',
|
|
348
|
+
'',
|
|
349
|
+
'Usage:',
|
|
350
|
+
' sloth-agent accounts remove --account-ref REF [--apply] [--base-url URL]',
|
|
351
|
+
'',
|
|
352
|
+
'Required input:',
|
|
353
|
+
' --account-ref REF Opaque accountRef from sloth-agent accounts.',
|
|
354
|
+
'',
|
|
355
|
+
'Write behavior:',
|
|
356
|
+
' Without --apply, returns a JSON preview without credentials or a network request.',
|
|
357
|
+
' With --apply, requires agent:write and archives the manual account.',
|
|
358
|
+
' Connected and partner-owned accounts cannot be removed.',
|
|
359
|
+
' Repeating an applied removal succeeds with changed false.',
|
|
360
|
+
...API_ORIGIN_HELP_LINES,
|
|
361
|
+
'',
|
|
362
|
+
'Output:',
|
|
363
|
+
' Preview mode returns dryRun, method, and endpoint.',
|
|
364
|
+
' Apply mode returns removed, changed, and accountRef.',
|
|
365
|
+
].join('\n');
|
|
366
|
+
}
|
|
332
367
|
export function investmentsHelpText() {
|
|
333
368
|
return [
|
|
334
369
|
'Sloth Agent CLI — investments',
|
|
@@ -562,7 +597,7 @@ export function goalsListHelpText() {
|
|
|
562
597
|
' This command is read-only.',
|
|
563
598
|
'',
|
|
564
599
|
'Output:',
|
|
565
|
-
' JSON containing currency and goals. Each goal contains id, name,',
|
|
600
|
+
' JSON containing currency and goals. Each goal contains id, name, priority,',
|
|
566
601
|
' targetAmount, targetMonthKey, isAchieved, and sharedWithPartner.',
|
|
567
602
|
].join('\n');
|
|
568
603
|
}
|
|
@@ -615,6 +650,7 @@ export function goalsUpdateHelpText() {
|
|
|
615
650
|
' --target-month YYYY-MM Optional. Replace the target month.',
|
|
616
651
|
' --clear-target-month Optional. Remove the target month.',
|
|
617
652
|
' --achieved=true|false Optional. Mark the goal achieved or active.',
|
|
653
|
+
' --priority POSITION Optional. Positive whole-number position; 1 is highest.',
|
|
618
654
|
' --apply Optional. Write the partial update.',
|
|
619
655
|
' --base-url URL Optional. Override the API origin.',
|
|
620
656
|
' -h, --help Show this help.',
|
|
@@ -622,6 +658,11 @@ export function goalsUpdateHelpText() {
|
|
|
622
658
|
'',
|
|
623
659
|
'Constraints:',
|
|
624
660
|
' Provide at least one field to update.',
|
|
661
|
+
' Priority must be updated on its own.',
|
|
662
|
+
' Priority 1 is highest. The position cannot exceed the current goal count.',
|
|
663
|
+
' Moving a goal shifts the intervening goals automatically.',
|
|
664
|
+
' Forecast assignments and shared progress refresh when the owner next opens',
|
|
665
|
+
' the Forecast screen.',
|
|
625
666
|
' Set and clear options for the same field are mutually exclusive.',
|
|
626
667
|
' Marking a goal achieved removes its forecast assignment.',
|
|
627
668
|
' Marking it active again does not restore the previous assignment.',
|
|
@@ -634,6 +675,9 @@ export function goalsUpdateHelpText() {
|
|
|
634
675
|
' Without --apply, the command returns a dry-run preview and does not write.',
|
|
635
676
|
' Applying requires a write-enabled token created with Allow changes.',
|
|
636
677
|
'',
|
|
678
|
+
'Example:',
|
|
679
|
+
' sloth-agent goals update --goal-id goal-3 --priority 2 --apply',
|
|
680
|
+
'',
|
|
637
681
|
'Output:',
|
|
638
682
|
' Preview mode returns dryRun, method, endpoint, and payload.',
|
|
639
683
|
' Apply mode returns the complete persisted goal and currency.',
|
|
@@ -702,6 +746,7 @@ export function commandHelpText(topic) {
|
|
|
702
746
|
'auth-logout': authLogoutHelpText,
|
|
703
747
|
accounts: accountsHelpText,
|
|
704
748
|
'accounts-update': accountsUpdateHelpText,
|
|
749
|
+
'accounts-remove': accountsRemoveHelpText,
|
|
705
750
|
investments: investmentsHelpText,
|
|
706
751
|
budget: budgetHelpText,
|
|
707
752
|
'budget-update': budgetUpdateHelpText,
|
|
@@ -724,6 +769,23 @@ export function commandHelpText(topic) {
|
|
|
724
769
|
function writeJson(write, data) {
|
|
725
770
|
write(`${JSON.stringify(data, null, 2)}\n`);
|
|
726
771
|
}
|
|
772
|
+
function withListedGoalPriorities(value) {
|
|
773
|
+
const response = value;
|
|
774
|
+
return {
|
|
775
|
+
...response,
|
|
776
|
+
goals: response.goals.map((goal, index) => ({
|
|
777
|
+
...goal,
|
|
778
|
+
priority: index + 1,
|
|
779
|
+
})),
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
function withUpdatedGoalPriority(value, priority) {
|
|
783
|
+
const response = value;
|
|
784
|
+
return {
|
|
785
|
+
...response,
|
|
786
|
+
goal: { ...response.goal, priority },
|
|
787
|
+
};
|
|
788
|
+
}
|
|
727
789
|
function redact(value, token) {
|
|
728
790
|
return token ? value.split(token).join('[REDACTED]') : value;
|
|
729
791
|
}
|
|
@@ -972,7 +1034,16 @@ export async function runCli(argv = process.argv.slice(2), options = {}) {
|
|
|
972
1034
|
dryRun: true,
|
|
973
1035
|
endpoint,
|
|
974
1036
|
method: 'PATCH',
|
|
975
|
-
payload:
|
|
1037
|
+
payload: parsed.update,
|
|
1038
|
+
});
|
|
1039
|
+
return 0;
|
|
1040
|
+
}
|
|
1041
|
+
if (parsed.command === 'accounts-remove' && !parsed.apply) {
|
|
1042
|
+
const endpoint = `${baseUrl}/api/agent/v1/accounts/${encodeURIComponent(parsed.accountRef)}`;
|
|
1043
|
+
writeJson(writeStdout, {
|
|
1044
|
+
dryRun: true,
|
|
1045
|
+
endpoint,
|
|
1046
|
+
method: 'DELETE',
|
|
976
1047
|
});
|
|
977
1048
|
return 0;
|
|
978
1049
|
}
|
|
@@ -997,11 +1068,21 @@ export async function runCli(argv = process.argv.slice(2), options = {}) {
|
|
|
997
1068
|
const headers = requestHeaders(token);
|
|
998
1069
|
if (parsed.command === 'accounts-update') {
|
|
999
1070
|
const endpoint = `${baseUrl}/api/agent/v1/accounts/${encodeURIComponent(parsed.accountRef)}`;
|
|
1000
|
-
const payload = { isGoalSavingsSource: parsed.isGoalSavingsSource };
|
|
1001
1071
|
const response = await fetchImplementation(endpoint, {
|
|
1002
1072
|
method: 'PATCH',
|
|
1003
1073
|
headers: { ...headers, 'Content-Type': 'application/json' },
|
|
1004
|
-
body: JSON.stringify(
|
|
1074
|
+
body: JSON.stringify(parsed.update),
|
|
1075
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
1076
|
+
});
|
|
1077
|
+
const data = parseApiResponse(parsed.command, await parseHttpResponse(response, token));
|
|
1078
|
+
writeJson(writeStdout, data);
|
|
1079
|
+
return 0;
|
|
1080
|
+
}
|
|
1081
|
+
if (parsed.command === 'accounts-remove') {
|
|
1082
|
+
const endpoint = `${baseUrl}/api/agent/v1/accounts/${encodeURIComponent(parsed.accountRef)}`;
|
|
1083
|
+
const response = await fetchImplementation(endpoint, {
|
|
1084
|
+
method: 'DELETE',
|
|
1085
|
+
headers,
|
|
1005
1086
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
1006
1087
|
});
|
|
1007
1088
|
const data = parseApiResponse(parsed.command, await parseHttpResponse(response, token));
|
|
@@ -1104,6 +1185,9 @@ export async function runCli(argv = process.argv.slice(2), options = {}) {
|
|
|
1104
1185
|
...(parsed.isAchieved === undefined
|
|
1105
1186
|
? {}
|
|
1106
1187
|
: { isAchieved: parsed.isAchieved }),
|
|
1188
|
+
...(parsed.priority === undefined
|
|
1189
|
+
? {}
|
|
1190
|
+
: { priority: parsed.priority }),
|
|
1107
1191
|
};
|
|
1108
1192
|
if (!parsed.apply) {
|
|
1109
1193
|
writeJson(writeStdout, {
|
|
@@ -1124,7 +1208,9 @@ export async function runCli(argv = process.argv.slice(2), options = {}) {
|
|
|
1124
1208
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
1125
1209
|
});
|
|
1126
1210
|
const data = parseApiResponse('goals-update', await parseHttpResponse(response, token));
|
|
1127
|
-
writeJson(writeStdout,
|
|
1211
|
+
writeJson(writeStdout, parsed.priority === undefined
|
|
1212
|
+
? data
|
|
1213
|
+
: withUpdatedGoalPriority(data, parsed.priority));
|
|
1128
1214
|
return 0;
|
|
1129
1215
|
}
|
|
1130
1216
|
if (parsed.command === 'goals-delete') {
|
|
@@ -1187,7 +1273,7 @@ export async function runCli(argv = process.argv.slice(2), options = {}) {
|
|
|
1187
1273
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
1188
1274
|
});
|
|
1189
1275
|
const data = parseApiResponse('goals-list', await parseHttpResponse(response, token));
|
|
1190
|
-
writeJson(writeStdout, data);
|
|
1276
|
+
writeJson(writeStdout, withListedGoalPriorities(data));
|
|
1191
1277
|
return 0;
|
|
1192
1278
|
}
|
|
1193
1279
|
if (parsed.command === 'budget') {
|
package/dist/contracts.js
CHANGED
|
@@ -423,6 +423,14 @@ function isAccountMutationResponse(value) {
|
|
|
423
423
|
&& typeof value.changed === 'boolean'
|
|
424
424
|
&& isAccount(value.account));
|
|
425
425
|
}
|
|
426
|
+
function isAccountRemovalResponse(value) {
|
|
427
|
+
return (isObject(value)
|
|
428
|
+
&& hasOnlyFields(value, ['removed', 'changed', 'accountRef'])
|
|
429
|
+
&& value.removed === true
|
|
430
|
+
&& typeof value.changed === 'boolean'
|
|
431
|
+
&& typeof value.accountRef === 'string'
|
|
432
|
+
&& /^sloth_account_v1_[A-Za-z0-9_-]{43}$/.test(value.accountRef));
|
|
433
|
+
}
|
|
426
434
|
function isInvestmentHolding(value) {
|
|
427
435
|
return (isObject(value)
|
|
428
436
|
&& hasOnlyFields(value, [
|
|
@@ -492,27 +500,29 @@ export function parseApiResponse(command, value) {
|
|
|
492
500
|
? isAccountsResponse(value)
|
|
493
501
|
: command === 'accounts-update'
|
|
494
502
|
? isAccountMutationResponse(value)
|
|
495
|
-
: command === '
|
|
496
|
-
?
|
|
497
|
-
: command === '
|
|
498
|
-
?
|
|
499
|
-
: command === '
|
|
500
|
-
?
|
|
501
|
-
: command === 'categories
|
|
502
|
-
?
|
|
503
|
-
: command === '
|
|
504
|
-
?
|
|
505
|
-
: command === '
|
|
506
|
-
?
|
|
507
|
-
: command === '
|
|
508
|
-
?
|
|
509
|
-
: command === '
|
|
510
|
-
?
|
|
511
|
-
: command === '
|
|
512
|
-
?
|
|
513
|
-
: command === 'goals-
|
|
514
|
-
?
|
|
515
|
-
:
|
|
503
|
+
: command === 'accounts-remove'
|
|
504
|
+
? isAccountRemovalResponse(value)
|
|
505
|
+
: command === 'investments'
|
|
506
|
+
? isInvestmentsResponse(value)
|
|
507
|
+
: command === 'budget' || command === 'budget-update'
|
|
508
|
+
? 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);
|
|
516
526
|
if (!valid) {
|
|
517
527
|
const label = command === 'assign' ? 'assignment' : command;
|
|
518
528
|
throw new ApiError(`Invalid ${label} response from the Agent API`);
|