@slothmoney/agent-cli 0.17.0 → 0.18.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 +27 -12
- package/dist/args.js +3 -7
- package/dist/cli.js +18 -19
- package/dist/contracts.js +85 -11
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.18.0 - 2026-08-25
|
|
4
|
+
|
|
5
|
+
- Add optional `--period YYYY-MM` to `budget status` for current and historical
|
|
6
|
+
Sloth budget periods.
|
|
7
|
+
- Replace signed spending-only status with clear money-in, money-out, and net
|
|
8
|
+
activity for Income, Transfer, None, ordinary, custom, and uncategorized
|
|
9
|
+
transactions.
|
|
10
|
+
- Accept the old current-status response temporarily during the coordinated
|
|
11
|
+
backend rollout; historical responses use the new strict contract.
|
|
12
|
+
- Add a release preflight that compares local, packed, globally installed, and
|
|
13
|
+
npm-registry versions and checks every nested help route.
|
|
14
|
+
|
|
3
15
|
## 0.17.0 - 2026-08-25
|
|
4
16
|
|
|
5
17
|
- Run applied transaction-assignment batches through resumable server
|
package/README.md
CHANGED
|
@@ -428,20 +428,27 @@ The result includes the budget period and status, currency, the effective plan,
|
|
|
428
428
|
stored funding amounts when available, categories, line items, and planned
|
|
429
429
|
amounts in pence.
|
|
430
430
|
|
|
431
|
-
Read
|
|
432
|
-
transactions yourself:
|
|
431
|
+
Read booked activity for the current or a historical Sloth period:
|
|
433
432
|
|
|
434
433
|
```bash
|
|
435
434
|
sloth-agent budget status --scope personal
|
|
435
|
+
sloth-agent budget status --scope personal --period 2026-07
|
|
436
436
|
```
|
|
437
437
|
|
|
438
|
-
The
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
`
|
|
444
|
-
|
|
438
|
+
The period key names the calendar month containing that Sloth period's start
|
|
439
|
+
boundary. Its end date can fall in the following month.
|
|
440
|
+
|
|
441
|
+
The current period uses Sloth's normal once-per-UTC-day transaction refresh;
|
|
442
|
+
historical periods are cache-only and return `refresh: null`. Activity contains
|
|
443
|
+
nonnegative `moneyInPence` and `moneyOutPence` plus their difference as
|
|
444
|
+
`netPence`. Income, Transfer, explicit None, budget categories, and observed
|
|
445
|
+
custom categories are normal rows. A transaction with no category at all is
|
|
446
|
+
reported separately under `activity.uncategorized`.
|
|
447
|
+
|
|
448
|
+
`budget` contains assigned, spent, and available category amounts when a
|
|
449
|
+
trustworthy period plan exists. It is `null` when it does not; activity still
|
|
450
|
+
returns. The response includes only the period budget currency and silently
|
|
451
|
+
ignores rows in other currencies. This command is read-only.
|
|
445
452
|
|
|
446
453
|
Update selected line-item amounts by creating `budget.json`:
|
|
447
454
|
|
|
@@ -638,9 +645,12 @@ sloth-agent investments --account-ref sloth_account_v1_...
|
|
|
638
645
|
Investment reads are cache-only and do not refresh a brokerage. Holding
|
|
639
646
|
quantities, unit prices, market values, currencies, and freshness are returned
|
|
640
647
|
in provider-native terms. They are not converted or guaranteed to reconcile
|
|
641
|
-
to an account total reported in another currency.
|
|
642
|
-
|
|
643
|
-
|
|
648
|
+
to an account total reported in another currency. An investment account total
|
|
649
|
+
and its nested holdings describe the same portfolio, so do not add them
|
|
650
|
+
together. Do not add values in different currencies without an explicit
|
|
651
|
+
conversion. Caller-owned personal and joint linked investment accounts are
|
|
652
|
+
included; partner-owned accounts, manual holdings, and investment activities
|
|
653
|
+
are not.
|
|
644
654
|
|
|
645
655
|
List your goals:
|
|
646
656
|
|
|
@@ -789,11 +799,16 @@ command with the same input resumes the same server operation.
|
|
|
789
799
|
```bash
|
|
790
800
|
npm ci
|
|
791
801
|
npm run verify
|
|
802
|
+
npm run release:preflight
|
|
792
803
|
```
|
|
793
804
|
|
|
794
805
|
`npm run test:package` packs the exact npm artifact, installs it into a clean
|
|
795
806
|
temporary project, and runs the installed binary.
|
|
796
807
|
|
|
808
|
+
`npm run release:preflight` reports the local, packed, globally installed, and
|
|
809
|
+
npm-registry versions, then exercises every parser command's nested help. It
|
|
810
|
+
fails when a parent help page stops advertising one of its child commands.
|
|
811
|
+
|
|
797
812
|
## Releasing
|
|
798
813
|
|
|
799
814
|
Releases are published only through the trusted `Publish npm release` GitHub
|
package/dist/args.js
CHANGED
|
@@ -486,7 +486,7 @@ function parseBudget(args, baseUrl) {
|
|
|
486
486
|
? 'budget move'
|
|
487
487
|
: 'budget';
|
|
488
488
|
const { values, apply } = parseNamedOptions(args, commandLabel, new Set(status
|
|
489
|
-
? ['--scope']
|
|
489
|
+
? ['--scope', '--period']
|
|
490
490
|
: update
|
|
491
491
|
? ['--scope', '--period', '--input']
|
|
492
492
|
: move
|
|
@@ -498,17 +498,13 @@ function parseBudget(args, baseUrl) {
|
|
|
498
498
|
if (scope !== 'personal' && scope !== 'joint') {
|
|
499
499
|
throw new UsageError('--scope must be personal or joint');
|
|
500
500
|
}
|
|
501
|
-
if (status) {
|
|
502
|
-
return withBaseUrl({
|
|
503
|
-
command: 'budget-status',
|
|
504
|
-
scope: scope,
|
|
505
|
-
}, baseUrl);
|
|
506
|
-
}
|
|
507
501
|
const period = values.get('--period');
|
|
508
502
|
const common = {
|
|
509
503
|
scope: scope,
|
|
510
504
|
...(period === undefined ? {} : { periodKey: parseGoalMonthKey(period, '--period') }),
|
|
511
505
|
};
|
|
506
|
+
if (status)
|
|
507
|
+
return withBaseUrl({ command: 'budget-status', ...common }, baseUrl);
|
|
512
508
|
if (!update && !move)
|
|
513
509
|
return withBaseUrl({ command: 'budget', ...common }, baseUrl);
|
|
514
510
|
if (move) {
|
package/dist/cli.js
CHANGED
|
@@ -6,7 +6,7 @@ import { ICON_KEYS } from './category-metadata.js';
|
|
|
6
6
|
import { parseApiResponse, parseAssignmentOperationResponse, toLegacyAssignmentResponse, validateAssignmentPayload, validateBudgetMovementResponse, validateBudgetUpdatePayload, validateNotificationRulePayload, validateReceiptConfirmation, } from './contracts.js';
|
|
7
7
|
import { createSystemCredentialStore, secureStorageUnavailableError, } from './credential-store.js';
|
|
8
8
|
import { ApiError, CliError, ConfigError, UsageError, } from './errors.js';
|
|
9
|
-
export const CLI_VERSION = '0.
|
|
9
|
+
export const CLI_VERSION = '0.18.0';
|
|
10
10
|
const REQUEST_TIMEOUT_MS = 60_000;
|
|
11
11
|
const MAX_CONTRACT_PDF_BYTES = 6_000_000;
|
|
12
12
|
const API_ORIGIN_HELP_LINES = [
|
|
@@ -35,7 +35,7 @@ export function usageText() {
|
|
|
35
35
|
' sloth-agent accounts remove --account-ref REF [--apply]',
|
|
36
36
|
' sloth-agent investments [--account-ref REF] [--base-url URL]',
|
|
37
37
|
' sloth-agent budget --scope personal|joint [--period YYYY-MM] [--base-url URL]',
|
|
38
|
-
' sloth-agent budget status --scope personal|joint [--base-url URL]',
|
|
38
|
+
' sloth-agent budget status --scope personal|joint [--period YYYY-MM] [--base-url URL]',
|
|
39
39
|
' sloth-agent budget update --scope personal|joint [--period YYYY-MM]',
|
|
40
40
|
' --input budget.json [--apply] [--base-url URL]',
|
|
41
41
|
' sloth-agent budget move --scope personal|joint [--period YYYY-MM]',
|
|
@@ -461,7 +461,7 @@ export function budgetHelpText() {
|
|
|
461
461
|
'',
|
|
462
462
|
'Options:',
|
|
463
463
|
' --scope personal|joint Required. Budget ownership scope.',
|
|
464
|
-
' --period YYYY-MM Optional.
|
|
464
|
+
' --period YYYY-MM Optional. Month containing the period start; defaults to the current Sloth period.',
|
|
465
465
|
' --base-url URL Optional. Override the API origin.',
|
|
466
466
|
' -h, --help Show this help.',
|
|
467
467
|
...API_ORIGIN_HELP_LINES,
|
|
@@ -480,29 +480,28 @@ export function budgetStatusHelpText() {
|
|
|
480
480
|
return [
|
|
481
481
|
'Sloth Agent CLI — budget status',
|
|
482
482
|
'',
|
|
483
|
-
'Read
|
|
483
|
+
'Read booked activity and any trustworthy budget for one Sloth budget period.',
|
|
484
484
|
'',
|
|
485
485
|
'Usage:',
|
|
486
|
-
' sloth-agent budget status --scope personal|joint [--base-url URL]',
|
|
486
|
+
' sloth-agent budget status --scope personal|joint [--period YYYY-MM] [--base-url URL]',
|
|
487
487
|
'',
|
|
488
488
|
'Options:',
|
|
489
489
|
' --scope personal|joint Required. Budget ownership scope.',
|
|
490
|
+
' --period YYYY-MM Optional. Month containing the period start; defaults to the current Sloth period.',
|
|
490
491
|
' --base-url URL Optional. Override the API origin.',
|
|
491
492
|
' -h, --help Show this help.',
|
|
492
493
|
...API_ORIGIN_HELP_LINES,
|
|
493
494
|
'',
|
|
494
495
|
'Access and freshness:',
|
|
495
496
|
' This command is read-only, requires agent:read, and never changes the budget.',
|
|
496
|
-
'
|
|
497
|
-
'
|
|
497
|
+
' Current periods use the normal transaction refresh policy; historical periods are cache-only.',
|
|
498
|
+
' A historical response returns refresh as null.',
|
|
498
499
|
'',
|
|
499
500
|
'Output:',
|
|
500
|
-
' categories
|
|
501
|
-
'
|
|
502
|
-
'
|
|
503
|
-
'
|
|
504
|
-
' activity contains the period dates, transaction count, uncategorizedSpentPence,',
|
|
505
|
-
' and unmappedSpentPence. Review either nonzero value before moving money.',
|
|
501
|
+
' activity.categories includes Income, Transfer, None, and ordinary categories.',
|
|
502
|
+
' moneyInPence and moneyOutPence are nonnegative; netPence is money in minus money out.',
|
|
503
|
+
' A transaction with no category appears under activity.uncategorized.',
|
|
504
|
+
' budget contains the period plan and balances, or budget is null when no trustworthy plan exists.',
|
|
506
505
|
].join('\n');
|
|
507
506
|
}
|
|
508
507
|
export function budgetUpdateHelpText() {
|
|
@@ -962,11 +961,11 @@ export function rulesHelpText() {
|
|
|
962
961
|
'Rules do not create transactions or recurring predictions.',
|
|
963
962
|
'',
|
|
964
963
|
'Commands:',
|
|
965
|
-
' rules list List every saved notification rule.',
|
|
966
|
-
' rules get Read the rule for one transaction.',
|
|
967
|
-
' rules set Preview or save a rule for one transaction.',
|
|
968
|
-
' rules delete Preview or remove a rule.',
|
|
969
|
-
' rules scan-contract Validate or scan a PDF for its renewal date.',
|
|
964
|
+
' sloth-agent rules list List every saved notification rule.',
|
|
965
|
+
' sloth-agent rules get Read the rule for one transaction.',
|
|
966
|
+
' sloth-agent rules set Preview or save a rule for one transaction.',
|
|
967
|
+
' sloth-agent rules delete Preview or remove a rule.',
|
|
968
|
+
' sloth-agent rules scan-contract Validate or scan a PDF for its renewal date.',
|
|
970
969
|
'',
|
|
971
970
|
'Use the exact transactionRef returned by sloth-agent transactions.',
|
|
972
971
|
...API_ORIGIN_HELP_LINES,
|
|
@@ -2065,7 +2064,7 @@ export async function runCli(argv = process.argv.slice(2), options = {}) {
|
|
|
2065
2064
|
}
|
|
2066
2065
|
if (parsed.command === 'budget' || parsed.command === 'budget-status') {
|
|
2067
2066
|
const query = new URLSearchParams({ scope: parsed.scope });
|
|
2068
|
-
if (parsed.
|
|
2067
|
+
if (parsed.periodKey !== undefined) {
|
|
2069
2068
|
query.set('periodKey', parsed.periodKey);
|
|
2070
2069
|
}
|
|
2071
2070
|
const response = await fetchImplementation(`${baseUrl}/api/agent/v1/${parsed.command === 'budget' ? 'budgets' : 'budget-status'}?${query.toString()}`, {
|
package/dist/contracts.js
CHANGED
|
@@ -666,6 +666,12 @@ function isBudgetCategory(value) {
|
|
|
666
666
|
&& Array.isArray(value.lineItems)
|
|
667
667
|
&& value.lineItems.every(isBudgetLineItem));
|
|
668
668
|
}
|
|
669
|
+
function isBudgetFunding(value) {
|
|
670
|
+
return value === null || (isObject(value)
|
|
671
|
+
&& hasOnlyFields(value, ['toAssignPence', 'nextPeriodReservePence'])
|
|
672
|
+
&& isSafeInteger(value.toAssignPence)
|
|
673
|
+
&& isSafeInteger(value.nextPeriodReservePence));
|
|
674
|
+
}
|
|
669
675
|
function isBudgetResponse(value) {
|
|
670
676
|
return (isObject(value)
|
|
671
677
|
&& hasOnlyFields(value, [
|
|
@@ -686,15 +692,11 @@ function isBudgetResponse(value) {
|
|
|
686
692
|
&& isCurrency(value.currency)
|
|
687
693
|
&& typeof value.effectiveFromPeriodKey === 'string'
|
|
688
694
|
&& /^\d{4}-(0[1-9]|1[0-2])$/.test(value.effectiveFromPeriodKey)
|
|
689
|
-
&& (value.funding
|
|
690
|
-
|| (isObject(value.funding)
|
|
691
|
-
&& hasOnlyFields(value.funding, ['toAssignPence', 'nextPeriodReservePence'])
|
|
692
|
-
&& isSafeInteger(value.funding.toAssignPence)
|
|
693
|
-
&& isSafeInteger(value.funding.nextPeriodReservePence)))
|
|
695
|
+
&& isBudgetFunding(value.funding)
|
|
694
696
|
&& Array.isArray(value.categories)
|
|
695
697
|
&& value.categories.every(isBudgetCategory));
|
|
696
698
|
}
|
|
697
|
-
function
|
|
699
|
+
function isLegacyBudgetStatusResponse(value) {
|
|
698
700
|
if (!isObject(value)
|
|
699
701
|
|| !hasOnlyFields(value, [
|
|
700
702
|
'scope',
|
|
@@ -715,11 +717,7 @@ function isBudgetStatusResponse(value) {
|
|
|
715
717
|
&& isCurrency(value.currency)
|
|
716
718
|
&& typeof value.effectiveFromPeriodKey === 'string'
|
|
717
719
|
&& /^\d{4}-(0[1-9]|1[0-2])$/.test(value.effectiveFromPeriodKey)
|
|
718
|
-
&& (value.funding
|
|
719
|
-
|| (isObject(value.funding)
|
|
720
|
-
&& hasOnlyFields(value.funding, ['toAssignPence', 'nextPeriodReservePence'])
|
|
721
|
-
&& isSafeInteger(value.funding.toAssignPence)
|
|
722
|
-
&& isSafeInteger(value.funding.nextPeriodReservePence)))
|
|
720
|
+
&& isBudgetFunding(value.funding)
|
|
723
721
|
&& isObject(value.activity)
|
|
724
722
|
&& hasOnlyFields(value.activity, [
|
|
725
723
|
'startDate',
|
|
@@ -756,6 +754,82 @@ function isBudgetStatusResponse(value) {
|
|
|
756
754
|
&& Number.isSafeInteger(category.assignedPence - category.spentPence)
|
|
757
755
|
&& category.availablePence === category.assignedPence - category.spentPence)));
|
|
758
756
|
}
|
|
757
|
+
function isActivityAmounts(value) {
|
|
758
|
+
return isObject(value)
|
|
759
|
+
&& hasOnlyFields(value, ['moneyInPence', 'moneyOutPence', 'netPence'])
|
|
760
|
+
&& isNonnegativeSafeInteger(value.moneyInPence)
|
|
761
|
+
&& isNonnegativeSafeInteger(value.moneyOutPence)
|
|
762
|
+
&& isSafeInteger(value.netPence)
|
|
763
|
+
&& Number.isSafeInteger(value.moneyInPence - value.moneyOutPence)
|
|
764
|
+
&& value.netPence === value.moneyInPence - value.moneyOutPence;
|
|
765
|
+
}
|
|
766
|
+
function isBudgetActivityStatusResponse(value) {
|
|
767
|
+
if (!isObject(value) || !hasOnlyFields(value, [
|
|
768
|
+
'scope', 'periodKey', 'periodStatus', 'currency', 'period', 'refresh', 'activity', 'budget',
|
|
769
|
+
]))
|
|
770
|
+
return false;
|
|
771
|
+
if (!isObject(value.period) || !hasOnlyFields(value.period, [
|
|
772
|
+
'startDate', 'endDate', 'dateRangeSource',
|
|
773
|
+
]))
|
|
774
|
+
return false;
|
|
775
|
+
if (!isObject(value.activity) || !hasOnlyFields(value.activity, [
|
|
776
|
+
'transactionCount', 'categories', 'uncategorized',
|
|
777
|
+
]))
|
|
778
|
+
return false;
|
|
779
|
+
const activityCategories = value.activity.categories;
|
|
780
|
+
if (!Array.isArray(activityCategories) || !activityCategories.every((category) => (isObject(category)
|
|
781
|
+
&& hasOnlyFields(category, ['id', 'name', 'moneyInPence', 'moneyOutPence', 'netPence'])
|
|
782
|
+
&& typeof category.id === 'string'
|
|
783
|
+
&& category.id.trim().length > 0
|
|
784
|
+
&& typeof category.name === 'string'
|
|
785
|
+
&& category.name.trim().length > 0
|
|
786
|
+
&& isActivityAmounts({
|
|
787
|
+
moneyInPence: category.moneyInPence,
|
|
788
|
+
moneyOutPence: category.moneyOutPence,
|
|
789
|
+
netPence: category.netPence,
|
|
790
|
+
}))))
|
|
791
|
+
return false;
|
|
792
|
+
const budget = value.budget;
|
|
793
|
+
if (budget !== null && (!isObject(budget)
|
|
794
|
+
|| !hasOnlyFields(budget, ['effectiveFromPeriodKey', 'funding', 'categories'])
|
|
795
|
+
|| typeof budget.effectiveFromPeriodKey !== 'string'
|
|
796
|
+
|| !/^\d{4}-(0[1-9]|1[0-2])$/.test(budget.effectiveFromPeriodKey)
|
|
797
|
+
|| !isBudgetFunding(budget.funding)
|
|
798
|
+
|| !Array.isArray(budget.categories)
|
|
799
|
+
|| !budget.categories.every((category) => (isObject(category)
|
|
800
|
+
&& hasOnlyFields(category, [
|
|
801
|
+
'id', 'name', 'plannedPence', 'assignedPence', 'spentPence', 'availablePence',
|
|
802
|
+
])
|
|
803
|
+
&& typeof category.id === 'string'
|
|
804
|
+
&& category.id.trim().length > 0
|
|
805
|
+
&& typeof category.name === 'string'
|
|
806
|
+
&& category.name.trim().length > 0
|
|
807
|
+
&& isNonnegativeSafeInteger(category.plannedPence)
|
|
808
|
+
&& isSafeInteger(category.assignedPence)
|
|
809
|
+
&& isSafeInteger(category.spentPence)
|
|
810
|
+
&& isSafeInteger(category.availablePence)
|
|
811
|
+
&& Number.isSafeInteger(category.assignedPence - category.spentPence)
|
|
812
|
+
&& category.availablePence === category.assignedPence - category.spentPence))))
|
|
813
|
+
return false;
|
|
814
|
+
return (value.scope === 'personal' || value.scope === 'joint')
|
|
815
|
+
&& typeof value.periodKey === 'string'
|
|
816
|
+
&& /^\d{4}-(0[1-9]|1[0-2])$/.test(value.periodKey)
|
|
817
|
+
&& (value.periodStatus === 'current' || value.periodStatus === 'historical')
|
|
818
|
+
&& isCurrency(value.currency)
|
|
819
|
+
&& isIsoDate(value.period.startDate)
|
|
820
|
+
&& isIsoDate(value.period.endDate)
|
|
821
|
+
&& value.period.startDate <= value.period.endDate
|
|
822
|
+
&& (value.period.dateRangeSource === 'stored'
|
|
823
|
+
|| value.period.dateRangeSource === 'legacy_settings_fallback')
|
|
824
|
+
&& isNonnegativeSafeInteger(value.activity.transactionCount)
|
|
825
|
+
&& isActivityAmounts(value.activity.uncategorized)
|
|
826
|
+
&& (value.periodStatus === 'historical'
|
|
827
|
+
? value.refresh === null
|
|
828
|
+
: isRefreshStatus(value.refresh));
|
|
829
|
+
}
|
|
830
|
+
function isBudgetStatusResponse(value) {
|
|
831
|
+
return isLegacyBudgetStatusResponse(value) || isBudgetActivityStatusResponse(value);
|
|
832
|
+
}
|
|
759
833
|
function isBudgetMovementResponse(value) {
|
|
760
834
|
return (isObject(value)
|
|
761
835
|
&& hasOnlyFields(value, [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@slothmoney/agent-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "Command-line access to the Sloth Money Agent API.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
"test": "vitest run",
|
|
18
18
|
"test:package": "node scripts/test-package.mjs",
|
|
19
19
|
"test:registry": "node scripts/test-registry-package.mjs",
|
|
20
|
+
"release:preflight": "node scripts/release-preflight.mjs",
|
|
20
21
|
"typecheck": "tsc --noEmit",
|
|
21
22
|
"verify": "npm run lint && npm run typecheck && npm test && npm run build && npm run test:package"
|
|
22
23
|
},
|