@slothmoney/agent-cli 0.8.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 +10 -0
- package/README.md +24 -7
- package/dist/args.js +75 -4
- package/dist/cli.js +65 -10
- package/dist/contracts.js +31 -21
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
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
|
+
|
|
5
15
|
## 0.8.0 - 2026-08-12
|
|
6
16
|
|
|
7
17
|
- Read each goal's one-based priority and move one goal to a new position with
|
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
|
package/dist/args.js
CHANGED
|
@@ -242,6 +242,19 @@ function parseAccountRef(value) {
|
|
|
242
242
|
}
|
|
243
243
|
return value;
|
|
244
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
|
+
}
|
|
245
258
|
function parseAccounts(args, baseUrl) {
|
|
246
259
|
const subcommand = args.shift();
|
|
247
260
|
if (subcommand === undefined || subcommand === 'list') {
|
|
@@ -250,15 +263,71 @@ function parseAccounts(args, baseUrl) {
|
|
|
250
263
|
return withBaseUrl({ command: 'accounts' }, baseUrl);
|
|
251
264
|
}
|
|
252
265
|
if (subcommand === 'update') {
|
|
253
|
-
const { values, apply } = parseNamedOptions(args, 'accounts update', new Set([
|
|
254
|
-
|
|
255
|
-
|
|
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') {
|
|
256
297
|
throw new UsageError('--goal-savings-source must be true or false');
|
|
257
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
|
+
}
|
|
258
319
|
return withBaseUrl({
|
|
259
320
|
command: 'accounts-update',
|
|
260
321
|
accountRef: parseAccountRef(requiredOption(values, '--account-ref', 'accounts update')),
|
|
261
|
-
|
|
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')),
|
|
262
331
|
apply,
|
|
263
332
|
}, baseUrl);
|
|
264
333
|
}
|
|
@@ -657,6 +726,8 @@ function helpTopic(argv) {
|
|
|
657
726
|
|| command === 'ask-partner') {
|
|
658
727
|
if (command === 'accounts' && subcommand === 'update')
|
|
659
728
|
return 'accounts-update';
|
|
729
|
+
if (command === 'accounts' && subcommand === 'remove')
|
|
730
|
+
return 'accounts-remove';
|
|
660
731
|
return command;
|
|
661
732
|
}
|
|
662
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',
|
|
@@ -711,6 +746,7 @@ export function commandHelpText(topic) {
|
|
|
711
746
|
'auth-logout': authLogoutHelpText,
|
|
712
747
|
accounts: accountsHelpText,
|
|
713
748
|
'accounts-update': accountsUpdateHelpText,
|
|
749
|
+
'accounts-remove': accountsRemoveHelpText,
|
|
714
750
|
investments: investmentsHelpText,
|
|
715
751
|
budget: budgetHelpText,
|
|
716
752
|
'budget-update': budgetUpdateHelpText,
|
|
@@ -998,7 +1034,16 @@ export async function runCli(argv = process.argv.slice(2), options = {}) {
|
|
|
998
1034
|
dryRun: true,
|
|
999
1035
|
endpoint,
|
|
1000
1036
|
method: 'PATCH',
|
|
1001
|
-
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',
|
|
1002
1047
|
});
|
|
1003
1048
|
return 0;
|
|
1004
1049
|
}
|
|
@@ -1023,11 +1068,21 @@ export async function runCli(argv = process.argv.slice(2), options = {}) {
|
|
|
1023
1068
|
const headers = requestHeaders(token);
|
|
1024
1069
|
if (parsed.command === 'accounts-update') {
|
|
1025
1070
|
const endpoint = `${baseUrl}/api/agent/v1/accounts/${encodeURIComponent(parsed.accountRef)}`;
|
|
1026
|
-
const payload = { isGoalSavingsSource: parsed.isGoalSavingsSource };
|
|
1027
1071
|
const response = await fetchImplementation(endpoint, {
|
|
1028
1072
|
method: 'PATCH',
|
|
1029
1073
|
headers: { ...headers, 'Content-Type': 'application/json' },
|
|
1030
|
-
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,
|
|
1031
1086
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
1032
1087
|
});
|
|
1033
1088
|
const data = parseApiResponse(parsed.command, await parseHttpResponse(response, token));
|
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`);
|