@slothmoney/agent-cli 0.11.0 → 0.12.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 +6 -0
- package/README.md +17 -1
- package/dist/args.js +27 -8
- package/dist/cli.js +40 -6
- package/dist/contracts.js +104 -36
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.12.0 - 2026-08-16
|
|
4
|
+
|
|
5
|
+
- Add read-only `budget status` for current-period assigned, spent, and
|
|
6
|
+
available category amounts, including refresh and unallocated-activity
|
|
7
|
+
signals for safer automated budget review.
|
|
8
|
+
|
|
3
9
|
## 0.11.0 - 2026-08-16
|
|
4
10
|
|
|
5
11
|
- Add preview-by-default `budget move` for atomically moving current assigned
|
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.12.0 -- sloth-agent --help
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
## Authenticate
|
|
@@ -104,6 +104,7 @@ options, output, and examples:
|
|
|
104
104
|
sloth-agent auth login --help
|
|
105
105
|
sloth-agent accounts --help
|
|
106
106
|
sloth-agent budget --help
|
|
107
|
+
sloth-agent budget status --help
|
|
107
108
|
sloth-agent budget update --help
|
|
108
109
|
sloth-agent budget move --help
|
|
109
110
|
sloth-agent categories --help
|
|
@@ -249,6 +250,21 @@ The result includes the budget period and status, currency, the effective plan,
|
|
|
249
250
|
stored funding amounts when available, categories, line items, and planned
|
|
250
251
|
amounts in pence.
|
|
251
252
|
|
|
253
|
+
Read current assigned, spent, and available money without aggregating
|
|
254
|
+
transactions yourself:
|
|
255
|
+
|
|
256
|
+
```bash
|
|
257
|
+
sloth-agent budget status --scope personal
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
The server applies its normal once-per-UTC-day automatic transaction refresh
|
|
261
|
+
policy before returning the current Sloth period dates and signed booked
|
|
262
|
+
activity. For each category, `availablePence` is
|
|
263
|
+
`assignedPence - spentPence`; a negative value is over budget, and refunds
|
|
264
|
+
reduce `spentPence`. Check `refresh`,
|
|
265
|
+
`activity.uncategorizedSpentPence`, and `activity.unmappedSpentPence` before
|
|
266
|
+
using the result to suggest a reallocation. This command is read-only.
|
|
267
|
+
|
|
252
268
|
Update selected line-item amounts by creating `budget.json`:
|
|
253
269
|
|
|
254
270
|
```json
|
package/dist/args.js
CHANGED
|
@@ -372,21 +372,38 @@ function parseInvestments(args, baseUrl) {
|
|
|
372
372
|
return withBaseUrl({ command: 'investments', ...(accountRef ? { accountRef } : {}) }, baseUrl);
|
|
373
373
|
}
|
|
374
374
|
function parseBudget(args, baseUrl) {
|
|
375
|
-
const subcommand = args[0] === '
|
|
375
|
+
const subcommand = args[0] === 'status' || args[0] === 'update' || args[0] === 'move'
|
|
376
|
+
? args.shift()
|
|
377
|
+
: undefined;
|
|
378
|
+
const status = subcommand === 'status';
|
|
376
379
|
const update = subcommand === 'update';
|
|
377
380
|
const move = subcommand === 'move';
|
|
378
|
-
const commandLabel =
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
381
|
+
const commandLabel = status
|
|
382
|
+
? 'budget status'
|
|
383
|
+
: update
|
|
384
|
+
? 'budget update'
|
|
385
|
+
: move
|
|
386
|
+
? 'budget move'
|
|
387
|
+
: 'budget';
|
|
388
|
+
const { values, apply } = parseNamedOptions(args, commandLabel, new Set(status
|
|
389
|
+
? ['--scope']
|
|
390
|
+
: update
|
|
391
|
+
? ['--scope', '--period', '--input']
|
|
392
|
+
: move
|
|
393
|
+
? ['--scope', '--period', '--from-category-id', '--to-category-id', '--amount']
|
|
394
|
+
: ['--scope', '--period']));
|
|
384
395
|
if (!update && !move && apply)
|
|
385
|
-
throw new UsageError(
|
|
396
|
+
throw new UsageError(`Unknown ${commandLabel} option: --apply`);
|
|
386
397
|
const scope = requiredOption(values, '--scope', commandLabel);
|
|
387
398
|
if (scope !== 'personal' && scope !== 'joint') {
|
|
388
399
|
throw new UsageError('--scope must be personal or joint');
|
|
389
400
|
}
|
|
401
|
+
if (status) {
|
|
402
|
+
return withBaseUrl({
|
|
403
|
+
command: 'budget-status',
|
|
404
|
+
scope: scope,
|
|
405
|
+
}, baseUrl);
|
|
406
|
+
}
|
|
390
407
|
const period = values.get('--period');
|
|
391
408
|
const common = {
|
|
392
409
|
scope: scope,
|
|
@@ -769,6 +786,8 @@ function helpTopic(argv) {
|
|
|
769
786
|
return undefined;
|
|
770
787
|
}
|
|
771
788
|
if (command === 'budget') {
|
|
789
|
+
if (subcommand === 'status')
|
|
790
|
+
return 'budget-status';
|
|
772
791
|
if (subcommand === 'update')
|
|
773
792
|
return 'budget-update';
|
|
774
793
|
if (subcommand === 'move')
|
package/dist/cli.js
CHANGED
|
@@ -4,7 +4,7 @@ import { ICON_KEYS } from './category-metadata.js';
|
|
|
4
4
|
import { parseApiResponse, validateAssignmentPayload, validateBudgetMovementResponse, 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.12.0';
|
|
8
8
|
const REQUEST_TIMEOUT_MS = 60_000;
|
|
9
9
|
const API_ORIGIN_HELP_LINES = [
|
|
10
10
|
'',
|
|
@@ -30,6 +30,7 @@ export function usageText() {
|
|
|
30
30
|
' sloth-agent accounts remove --account-ref REF [--apply]',
|
|
31
31
|
' sloth-agent investments [--account-ref REF] [--base-url URL]',
|
|
32
32
|
' sloth-agent budget --scope personal|joint [--period YYYY-MM] [--base-url URL]',
|
|
33
|
+
' sloth-agent budget status --scope personal|joint [--base-url URL]',
|
|
33
34
|
' sloth-agent budget update --scope personal|joint [--period YYYY-MM]',
|
|
34
35
|
' --input budget.json [--apply] [--base-url URL]',
|
|
35
36
|
' sloth-agent budget move --scope personal|joint [--period YYYY-MM]',
|
|
@@ -421,6 +422,35 @@ export function budgetHelpText() {
|
|
|
421
422
|
' Categories also include plannedPence and assignedPence.',
|
|
422
423
|
].join('\n');
|
|
423
424
|
}
|
|
425
|
+
export function budgetStatusHelpText() {
|
|
426
|
+
return [
|
|
427
|
+
'Sloth Agent CLI — budget status',
|
|
428
|
+
'',
|
|
429
|
+
'Read assigned, spent, and available money for the current Sloth budget period.',
|
|
430
|
+
'',
|
|
431
|
+
'Usage:',
|
|
432
|
+
' sloth-agent budget status --scope personal|joint [--base-url URL]',
|
|
433
|
+
'',
|
|
434
|
+
'Options:',
|
|
435
|
+
' --scope personal|joint Required. Budget ownership scope.',
|
|
436
|
+
' --base-url URL Optional. Override the API origin.',
|
|
437
|
+
' -h, --help Show this help.',
|
|
438
|
+
...API_ORIGIN_HELP_LINES,
|
|
439
|
+
'',
|
|
440
|
+
'Access and freshness:',
|
|
441
|
+
' This command is read-only, requires agent:read, and never changes the budget.',
|
|
442
|
+
' The server applies its normal once-per-UTC-day automatic transaction refresh policy.',
|
|
443
|
+
' Inspect refresh.status and refresh.reason before relying on the result.',
|
|
444
|
+
'',
|
|
445
|
+
'Output:',
|
|
446
|
+
' categories[].assignedPence is the money assigned to the category.',
|
|
447
|
+
' categories[].spentPence is signed booked activity; refunds reduce it.',
|
|
448
|
+
' categories[].availablePence equals assignedPence minus spentPence.',
|
|
449
|
+
' Negative availablePence means the category is over budget.',
|
|
450
|
+
' activity contains the period dates, transaction count, uncategorizedSpentPence,',
|
|
451
|
+
' and unmappedSpentPence. Review either nonzero value before moving money.',
|
|
452
|
+
].join('\n');
|
|
453
|
+
}
|
|
424
454
|
export function budgetUpdateHelpText() {
|
|
425
455
|
return [
|
|
426
456
|
'Sloth Agent CLI — budget update',
|
|
@@ -867,6 +897,7 @@ export function commandHelpText(topic) {
|
|
|
867
897
|
'accounts-remove': accountsRemoveHelpText,
|
|
868
898
|
investments: investmentsHelpText,
|
|
869
899
|
budget: budgetHelpText,
|
|
900
|
+
'budget-status': budgetStatusHelpText,
|
|
870
901
|
'budget-move': budgetMoveHelpText,
|
|
871
902
|
'budget-update': budgetUpdateHelpText,
|
|
872
903
|
categories: categoriesHelpText,
|
|
@@ -1429,16 +1460,19 @@ export async function runCli(argv = process.argv.slice(2), options = {}) {
|
|
|
1429
1460
|
writeJson(writeStdout, withListedGoalPriorities(data));
|
|
1430
1461
|
return 0;
|
|
1431
1462
|
}
|
|
1432
|
-
if (parsed.command === 'budget') {
|
|
1463
|
+
if (parsed.command === 'budget' || parsed.command === 'budget-status') {
|
|
1433
1464
|
const query = new URLSearchParams({ scope: parsed.scope });
|
|
1434
|
-
if (parsed.periodKey !== undefined)
|
|
1465
|
+
if (parsed.command === 'budget' && parsed.periodKey !== undefined) {
|
|
1435
1466
|
query.set('periodKey', parsed.periodKey);
|
|
1436
|
-
|
|
1467
|
+
}
|
|
1468
|
+
const response = await fetchImplementation(`${baseUrl}/api/agent/v1/${parsed.command === 'budget' ? 'budgets' : 'budget-status'}?${query.toString()}`, {
|
|
1437
1469
|
method: 'GET',
|
|
1438
|
-
headers
|
|
1470
|
+
headers: parsed.command === 'budget-status'
|
|
1471
|
+
? { ...headers, Prefer: 'wait=45' }
|
|
1472
|
+
: headers,
|
|
1439
1473
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
1440
1474
|
});
|
|
1441
|
-
const data = parseApiResponse(
|
|
1475
|
+
const data = parseApiResponse(parsed.command, await parseHttpResponse(response, token));
|
|
1442
1476
|
writeJson(writeStdout, data);
|
|
1443
1477
|
return 0;
|
|
1444
1478
|
}
|
package/dist/contracts.js
CHANGED
|
@@ -227,28 +227,32 @@ function isTransaction(value) {
|
|
|
227
227
|
|| value.incomeSubtype === 'pay'
|
|
228
228
|
|| value.incomeSubtype === 'interest'));
|
|
229
229
|
}
|
|
230
|
+
const REFRESH_STATUSES = new Set(['skipped', 'completed', 'in_progress', 'partial', 'failed']);
|
|
231
|
+
const REFRESH_REASONS = new Set([
|
|
232
|
+
'all_fetched_today',
|
|
233
|
+
'no_api_connections',
|
|
234
|
+
'no_selected_accounts',
|
|
235
|
+
'refreshed',
|
|
236
|
+
'wait_timeout',
|
|
237
|
+
'account_failures',
|
|
238
|
+
'partial_already_attempted',
|
|
239
|
+
'refresh_error',
|
|
240
|
+
]);
|
|
241
|
+
function isRefreshStatus(value) {
|
|
242
|
+
return (isObject(value)
|
|
243
|
+
&& hasOnlyFields(value, ['status', 'reason', 'utcDate'])
|
|
244
|
+
&& typeof value.status === 'string'
|
|
245
|
+
&& REFRESH_STATUSES.has(value.status)
|
|
246
|
+
&& typeof value.reason === 'string'
|
|
247
|
+
&& REFRESH_REASONS.has(value.reason)
|
|
248
|
+
&& isIsoDate(value.utcDate));
|
|
249
|
+
}
|
|
230
250
|
function isTransactionsResponse(value) {
|
|
231
|
-
const validStatuses = new Set(['skipped', 'completed', 'in_progress', 'partial', 'failed']);
|
|
232
|
-
const validReasons = new Set([
|
|
233
|
-
'all_fetched_today',
|
|
234
|
-
'no_api_connections',
|
|
235
|
-
'no_selected_accounts',
|
|
236
|
-
'refreshed',
|
|
237
|
-
'wait_timeout',
|
|
238
|
-
'account_failures',
|
|
239
|
-
'refresh_error',
|
|
240
|
-
]);
|
|
241
|
-
const refresh = isObject(value) ? value.refresh : undefined;
|
|
242
251
|
return (isObject(value)
|
|
243
252
|
&& Array.isArray(value.transactions)
|
|
244
253
|
&& value.transactions.every(isTransaction)
|
|
245
254
|
&& (value.nextCursor === null || typeof value.nextCursor === 'string')
|
|
246
|
-
&&
|
|
247
|
-
&& typeof refresh.status === 'string'
|
|
248
|
-
&& validStatuses.has(refresh.status)
|
|
249
|
-
&& typeof refresh.reason === 'string'
|
|
250
|
-
&& validReasons.has(refresh.reason)
|
|
251
|
-
&& isIsoDate(refresh.utcDate));
|
|
255
|
+
&& isRefreshStatus(value.refresh));
|
|
252
256
|
}
|
|
253
257
|
function isAssignmentResponse(value) {
|
|
254
258
|
return (isObject(value)
|
|
@@ -375,6 +379,68 @@ function isBudgetResponse(value) {
|
|
|
375
379
|
&& Array.isArray(value.categories)
|
|
376
380
|
&& value.categories.every(isBudgetCategory));
|
|
377
381
|
}
|
|
382
|
+
function isBudgetStatusResponse(value) {
|
|
383
|
+
if (!isObject(value)
|
|
384
|
+
|| !hasOnlyFields(value, [
|
|
385
|
+
'scope',
|
|
386
|
+
'periodKey',
|
|
387
|
+
'periodStatus',
|
|
388
|
+
'currency',
|
|
389
|
+
'effectiveFromPeriodKey',
|
|
390
|
+
'funding',
|
|
391
|
+
'activity',
|
|
392
|
+
'refresh',
|
|
393
|
+
'categories',
|
|
394
|
+
]))
|
|
395
|
+
return false;
|
|
396
|
+
return ((value.scope === 'personal' || value.scope === 'joint')
|
|
397
|
+
&& typeof value.periodKey === 'string'
|
|
398
|
+
&& /^\d{4}-(0[1-9]|1[0-2])$/.test(value.periodKey)
|
|
399
|
+
&& value.periodStatus === 'current'
|
|
400
|
+
&& isCurrency(value.currency)
|
|
401
|
+
&& typeof value.effectiveFromPeriodKey === 'string'
|
|
402
|
+
&& /^\d{4}-(0[1-9]|1[0-2])$/.test(value.effectiveFromPeriodKey)
|
|
403
|
+
&& (value.funding === null
|
|
404
|
+
|| (isObject(value.funding)
|
|
405
|
+
&& hasOnlyFields(value.funding, ['toAssignPence', 'nextPeriodReservePence'])
|
|
406
|
+
&& isSafeInteger(value.funding.toAssignPence)
|
|
407
|
+
&& isSafeInteger(value.funding.nextPeriodReservePence)))
|
|
408
|
+
&& isObject(value.activity)
|
|
409
|
+
&& hasOnlyFields(value.activity, [
|
|
410
|
+
'startDate',
|
|
411
|
+
'endDate',
|
|
412
|
+
'transactionCount',
|
|
413
|
+
'uncategorizedSpentPence',
|
|
414
|
+
'unmappedSpentPence',
|
|
415
|
+
])
|
|
416
|
+
&& isIsoDate(value.activity.startDate)
|
|
417
|
+
&& isIsoDate(value.activity.endDate)
|
|
418
|
+
&& value.activity.startDate <= value.activity.endDate
|
|
419
|
+
&& isNonnegativeSafeInteger(value.activity.transactionCount)
|
|
420
|
+
&& isSafeInteger(value.activity.uncategorizedSpentPence)
|
|
421
|
+
&& isSafeInteger(value.activity.unmappedSpentPence)
|
|
422
|
+
&& isRefreshStatus(value.refresh)
|
|
423
|
+
&& Array.isArray(value.categories)
|
|
424
|
+
&& value.categories.every((category) => (isObject(category)
|
|
425
|
+
&& hasOnlyFields(category, [
|
|
426
|
+
'id',
|
|
427
|
+
'name',
|
|
428
|
+
'plannedPence',
|
|
429
|
+
'assignedPence',
|
|
430
|
+
'spentPence',
|
|
431
|
+
'availablePence',
|
|
432
|
+
])
|
|
433
|
+
&& typeof category.id === 'string'
|
|
434
|
+
&& category.id.trim().length > 0
|
|
435
|
+
&& typeof category.name === 'string'
|
|
436
|
+
&& category.name.trim().length > 0
|
|
437
|
+
&& isNonnegativeSafeInteger(category.plannedPence)
|
|
438
|
+
&& isSafeInteger(category.assignedPence)
|
|
439
|
+
&& isSafeInteger(category.spentPence)
|
|
440
|
+
&& isSafeInteger(category.availablePence)
|
|
441
|
+
&& Number.isSafeInteger(category.assignedPence - category.spentPence)
|
|
442
|
+
&& category.availablePence === category.assignedPence - category.spentPence)));
|
|
443
|
+
}
|
|
378
444
|
function isBudgetMovementResponse(value) {
|
|
379
445
|
return (isObject(value)
|
|
380
446
|
&& hasOnlyFields(value, [
|
|
@@ -568,25 +634,27 @@ export function parseApiResponse(command, value) {
|
|
|
568
634
|
? isInvestmentsResponse(value)
|
|
569
635
|
: command === 'budget' || command === 'budget-update'
|
|
570
636
|
? isBudgetResponse(value)
|
|
571
|
-
: command === 'budget-
|
|
572
|
-
?
|
|
573
|
-
: command === '
|
|
574
|
-
?
|
|
575
|
-
: command === 'categories
|
|
576
|
-
?
|
|
577
|
-
: command === '
|
|
578
|
-
?
|
|
579
|
-
: command === '
|
|
580
|
-
?
|
|
581
|
-
: command === '
|
|
582
|
-
?
|
|
583
|
-
: command === '
|
|
584
|
-
?
|
|
585
|
-
: command === '
|
|
586
|
-
?
|
|
587
|
-
: command === 'goals-
|
|
588
|
-
?
|
|
589
|
-
:
|
|
637
|
+
: command === 'budget-status'
|
|
638
|
+
? isBudgetStatusResponse(value)
|
|
639
|
+
: command === 'budget-move'
|
|
640
|
+
? isBudgetMovementResponse(value)
|
|
641
|
+
: command === 'categories'
|
|
642
|
+
? isCategoryResponse(value)
|
|
643
|
+
: command === 'categories-create' || command === 'categories-rename'
|
|
644
|
+
? isCategoryMutationResponse(value)
|
|
645
|
+
: command === 'line-items-create' || command === 'line-items-rename'
|
|
646
|
+
? isLineItemMutationResponse(value)
|
|
647
|
+
: command === 'transactions'
|
|
648
|
+
? isTransactionsResponse(value)
|
|
649
|
+
: command === 'assign'
|
|
650
|
+
? isAssignmentResponse(value)
|
|
651
|
+
: command === 'ask-partner'
|
|
652
|
+
? isPartnerResponse(value)
|
|
653
|
+
: command === 'goals-list'
|
|
654
|
+
? isGoalsResponse(value)
|
|
655
|
+
: command === 'goals-delete'
|
|
656
|
+
? isGoalDeleteResponse(value)
|
|
657
|
+
: isGoalMutationResponse(value);
|
|
590
658
|
if (!valid) {
|
|
591
659
|
const label = command === 'assign' ? 'assignment' : command;
|
|
592
660
|
throw new ApiError(`Invalid ${label} response from the Agent API`);
|