@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 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.8.0 -- sloth-agent --help
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, change goal-savings account membership, ask a partner for an explanation, or manage goals. Token
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
- Goal-savings changes are previews unless `--apply` is present. Only
309
- caller-owned connected accounts can be changed; partner-owned shared accounts
310
- and fixed manual accounts return an explanatory error.
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
- --goal-savings-source true
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 true \
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(['--account-ref', '--goal-savings-source']));
254
- const source = requiredOption(values, '--goal-savings-source', 'accounts update');
255
- if (source !== 'true' && source !== 'false') {
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
- isGoalSavingsSource: source === 'true',
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.8.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 --goal-savings-source true|false [--apply]',
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 whether an owned connected account is used for goal savings.',
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 --goal-savings-source true|false [--apply] [--base-url URL]',
315
+ ' sloth-agent accounts update --account-ref REF [fields] [--apply] [--base-url URL]',
315
316
  '',
316
- 'Required inputs:',
317
+ 'Required input:',
317
318
  ' --account-ref REF Opaque accountRef from sloth-agent accounts.',
318
- ' --goal-savings-source true|false Enable or disable goal-savings membership.',
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
- ' Partner-owned shared accounts and manual accounts cannot be changed.',
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: { isGoalSavingsSource: parsed.isGoalSavingsSource },
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(payload),
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 === 'investments'
496
- ? isInvestmentsResponse(value)
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);
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`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@slothmoney/agent-cli",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Command-line access to the Sloth Money Agent API.",
5
5
  "type": "module",
6
6
  "bin": {