@slothmoney/agent-cli 0.3.1 → 0.4.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,14 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ - Add the read-only `sloth-agent accounts` command with strict runtime
6
+ validation for opaque references, ownership, native balances, sources, and
7
+ freshness metadata.
8
+ - Prepare 0.4.0 by removing the obsolete `joint-budget-settings` command. Shared
9
+ personal transactions now enter the joint budget through their assignment.
10
+ - Verify each trusted npm release from a fresh temporary directory so the
11
+ registry smoke test cannot resolve a repository-local CLI executable.
12
+
5
13
  ## 0.3.1 - 2026-07-31
6
14
 
7
15
  - Coordinate transaction reads with the Sloth Budget daily refresh process.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Sloth Agent CLI
2
2
 
3
- Use your own agent to manage goals and categorise transactions through the
3
+ Use your own agent to inspect known accounts, manage goals, and categorise transactions through the
4
4
  [Sloth Money Agent API](https://slothmoney.app/developers/).
5
5
 
6
6
  ## Install
@@ -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.3.1 -- sloth-agent --help
18
+ npm exec --yes --package=@slothmoney/agent-cli@0.4.0 -- sloth-agent --help
19
19
  ```
20
20
 
21
21
  ## Authenticate
@@ -96,10 +96,10 @@ options, output, and examples:
96
96
 
97
97
  ```bash
98
98
  sloth-agent auth login --help
99
+ sloth-agent accounts --help
99
100
  sloth-agent categories --help
100
101
  sloth-agent transactions --help
101
102
  sloth-agent assign --help
102
- sloth-agent joint-budget-settings --help
103
103
  sloth-agent goals create --help
104
104
  sloth-agent goals update --help
105
105
  sloth-agent ask-partner --help
@@ -174,6 +174,20 @@ query. Assignments do not create a separate list.
174
174
 
175
175
  ### Other workflows
176
176
 
177
+ Read the existing Sloth account inventory:
178
+
179
+ ```bash
180
+ sloth-agent accounts
181
+ ```
182
+
183
+ The command is read-only and cache-only: it does not refresh linked banks or
184
+ change account data. Each result contains an opaque `accountRef`, personal or
185
+ joint ownership, connected or manual source, native balance/currency when
186
+ known, `lastBalanceUpdatedAt`, and `connectionState`. Missing values are JSON
187
+ `null`; currencies are never converted or combined. Partner personal accounts
188
+ are excluded, while enabled shared joint accounts follow Sloth's existing
189
+ visibility rules.
190
+
177
191
  List your goals:
178
192
 
179
193
  ```bash
@@ -243,16 +257,13 @@ account failure remains eligible for an automatic retry.
243
257
  Set `"assignmentScope": "joint"` on an assignment to categorise the eligible
244
258
  shared portion for the joint budget.
245
259
 
246
- Set whether the shared portions of personal-account transactions count in the
247
- linked joint budget. The first command previews; the second applies:
260
+ Transaction reads expose `personalBudgetAmountPence` for the caller's explicit
261
+ personal-only portion and `jointBudgetContribution.amountPence` for the full
262
+ shared portion. The 60/40 settlement ratio does not reduce joint-budget spend.
248
263
 
249
- ```bash
250
- sloth-agent joint-budget-settings \
251
- --include-shared-personal-transactions=true
252
- sloth-agent joint-budget-settings \
253
- --include-shared-personal-transactions=true \
254
- --apply
255
- ```
264
+ Shared personal-account transactions with a joint assignment are included in
265
+ the joint budget automatically. The settlement ratio remains independent from
266
+ the amount attributed to the joint budget.
256
267
 
257
268
  Create a partner clarification link:
258
269
 
@@ -296,3 +307,15 @@ npm run verify
296
307
 
297
308
  `npm run test:package` packs the exact npm artifact, installs it into a clean
298
309
  temporary project, and runs the installed binary.
310
+
311
+ ## Releasing
312
+
313
+ Releases are published only through the trusted `Publish npm release` GitHub
314
+ workflow from a reviewed `v*` tag whose version matches `package.json`. The
315
+ workflow runs the full verification suite before publishing.
316
+
317
+ After npm accepts the package, the workflow verifies the exact published
318
+ version with `npm run test:registry -- VERSION`. That script runs `npm exec`
319
+ from a fresh temporary directory with an isolated npm cache, so a checkout's
320
+ older local `sloth-agent` executable cannot satisfy the registry smoke test.
321
+ The temporary directory is removed after the check.
package/dist/args.js CHANGED
@@ -411,10 +411,10 @@ function helpTopic(argv) {
411
411
  return 'goals-delete';
412
412
  return 'goals';
413
413
  }
414
- if (command === 'categories'
414
+ if (command === 'accounts'
415
+ || command === 'categories'
415
416
  || command === 'transactions'
416
417
  || command === 'assign'
417
- || command === 'joint-budget-settings'
418
418
  || command === 'ask-partner') {
419
419
  return command;
420
420
  }
@@ -443,6 +443,12 @@ export function parseArgs(argv) {
443
443
  }
444
444
  return withBaseUrl({ command }, baseUrl);
445
445
  }
446
+ if (command === 'accounts') {
447
+ if (args.length > 0) {
448
+ throw new UsageError(`Unknown accounts option: ${args[0]}`);
449
+ }
450
+ return withBaseUrl({ command }, baseUrl);
451
+ }
446
452
  if (command === 'transactions') {
447
453
  return withBaseUrl({ command, filters: parseTransactions(args) }, baseUrl);
448
454
  }
@@ -471,43 +477,6 @@ export function parseArgs(argv) {
471
477
  throw new UsageError('assign requires --input <file>');
472
478
  return withBaseUrl({ command, input, apply }, baseUrl);
473
479
  }
474
- if (command === 'joint-budget-settings') {
475
- let includeSharedPersonalTransactions;
476
- let apply = false;
477
- for (let index = 0; index < args.length; index += 1) {
478
- const argument = args[index];
479
- if (argument === '--apply') {
480
- if (apply)
481
- throw new UsageError('--apply may only be provided once');
482
- apply = true;
483
- continue;
484
- }
485
- const optionName = '--include-shared-personal-transactions';
486
- if (argument === optionName || argument.startsWith(`${optionName}=`)) {
487
- const value = argument === optionName
488
- ? readOptionValue(args, index, optionName)
489
- : argument.slice(`${optionName}=`.length);
490
- if (argument === optionName)
491
- index += 1;
492
- if (value !== 'true' && value !== 'false') {
493
- throw new UsageError(`${optionName} must be true or false`);
494
- }
495
- includeSharedPersonalTransactions = setOnce(includeSharedPersonalTransactions, value === 'true', optionName);
496
- continue;
497
- }
498
- throw new UsageError(`Unknown joint-budget-settings option: ${argument}`);
499
- }
500
- if (apply && includeSharedPersonalTransactions === undefined) {
501
- throw new UsageError('--apply requires --include-shared-personal-transactions=true|false');
502
- }
503
- return withBaseUrl({
504
- command,
505
- ...(includeSharedPersonalTransactions === undefined
506
- ? {}
507
- : { includeSharedPersonalTransactions }),
508
- apply,
509
- }, baseUrl);
510
- }
511
480
  if (command === 'ask-partner') {
512
481
  let transactionRef;
513
482
  for (let index = 0; index < args.length; index += 1) {
package/dist/cli.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import fs from 'node:fs';
2
2
  import { parseArgs, resolveBaseUrl, } from './args.js';
3
- import { parseApiResponse, validateAssignmentPayload, validateJointBudgetSettingsResponse, } from './contracts.js';
3
+ import { parseApiResponse, validateAssignmentPayload, } from './contracts.js';
4
4
  import { createSystemCredentialStore, secureStorageUnavailableError, } from './credential-store.js';
5
5
  import { ApiError, CliError, ConfigError, UsageError, } from './errors.js';
6
- export const CLI_VERSION = '0.3.1';
6
+ export const CLI_VERSION = '0.4.0';
7
7
  const REQUEST_TIMEOUT_MS = 60_000;
8
8
  const API_ORIGIN_HELP_LINES = [
9
9
  '',
@@ -24,12 +24,12 @@ export function usageText() {
24
24
  ' sloth-agent auth login [--token-stdin | --from-env] [--base-url URL]',
25
25
  ' sloth-agent auth status [--base-url URL]',
26
26
  ' sloth-agent auth logout [--base-url URL]',
27
+ ' sloth-agent accounts [--base-url URL]',
27
28
  ' sloth-agent categories [--base-url URL]',
28
29
  ' sloth-agent transactions [--uncategorized[=true|false]] [--limit N]',
29
30
  ' [--start-date YYYY-MM-DD] [--end-date YYYY-MM-DD] [--q TEXT]',
30
31
  ' [--account-id ID] [--category-id ID] [--cursor CURSOR] [--base-url URL]',
31
32
  ' sloth-agent assign --input assignments.json [--apply] [--base-url URL]',
32
- ' sloth-agent joint-budget-settings [--include-shared-personal-transactions=true|false] [--apply]',
33
33
  ' sloth-agent goals [list] [--base-url URL]',
34
34
  ' sloth-agent goals create --name NAME [--target-amount AMOUNT]',
35
35
  ' [--target-month YYYY-MM] [--apply] [--base-url URL]',
@@ -182,6 +182,31 @@ export function categoriesHelpText() {
182
182
  ' jointLineItemsByCategoryId Joint child line items keyed by category ID',
183
183
  ].join('\n');
184
184
  }
185
+ export function accountsHelpText() {
186
+ return [
187
+ 'Sloth Agent CLI — accounts',
188
+ '',
189
+ 'Read the existing Sloth account inventory known to the authenticated user.',
190
+ '',
191
+ 'Usage:',
192
+ ' sloth-agent accounts [--base-url URL]',
193
+ '',
194
+ 'Options:',
195
+ ' --base-url URL Optional. Override the API origin.',
196
+ ' -h, --help Show this help.',
197
+ ...API_ORIGIN_HELP_LINES,
198
+ '',
199
+ 'Access:',
200
+ ' This command is read-only and does not refresh connected accounts.',
201
+ '',
202
+ 'Output:',
203
+ ' asOf Server response time',
204
+ ' accounts[].accountRef Opaque stable account reference',
205
+ ' accounts[].ownership personal or joint',
206
+ ' accounts[].balanceAmount and currency in the native currency when known',
207
+ ' accounts[].connectionState and lastBalanceUpdatedAt for freshness',
208
+ ].join('\n');
209
+ }
185
210
  export function transactionsHelpText() {
186
211
  return [
187
212
  'Sloth Agent CLI — transactions',
@@ -288,37 +313,6 @@ export function assignHelpText() {
288
313
  ' Assignments do not create a separate list.',
289
314
  ].join('\n');
290
315
  }
291
- export function jointBudgetSettingsHelpText() {
292
- return [
293
- 'Sloth Agent CLI — joint-budget-settings',
294
- '',
295
- 'Read or update whether shared personal transactions count in the linked joint budget.',
296
- '',
297
- 'Usage:',
298
- ' sloth-agent joint-budget-settings [options]',
299
- '',
300
- 'Options:',
301
- ' --include-shared-personal-transactions=true|false',
302
- ' Optional. Preview the linked setting change.',
303
- ' --apply Optional. Apply the previewed setting change.',
304
- ' --base-url URL Optional. Override the API origin.',
305
- ' -h, --help Show this help.',
306
- ...API_ORIGIN_HELP_LINES,
307
- '',
308
- 'Safety:',
309
- ' With no setting option, the command is read-only.',
310
- ' Without --apply, a setting option returns a dry-run preview and does not write.',
311
- ' --apply requires an explicit true or false setting value.',
312
- '',
313
- 'Examples:',
314
- ' sloth-agent joint-budget-settings',
315
- ' sloth-agent joint-budget-settings --include-shared-personal-transactions=true',
316
- ' sloth-agent joint-budget-settings --include-shared-personal-transactions=true --apply',
317
- '',
318
- 'Output:',
319
- ' JSON containing the linked setting, audit metadata, or a dry-run payload.',
320
- ].join('\n');
321
- }
322
316
  export function goalsHelpText() {
323
317
  return [
324
318
  'Sloth Agent CLI — goals',
@@ -489,10 +483,10 @@ export function commandHelpText(topic) {
489
483
  'auth-login': authLoginHelpText,
490
484
  'auth-status': authStatusHelpText,
491
485
  'auth-logout': authLogoutHelpText,
486
+ accounts: accountsHelpText,
492
487
  categories: categoriesHelpText,
493
488
  transactions: transactionsHelpText,
494
489
  assign: assignHelpText,
495
- 'joint-budget-settings': jointBudgetSettingsHelpText,
496
490
  goals: goalsHelpText,
497
491
  'goals-list': goalsListHelpText,
498
492
  'goals-create': goalsCreateHelpText,
@@ -847,36 +841,6 @@ export async function runCli(argv = process.argv.slice(2), options = {}) {
847
841
  writeJson(writeStdout, data);
848
842
  return hasFailures(data) ? 1 : 0;
849
843
  }
850
- if (parsed.command === 'joint-budget-settings') {
851
- const endpoint = `${baseUrl}/api/agent/v1/joint-budget-settings`;
852
- if (parsed.includeSharedPersonalTransactions !== undefined && !parsed.apply) {
853
- writeJson(writeStdout, {
854
- dryRun: true,
855
- endpoint,
856
- payload: {
857
- includeSharedPersonalTransactions: parsed.includeSharedPersonalTransactions,
858
- },
859
- });
860
- return 0;
861
- }
862
- const response = await fetchImplementation(endpoint, {
863
- method: parsed.apply ? 'PUT' : 'GET',
864
- headers: parsed.apply
865
- ? { ...headers, 'Content-Type': 'application/json' }
866
- : headers,
867
- ...(parsed.apply
868
- ? {
869
- body: JSON.stringify({
870
- includeSharedPersonalTransactions: parsed.includeSharedPersonalTransactions,
871
- }),
872
- }
873
- : {}),
874
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
875
- });
876
- const data = validateJointBudgetSettingsResponse(await parseHttpResponse(response, token));
877
- writeJson(writeStdout, data);
878
- return 0;
879
- }
880
844
  if (parsed.command === 'ask-partner') {
881
845
  const response = await fetchImplementation(`${baseUrl}/api/agent/v1/transaction-explanation-requests`, {
882
846
  method: 'POST',
@@ -901,12 +865,14 @@ export async function runCli(argv = process.argv.slice(2), options = {}) {
901
865
  writeJson(writeStdout, data);
902
866
  return 0;
903
867
  }
904
- const path = parsed.command === 'categories'
905
- ? '/api/agent/v1/categories'
906
- : `/api/agent/v1/transactions${(() => {
907
- const query = buildTransactionsQuery(parsed.filters);
908
- return query ? `?${query}` : '';
909
- })()}`;
868
+ const path = parsed.command === 'accounts'
869
+ ? '/api/agent/v1/accounts'
870
+ : parsed.command === 'categories'
871
+ ? '/api/agent/v1/categories'
872
+ : `/api/agent/v1/transactions${(() => {
873
+ const query = buildTransactionsQuery(parsed.filters);
874
+ return query ? `?${query}` : '';
875
+ })()}`;
910
876
  const response = await fetchImplementation(`${baseUrl}${path}`, {
911
877
  method: 'GET',
912
878
  headers: parsed.command === 'transactions'
package/dist/contracts.js CHANGED
@@ -145,6 +145,8 @@ function isTransaction(value) {
145
145
  && (value.categoryId === null || typeof value.categoryId === 'string')
146
146
  && (value.lineItemId === null || typeof value.lineItemId === 'string')
147
147
  && Array.isArray(value.categorySplits)
148
+ && Number.isInteger(value.personalBudgetAmountPence)
149
+ && Number(value.personalBudgetAmountPence) >= 0
148
150
  && (value.jointBudgetContribution === null
149
151
  || (isObject(value.jointBudgetContribution)
150
152
  && typeof value.jointBudgetContribution.eligible === 'boolean'
@@ -197,20 +199,6 @@ function isAssignmentResponse(value) {
197
199
  && typeof item.error === 'string'
198
200
  && (item.transactionRef === undefined || typeof item.transactionRef === 'string'))));
199
201
  }
200
- export function validateJointBudgetSettingsResponse(value) {
201
- if (!isObject(value)
202
- || Object.keys(value).some((key) => !new Set([
203
- 'includeSharedPersonalTransactions',
204
- 'updatedAt',
205
- 'updatedBy',
206
- ]).has(key))
207
- || typeof value.includeSharedPersonalTransactions !== 'boolean'
208
- || (value.updatedAt !== null && !isIsoDateTime(value.updatedAt))
209
- || (value.updatedBy !== null && typeof value.updatedBy !== 'string')) {
210
- throw new ApiError('Invalid joint budget settings response from the Agent API');
211
- }
212
- return value;
213
- }
214
202
  function isHttpUrl(value) {
215
203
  if (typeof value !== 'string')
216
204
  return false;
@@ -266,6 +254,51 @@ function isGoal(value) {
266
254
  function isCurrency(value) {
267
255
  return typeof value === 'string' && /^[A-Z]{3}$/.test(value);
268
256
  }
257
+ function isNullableNonEmptyString(value) {
258
+ return value === null || (typeof value === 'string'
259
+ && value.length > 0
260
+ && value === value.trim());
261
+ }
262
+ function isAccount(value) {
263
+ return (isObject(value)
264
+ && hasOnlyFields(value, [
265
+ 'accountRef',
266
+ 'accountName',
267
+ 'institutionName',
268
+ 'accountType',
269
+ 'ownership',
270
+ 'balanceAmount',
271
+ 'currency',
272
+ 'source',
273
+ 'lastBalanceUpdatedAt',
274
+ 'connectionState',
275
+ ])
276
+ && typeof value.accountRef === 'string'
277
+ && /^sloth_account_v1_[A-Za-z0-9_-]{43}$/.test(value.accountRef)
278
+ && isNullableNonEmptyString(value.accountName)
279
+ && isNullableNonEmptyString(value.institutionName)
280
+ && (value.accountType === 'current'
281
+ || value.accountType === 'savings'
282
+ || value.accountType === 'investments')
283
+ && (value.ownership === 'personal' || value.ownership === 'joint')
284
+ && (value.balanceAmount === null
285
+ || (typeof value.balanceAmount === 'number' && Number.isFinite(value.balanceAmount)))
286
+ && (value.currency === null || isCurrency(value.currency))
287
+ && (value.source === 'connected' || value.source === 'manual')
288
+ && (value.lastBalanceUpdatedAt === null
289
+ || isIsoDateTime(value.lastBalanceUpdatedAt))
290
+ && (value.connectionState === 'active'
291
+ || value.connectionState === 'expired'
292
+ || value.connectionState === 'manual'
293
+ || value.connectionState === 'unknown'));
294
+ }
295
+ function isAccountsResponse(value) {
296
+ return (isObject(value)
297
+ && hasOnlyFields(value, ['asOf', 'accounts'])
298
+ && isIsoDateTime(value.asOf)
299
+ && Array.isArray(value.accounts)
300
+ && value.accounts.every(isAccount));
301
+ }
269
302
  function isGoalsResponse(value) {
270
303
  return (isObject(value)
271
304
  && hasOnlyFields(value, ['currency', 'goals'])
@@ -287,19 +320,21 @@ function isGoalDeleteResponse(value) {
287
320
  && value.deletedGoalId.trim().length > 0);
288
321
  }
289
322
  export function parseApiResponse(command, value) {
290
- const valid = command === 'categories'
291
- ? isCategoryResponse(value)
292
- : command === 'transactions'
293
- ? isTransactionsResponse(value)
294
- : command === 'assign'
295
- ? isAssignmentResponse(value)
296
- : command === 'ask-partner'
297
- ? isPartnerResponse(value)
298
- : command === 'goals-list'
299
- ? isGoalsResponse(value)
300
- : command === 'goals-delete'
301
- ? isGoalDeleteResponse(value)
302
- : isGoalMutationResponse(value);
323
+ const valid = command === 'accounts'
324
+ ? isAccountsResponse(value)
325
+ : command === 'categories'
326
+ ? isCategoryResponse(value)
327
+ : command === 'transactions'
328
+ ? isTransactionsResponse(value)
329
+ : command === 'assign'
330
+ ? isAssignmentResponse(value)
331
+ : command === 'ask-partner'
332
+ ? isPartnerResponse(value)
333
+ : command === 'goals-list'
334
+ ? isGoalsResponse(value)
335
+ : command === 'goals-delete'
336
+ ? isGoalDeleteResponse(value)
337
+ : isGoalMutationResponse(value);
303
338
  if (!valid) {
304
339
  const label = command === 'assign' ? 'assignment' : command;
305
340
  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.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "Command-line access to the Sloth Money Agent API.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -16,6 +16,7 @@
16
16
  "prepack": "npm run build",
17
17
  "test": "vitest run",
18
18
  "test:package": "node scripts/test-package.mjs",
19
+ "test:registry": "node scripts/test-registry-package.mjs",
19
20
  "typecheck": "tsc --noEmit",
20
21
  "verify": "npm run lint && npm run typecheck && npm test && npm run build && npm run test:package"
21
22
  },