@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 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 current assigned, spent, and available money without aggregating
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 server applies its normal once-per-UTC-day automatic transaction refresh
439
- policy before returning the current Sloth period dates and signed booked
440
- activity. For each category, `availablePence` is
441
- `assignedPence - spentPence`; a negative value is over budget, and refunds
442
- reduce `spentPence`. Check `refresh`,
443
- `activity.uncategorizedSpentPence`, and `activity.unmappedSpentPence` before
444
- using the result to suggest a reallocation. This command is read-only.
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. Caller-owned personal and
642
- joint linked investment accounts are included; partner-owned accounts, manual
643
- holdings, and investment activities are not.
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.17.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. Defaults to the current Sloth budget period.',
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 assigned, spent, and available money for the current Sloth budget period.',
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
- ' The server applies its normal once-per-UTC-day automatic transaction refresh policy.',
497
- ' Inspect refresh.status and refresh.reason before relying on the result.',
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[].assignedPence is the money assigned to the category.',
501
- ' categories[].spentPence is signed booked activity; refunds reduce it.',
502
- ' categories[].availablePence equals assignedPence minus spentPence.',
503
- ' Negative availablePence means the category is over budget.',
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.command === 'budget' && parsed.periodKey !== undefined) {
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 === null
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 isBudgetStatusResponse(value) {
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 === null
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.17.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
  },