@slothmoney/agent-cli 0.14.0 → 0.15.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 +18 -0
- package/README.md +57 -6
- package/dist/args.js +57 -8
- package/dist/cli.js +289 -8
- package/dist/contracts.js +113 -12
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.15.0 - 2026-08-21
|
|
4
|
+
|
|
5
|
+
- Add `rules` commands to list, read, preview, apply, and delete notification
|
|
6
|
+
rules anchored to existing transaction references.
|
|
7
|
+
- Add optional one-time PDF renewal-date extraction. Preview mode keeps the
|
|
8
|
+
file local; apply mode sends it for extraction and the service discards it
|
|
9
|
+
without storing the contract.
|
|
10
|
+
- Keep recurring predictions and transaction creation outside the Rules
|
|
11
|
+
contract.
|
|
12
|
+
- Remove the legacy transaction `--account-id` filter and stop exposing
|
|
13
|
+
internal account and connection locator fields in transaction responses.
|
|
14
|
+
Use the opaque `accountRef` returned by `sloth-agent accounts`.
|
|
15
|
+
|
|
16
|
+
## 0.14.1 - 2026-08-21
|
|
17
|
+
|
|
18
|
+
- Make every parent help page list its nested commands, and give
|
|
19
|
+
`line-items --help` its own group page instead of generic top-level usage.
|
|
20
|
+
|
|
3
21
|
## 0.14.0 - 2026-08-20
|
|
4
22
|
|
|
5
23
|
- Add `transactions --account-ref REF` using the opaque reference returned by
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Sloth Agent CLI
|
|
2
2
|
|
|
3
|
-
Use your own agent to inspect accounts, investments, and budgets, manage goals, move assigned budget money, update planned amounts,
|
|
3
|
+
Use your own agent to inspect accounts, investments, and budgets, manage goals, move assigned budget money, update planned amounts, categorise transactions, and configure payment notifications 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.
|
|
18
|
+
npm exec --yes --package=@slothmoney/agent-cli@0.15.0 -- sloth-agent --help
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
## Authenticate
|
|
@@ -98,7 +98,7 @@ An assignment can change an owned transaction's sharing, categorisation, or
|
|
|
98
98
|
both.
|
|
99
99
|
|
|
100
100
|
Every command has built-in reference documentation covering its inputs,
|
|
101
|
-
options, output, and examples:
|
|
101
|
+
options, output, and examples. For example:
|
|
102
102
|
|
|
103
103
|
```bash
|
|
104
104
|
sloth-agent auth login --help
|
|
@@ -109,8 +109,10 @@ sloth-agent budget update --help
|
|
|
109
109
|
sloth-agent budget move --help
|
|
110
110
|
sloth-agent categories --help
|
|
111
111
|
sloth-agent categories create --help
|
|
112
|
+
sloth-agent line-items --help
|
|
112
113
|
sloth-agent line-items create --help
|
|
113
114
|
sloth-agent transactions --help
|
|
115
|
+
sloth-agent rules --help
|
|
114
116
|
sloth-agent assign --help
|
|
115
117
|
sloth-agent goals create --help
|
|
116
118
|
sloth-agent goals update --help
|
|
@@ -119,6 +121,56 @@ sloth-agent goals restore --help
|
|
|
119
121
|
sloth-agent ask-partner --help
|
|
120
122
|
```
|
|
121
123
|
|
|
124
|
+
### Add a transaction notification rule
|
|
125
|
+
|
|
126
|
+
Rules watch future payments that match an existing transaction. They can alert
|
|
127
|
+
you when the amount changes or before a renewal date. They do not create
|
|
128
|
+
transactions or recurring predictions.
|
|
129
|
+
|
|
130
|
+
First, run `sloth-agent transactions` and copy the exact `transactionRef` into
|
|
131
|
+
`rule.json` alongside this rule definition:
|
|
132
|
+
|
|
133
|
+
```json
|
|
134
|
+
{
|
|
135
|
+
"amountChange": {
|
|
136
|
+
"enabled": true,
|
|
137
|
+
"comparison": "increase",
|
|
138
|
+
"baselinePence": 3184
|
|
139
|
+
},
|
|
140
|
+
"renewalReminder": {
|
|
141
|
+
"enabled": true,
|
|
142
|
+
"renewalDate": "2027-07-30",
|
|
143
|
+
"leadDays": 30
|
|
144
|
+
},
|
|
145
|
+
"delivery": {
|
|
146
|
+
"inApp": true,
|
|
147
|
+
"email": true
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Preview the write locally, then apply the same validated file:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
sloth-agent rules set \
|
|
156
|
+
--transaction-ref PASTE_THE_EXACT_TRANSACTION_REF_HERE \
|
|
157
|
+
--input rule.json
|
|
158
|
+
|
|
159
|
+
sloth-agent rules set \
|
|
160
|
+
--transaction-ref PASTE_THE_EXACT_TRANSACTION_REF_HERE \
|
|
161
|
+
--input rule.json \
|
|
162
|
+
--apply
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
To extract a renewal date first, scan a PDF no larger than 6 MB:
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
sloth-agent rules scan-contract --contract contract.pdf --apply
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
The PDF is discarded after extraction and is not stored. Scanning only returns
|
|
172
|
+
the date and confidence; use `rules set` to save the resulting reminder.
|
|
173
|
+
|
|
122
174
|
### How transaction categorisation is represented
|
|
123
175
|
|
|
124
176
|
Personal and joint category assignments are separate. A personal assignment
|
|
@@ -481,9 +533,8 @@ sloth-agent transactions \
|
|
|
481
533
|
--limit 50
|
|
482
534
|
```
|
|
483
535
|
|
|
484
|
-
Copy the value from `sloth-agent accounts`.
|
|
485
|
-
|
|
486
|
-
`--account-ref`. Do not provide both filters in one command.
|
|
536
|
+
Copy the value from `sloth-agent accounts`. Account references are the public
|
|
537
|
+
account identifier for transaction filtering.
|
|
487
538
|
|
|
488
539
|
Account changes are previews unless `--apply` is present. Connected accounts
|
|
489
540
|
support only goal-savings membership. Manual current accounts support their
|
package/dist/args.js
CHANGED
|
@@ -177,7 +177,6 @@ function parseTransactions(args) {
|
|
|
177
177
|
'--end-date',
|
|
178
178
|
'--q',
|
|
179
179
|
'--account-ref',
|
|
180
|
-
'--account-id',
|
|
181
180
|
'--category-id',
|
|
182
181
|
'--line-item-id',
|
|
183
182
|
'--assignment-scope',
|
|
@@ -213,9 +212,6 @@ function parseTransactions(args) {
|
|
|
213
212
|
else if (name === '--account-ref') {
|
|
214
213
|
filters.accountRef = setOnce(filters.accountRef, parseAccountRef(value), name);
|
|
215
214
|
}
|
|
216
|
-
else if (name === '--account-id') {
|
|
217
|
-
filters.accountId = setOnce(filters.accountId, value, name);
|
|
218
|
-
}
|
|
219
215
|
else if (name === '--category-id') {
|
|
220
216
|
filters.categoryId = setOnce(filters.categoryId, value, name);
|
|
221
217
|
}
|
|
@@ -237,9 +233,6 @@ function parseTransactions(args) {
|
|
|
237
233
|
&& filters.endDate < filters.startDate) {
|
|
238
234
|
throw new UsageError('--end-date must not be before --start-date');
|
|
239
235
|
}
|
|
240
|
-
if (filters.accountRef !== undefined && filters.accountId !== undefined) {
|
|
241
|
-
throw new UsageError('Use either --account-ref or --account-id, not both');
|
|
242
|
-
}
|
|
243
236
|
return filters;
|
|
244
237
|
}
|
|
245
238
|
function parseNamedOptions(args, commandLabel, allowed) {
|
|
@@ -752,6 +745,48 @@ function parseGoals(args, baseUrl) {
|
|
|
752
745
|
}
|
|
753
746
|
throw new UsageError(`Unknown goals command: ${subcommand}`);
|
|
754
747
|
}
|
|
748
|
+
function parseRules(args, baseUrl) {
|
|
749
|
+
const subcommand = args.shift();
|
|
750
|
+
if (subcommand === undefined || subcommand === 'list') {
|
|
751
|
+
if (args.length > 0) {
|
|
752
|
+
throw new UsageError(`Unknown rules list option: ${args[0]}`);
|
|
753
|
+
}
|
|
754
|
+
return withBaseUrl({ command: 'rules-list' }, baseUrl);
|
|
755
|
+
}
|
|
756
|
+
if (subcommand === 'get' || subcommand === 'delete') {
|
|
757
|
+
const allowed = new Set(['--transaction-ref']);
|
|
758
|
+
const { values, apply } = parseNamedOptions(args, `rules ${subcommand}`, allowed);
|
|
759
|
+
const transactionRef = requiredOption(values, '--transaction-ref', `rules ${subcommand}`);
|
|
760
|
+
if (subcommand === 'get') {
|
|
761
|
+
if (apply)
|
|
762
|
+
throw new UsageError('rules get does not accept --apply');
|
|
763
|
+
return withBaseUrl({ command: 'rules-get', transactionRef }, baseUrl);
|
|
764
|
+
}
|
|
765
|
+
return withBaseUrl({
|
|
766
|
+
command: 'rules-delete',
|
|
767
|
+
transactionRef,
|
|
768
|
+
apply,
|
|
769
|
+
}, baseUrl);
|
|
770
|
+
}
|
|
771
|
+
if (subcommand === 'set') {
|
|
772
|
+
const { values, apply } = parseNamedOptions(args, 'rules set', new Set(['--transaction-ref', '--input']));
|
|
773
|
+
return withBaseUrl({
|
|
774
|
+
command: 'rules-set',
|
|
775
|
+
transactionRef: requiredOption(values, '--transaction-ref', 'rules set'),
|
|
776
|
+
input: requiredOption(values, '--input', 'rules set'),
|
|
777
|
+
apply,
|
|
778
|
+
}, baseUrl);
|
|
779
|
+
}
|
|
780
|
+
if (subcommand === 'scan-contract') {
|
|
781
|
+
const { values, apply } = parseNamedOptions(args, 'rules scan-contract', new Set(['--contract']));
|
|
782
|
+
return withBaseUrl({
|
|
783
|
+
command: 'rules-scan-contract',
|
|
784
|
+
contract: requiredOption(values, '--contract', 'rules scan-contract'),
|
|
785
|
+
apply,
|
|
786
|
+
}, baseUrl);
|
|
787
|
+
}
|
|
788
|
+
throw new UsageError(`Unknown rules command: ${subcommand}`);
|
|
789
|
+
}
|
|
755
790
|
function helpTopic(argv) {
|
|
756
791
|
const positionals = [];
|
|
757
792
|
for (let index = 0; index < argv.length; index += 1) {
|
|
@@ -791,6 +826,17 @@ function helpTopic(argv) {
|
|
|
791
826
|
return 'goals-delete';
|
|
792
827
|
return 'goals';
|
|
793
828
|
}
|
|
829
|
+
if (command === 'rules') {
|
|
830
|
+
if (subcommand === 'get')
|
|
831
|
+
return 'rules-get';
|
|
832
|
+
if (subcommand === 'set')
|
|
833
|
+
return 'rules-set';
|
|
834
|
+
if (subcommand === 'delete')
|
|
835
|
+
return 'rules-delete';
|
|
836
|
+
if (subcommand === 'scan-contract')
|
|
837
|
+
return 'rules-scan-contract';
|
|
838
|
+
return 'rules';
|
|
839
|
+
}
|
|
794
840
|
if (command === 'categories') {
|
|
795
841
|
if (subcommand === 'create')
|
|
796
842
|
return 'categories-create';
|
|
@@ -803,7 +849,7 @@ function helpTopic(argv) {
|
|
|
803
849
|
return 'line-items-create';
|
|
804
850
|
if (subcommand === 'rename')
|
|
805
851
|
return 'line-items-rename';
|
|
806
|
-
return
|
|
852
|
+
return 'line-items';
|
|
807
853
|
}
|
|
808
854
|
if (command === 'budget') {
|
|
809
855
|
if (subcommand === 'status')
|
|
@@ -845,6 +891,9 @@ export function parseArgs(argv) {
|
|
|
845
891
|
if (command === 'goals') {
|
|
846
892
|
return parseGoals(args, baseUrl);
|
|
847
893
|
}
|
|
894
|
+
if (command === 'rules') {
|
|
895
|
+
return parseRules(args, baseUrl);
|
|
896
|
+
}
|
|
848
897
|
if (command === 'categories') {
|
|
849
898
|
return parseCategories(args, baseUrl);
|
|
850
899
|
}
|
package/dist/cli.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import { parseArgs, resolveBaseUrl, } from './args.js';
|
|
3
3
|
import { ICON_KEYS } from './category-metadata.js';
|
|
4
|
-
import { parseApiResponse, validateAssignmentPayload, validateBudgetMovementResponse, validateBudgetUpdatePayload, } from './contracts.js';
|
|
4
|
+
import { parseApiResponse, validateAssignmentPayload, validateBudgetMovementResponse, validateBudgetUpdatePayload, validateNotificationRulePayload, } 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.15.0';
|
|
8
8
|
const REQUEST_TIMEOUT_MS = 60_000;
|
|
9
9
|
const API_ORIGIN_HELP_LINES = [
|
|
10
10
|
'',
|
|
@@ -42,9 +42,14 @@ export function usageText() {
|
|
|
42
42
|
' sloth-agent line-items rename --scope personal|joint --category-id ID --line-item-id ID --name NAME [--apply]',
|
|
43
43
|
' sloth-agent transactions [--uncategorized[=true|false]] [--shared[=true|false]] [--limit N]',
|
|
44
44
|
' [--start-date YYYY-MM-DD] [--end-date YYYY-MM-DD] [--q TEXT]',
|
|
45
|
-
' [--account-ref REF] [--
|
|
45
|
+
' [--account-ref REF] [--category-id ID] [--line-item-id ID]',
|
|
46
46
|
' [--cursor CURSOR] [--base-url URL]',
|
|
47
47
|
' sloth-agent assign --input assignments.json [--apply] [--base-url URL]',
|
|
48
|
+
' sloth-agent rules [list] [--base-url URL]',
|
|
49
|
+
' sloth-agent rules get --transaction-ref REF [--base-url URL]',
|
|
50
|
+
' sloth-agent rules set --transaction-ref REF --input rule.json [--apply]',
|
|
51
|
+
' sloth-agent rules delete --transaction-ref REF [--apply]',
|
|
52
|
+
' sloth-agent rules scan-contract --contract FILE.pdf [--apply]',
|
|
48
53
|
' sloth-agent goals [list] [--base-url URL]',
|
|
49
54
|
' sloth-agent goals create --name NAME --target-amount AMOUNT',
|
|
50
55
|
' --type keep|spend [--target-month YYYY-MM] [--apply] [--base-url URL]',
|
|
@@ -56,7 +61,7 @@ export function usageText() {
|
|
|
56
61
|
'',
|
|
57
62
|
'Help:',
|
|
58
63
|
' Run sloth-agent <command> --help for options, inputs, output, and examples.',
|
|
59
|
-
'
|
|
64
|
+
' Every nested subcommand has its own help, for example:',
|
|
60
65
|
' sloth-agent auth login --help',
|
|
61
66
|
' sloth-agent goals update --help',
|
|
62
67
|
'',
|
|
@@ -176,6 +181,14 @@ export function categoriesHelpText() {
|
|
|
176
181
|
'',
|
|
177
182
|
'Read categories and the personal and joint line items within them.',
|
|
178
183
|
'',
|
|
184
|
+
'Commands:',
|
|
185
|
+
' sloth-agent categories list List categories; "sloth-agent categories" is equivalent.',
|
|
186
|
+
' sloth-agent categories create Preview or create a custom category.',
|
|
187
|
+
' sloth-agent categories rename Preview or rename a custom category.',
|
|
188
|
+
'',
|
|
189
|
+
'Help:',
|
|
190
|
+
' Run sloth-agent categories <command> --help for command-specific details.',
|
|
191
|
+
'',
|
|
179
192
|
'Usage:',
|
|
180
193
|
' sloth-agent categories [list] [--base-url URL]',
|
|
181
194
|
'',
|
|
@@ -286,12 +299,35 @@ export function lineItemsCreateHelpText() {
|
|
|
286
299
|
export function lineItemsRenameHelpText() {
|
|
287
300
|
return lineItemsMutationHelpText('rename');
|
|
288
301
|
}
|
|
302
|
+
export function lineItemsHelpText() {
|
|
303
|
+
return [
|
|
304
|
+
'Sloth Agent CLI — line-items',
|
|
305
|
+
'',
|
|
306
|
+
'Create or rename personal or joint budget line items.',
|
|
307
|
+
'',
|
|
308
|
+
'Commands:',
|
|
309
|
+
' sloth-agent line-items create Preview or create a scoped line item.',
|
|
310
|
+
' sloth-agent line-items rename Preview or rename a scoped line item.',
|
|
311
|
+
'',
|
|
312
|
+
'Help:',
|
|
313
|
+
' Run sloth-agent line-items <command> --help for command-specific details.',
|
|
314
|
+
...API_ORIGIN_HELP_LINES,
|
|
315
|
+
].join('\n');
|
|
316
|
+
}
|
|
289
317
|
export function accountsHelpText() {
|
|
290
318
|
return [
|
|
291
319
|
'Sloth Agent CLI — accounts',
|
|
292
320
|
'',
|
|
293
321
|
'Read the existing Sloth account inventory known to the authenticated user.',
|
|
294
322
|
'',
|
|
323
|
+
'Commands:',
|
|
324
|
+
' sloth-agent accounts list List accounts; "sloth-agent accounts" is equivalent.',
|
|
325
|
+
' sloth-agent accounts update Preview or update an owned account.',
|
|
326
|
+
' sloth-agent accounts remove Preview or archive an owned manual account.',
|
|
327
|
+
'',
|
|
328
|
+
'Help:',
|
|
329
|
+
' Run sloth-agent accounts <command> --help for command-specific details.',
|
|
330
|
+
'',
|
|
295
331
|
'Usage:',
|
|
296
332
|
' sloth-agent accounts [list] [--base-url URL]',
|
|
297
333
|
'',
|
|
@@ -402,6 +438,15 @@ export function budgetHelpText() {
|
|
|
402
438
|
'',
|
|
403
439
|
'Read one personal or joint budget period.',
|
|
404
440
|
'',
|
|
441
|
+
'Commands:',
|
|
442
|
+
' sloth-agent budget Read one budget period.',
|
|
443
|
+
' sloth-agent budget status Read assigned, spent, and available money.',
|
|
444
|
+
' sloth-agent budget update Preview or update planned line-item amounts.',
|
|
445
|
+
' sloth-agent budget move Preview or move assigned money.',
|
|
446
|
+
'',
|
|
447
|
+
'Help:',
|
|
448
|
+
' Run sloth-agent budget <command> --help for command-specific details.',
|
|
449
|
+
'',
|
|
405
450
|
'Usage:',
|
|
406
451
|
' sloth-agent budget --scope personal|joint [--period YYYY-MM] [--base-url URL]',
|
|
407
452
|
'',
|
|
@@ -549,7 +594,6 @@ export function transactionsHelpText() {
|
|
|
549
594
|
' --q TEXT Optional. Search transactions by text.',
|
|
550
595
|
' --account-ref REF Optional. Filter by the opaque accountRef from sloth-agent accounts.',
|
|
551
596
|
' Copy the exact sloth_account_v1_... value.',
|
|
552
|
-
' --account-id ID Optional legacy filter by provider or stored account ID.',
|
|
553
597
|
' --category-id ID Optional. Filter by category ID.',
|
|
554
598
|
' --line-item-id ID Optional. Filter primary or split assignments by line-item ID.',
|
|
555
599
|
' --assignment-scope SCOPE Optional. Filter assignments by personal or joint.',
|
|
@@ -561,7 +605,6 @@ export function transactionsHelpText() {
|
|
|
561
605
|
'',
|
|
562
606
|
'Constraints:',
|
|
563
607
|
' All filters are omitted by default.',
|
|
564
|
-
' Use either --account-ref or --account-id, not both.',
|
|
565
608
|
' --end-date must not be before --start-date.',
|
|
566
609
|
' The first transaction read each UTC day may refresh linked bank data.',
|
|
567
610
|
' Refresh remotely persists booked transactions and account balances.',
|
|
@@ -896,6 +939,135 @@ export function askPartnerHelpText() {
|
|
|
896
939
|
' JSON containing requestId, publicUrl, message, expiresAt, and status.',
|
|
897
940
|
].join('\n');
|
|
898
941
|
}
|
|
942
|
+
export function rulesHelpText() {
|
|
943
|
+
return [
|
|
944
|
+
'Sloth Agent CLI — rules',
|
|
945
|
+
'',
|
|
946
|
+
'Manage notifications for future payments that match an existing transaction.',
|
|
947
|
+
'Rules do not create transactions or recurring predictions.',
|
|
948
|
+
'',
|
|
949
|
+
'Commands:',
|
|
950
|
+
' rules list List every saved notification rule.',
|
|
951
|
+
' rules get Read the rule for one transaction.',
|
|
952
|
+
' rules set Preview or save a rule for one transaction.',
|
|
953
|
+
' rules delete Preview or remove a rule.',
|
|
954
|
+
' rules scan-contract Validate or scan a PDF for its renewal date.',
|
|
955
|
+
'',
|
|
956
|
+
'Use the exact transactionRef returned by sloth-agent transactions.',
|
|
957
|
+
...API_ORIGIN_HELP_LINES,
|
|
958
|
+
].join('\n');
|
|
959
|
+
}
|
|
960
|
+
export function rulesGetHelpText() {
|
|
961
|
+
return [
|
|
962
|
+
'Sloth Agent CLI — rules get',
|
|
963
|
+
'',
|
|
964
|
+
'Read the saved notification rule for one existing transaction.',
|
|
965
|
+
'',
|
|
966
|
+
'Usage:',
|
|
967
|
+
' sloth-agent rules get --transaction-ref REF',
|
|
968
|
+
'',
|
|
969
|
+
'Required:',
|
|
970
|
+
' --transaction-ref REF The exact transactionRef from sloth-agent transactions.',
|
|
971
|
+
'',
|
|
972
|
+
'Behavior:',
|
|
973
|
+
' This command is read-only and requires an agent:read token.',
|
|
974
|
+
'',
|
|
975
|
+
'Output:',
|
|
976
|
+
' JSON containing the saved rule, or rule: null when no rule exists.',
|
|
977
|
+
'',
|
|
978
|
+
'Example:',
|
|
979
|
+
' sloth-agent rules get --transaction-ref PASTE_THE_EXACT_TRANSACTION_REF_HERE',
|
|
980
|
+
...API_ORIGIN_HELP_LINES,
|
|
981
|
+
].join('\n');
|
|
982
|
+
}
|
|
983
|
+
export function rulesSetHelpText() {
|
|
984
|
+
return [
|
|
985
|
+
'Sloth Agent CLI — rules set',
|
|
986
|
+
'',
|
|
987
|
+
'Preview or save notification conditions for one existing transaction.',
|
|
988
|
+
'',
|
|
989
|
+
'Usage:',
|
|
990
|
+
' sloth-agent rules set --transaction-ref REF --input FILE [--apply]',
|
|
991
|
+
'',
|
|
992
|
+
'Required:',
|
|
993
|
+
' --transaction-ref REF The exact transactionRef from sloth-agent transactions.',
|
|
994
|
+
' --input FILE A JSON rule file such as rule.json.',
|
|
995
|
+
'',
|
|
996
|
+
'Input:',
|
|
997
|
+
' {',
|
|
998
|
+
' "amountChange": {',
|
|
999
|
+
' "enabled": true,',
|
|
1000
|
+
' "comparison": "increase",',
|
|
1001
|
+
' "baselinePence": 3184',
|
|
1002
|
+
' },',
|
|
1003
|
+
' "renewalReminder": {',
|
|
1004
|
+
' "enabled": true,',
|
|
1005
|
+
' "renewalDate": "2027-07-30",',
|
|
1006
|
+
' "leadDays": 30',
|
|
1007
|
+
' },',
|
|
1008
|
+
' "delivery": { "inApp": true, "email": true }',
|
|
1009
|
+
' }',
|
|
1010
|
+
'',
|
|
1011
|
+
' comparison accepts increase or any. baselinePence is a positive integer.',
|
|
1012
|
+
' renewalDate is YYYY-MM-DD or null when its reminder is disabled.',
|
|
1013
|
+
' leadDays is an integer from 0 to 365. At least one condition must be enabled.',
|
|
1014
|
+
' delivery.inApp must be true; delivery.email adds email delivery.',
|
|
1015
|
+
'',
|
|
1016
|
+
'Write behavior:',
|
|
1017
|
+
' Without --apply, Sloth validates the file and prints a local preview.',
|
|
1018
|
+
' With --apply, Sloth replaces the saved rule using a write-enabled token.',
|
|
1019
|
+
'',
|
|
1020
|
+
'Example:',
|
|
1021
|
+
' sloth-agent rules set --transaction-ref PASTE_THE_EXACT_TRANSACTION_REF_HERE \\',
|
|
1022
|
+
' --input rule.json --apply',
|
|
1023
|
+
...API_ORIGIN_HELP_LINES,
|
|
1024
|
+
].join('\n');
|
|
1025
|
+
}
|
|
1026
|
+
export function rulesDeleteHelpText() {
|
|
1027
|
+
return [
|
|
1028
|
+
'Sloth Agent CLI — rules delete',
|
|
1029
|
+
'',
|
|
1030
|
+
'Preview or remove the notification rule for one existing transaction.',
|
|
1031
|
+
'',
|
|
1032
|
+
'Usage:',
|
|
1033
|
+
' sloth-agent rules delete --transaction-ref REF [--apply]',
|
|
1034
|
+
'',
|
|
1035
|
+
'Required:',
|
|
1036
|
+
' --transaction-ref REF The exact transactionRef from sloth-agent transactions.',
|
|
1037
|
+
'',
|
|
1038
|
+
'Write behavior:',
|
|
1039
|
+
' Without --apply, Sloth prints a local deletion preview.',
|
|
1040
|
+
' With --apply, Sloth removes the rule using a write-enabled token.',
|
|
1041
|
+
...API_ORIGIN_HELP_LINES,
|
|
1042
|
+
].join('\n');
|
|
1043
|
+
}
|
|
1044
|
+
export function rulesScanContractHelpText() {
|
|
1045
|
+
return [
|
|
1046
|
+
'Sloth Agent CLI — rules scan-contract',
|
|
1047
|
+
'',
|
|
1048
|
+
'Validate or scan a contract PDF for its renewal date.',
|
|
1049
|
+
'',
|
|
1050
|
+
'Usage:',
|
|
1051
|
+
' sloth-agent rules scan-contract --contract FILE.pdf [--apply]',
|
|
1052
|
+
'',
|
|
1053
|
+
'Required:',
|
|
1054
|
+
' --contract FILE.pdf A PDF no larger than 6 MB.',
|
|
1055
|
+
'',
|
|
1056
|
+
'Behavior:',
|
|
1057
|
+
' Without --apply, the PDF is validated locally and is not sent anywhere.',
|
|
1058
|
+
' With --apply, the PDF is sent using a write-enabled token.',
|
|
1059
|
+
'',
|
|
1060
|
+
'Contract privacy:',
|
|
1061
|
+
' Sloth extracts a renewal date and then discards the file. It is not stored.',
|
|
1062
|
+
'',
|
|
1063
|
+
'Output:',
|
|
1064
|
+
' JSON containing renewalDate and confidence: high, medium, or low.',
|
|
1065
|
+
'',
|
|
1066
|
+
'Example:',
|
|
1067
|
+
' sloth-agent rules scan-contract --contract contract.pdf --apply',
|
|
1068
|
+
...API_ORIGIN_HELP_LINES,
|
|
1069
|
+
].join('\n');
|
|
1070
|
+
}
|
|
899
1071
|
export function commandHelpText(topic) {
|
|
900
1072
|
const helpByTopic = {
|
|
901
1073
|
auth: authHelpText,
|
|
@@ -913,10 +1085,16 @@ export function commandHelpText(topic) {
|
|
|
913
1085
|
categories: categoriesHelpText,
|
|
914
1086
|
'categories-create': categoriesCreateHelpText,
|
|
915
1087
|
'categories-rename': categoriesRenameHelpText,
|
|
1088
|
+
'line-items': lineItemsHelpText,
|
|
916
1089
|
'line-items-create': lineItemsCreateHelpText,
|
|
917
1090
|
'line-items-rename': lineItemsRenameHelpText,
|
|
918
1091
|
transactions: transactionsHelpText,
|
|
919
1092
|
assign: assignHelpText,
|
|
1093
|
+
rules: rulesHelpText,
|
|
1094
|
+
'rules-get': rulesGetHelpText,
|
|
1095
|
+
'rules-set': rulesSetHelpText,
|
|
1096
|
+
'rules-delete': rulesDeleteHelpText,
|
|
1097
|
+
'rules-scan-contract': rulesScanContractHelpText,
|
|
920
1098
|
goals: goalsHelpText,
|
|
921
1099
|
'goals-list': goalsListHelpText,
|
|
922
1100
|
'goals-create': goalsCreateHelpText,
|
|
@@ -1023,6 +1201,29 @@ function readBudgetFile(filePath) {
|
|
|
1023
1201
|
throw new UsageError(`Failed to read budget JSON: ${message}`);
|
|
1024
1202
|
}
|
|
1025
1203
|
}
|
|
1204
|
+
function readNotificationRuleFile(filePath) {
|
|
1205
|
+
try {
|
|
1206
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
1207
|
+
}
|
|
1208
|
+
catch (error) {
|
|
1209
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1210
|
+
throw new UsageError(`Failed to read notification rule JSON: ${message}`);
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
function readContractPdf(filePath) {
|
|
1214
|
+
let file;
|
|
1215
|
+
try {
|
|
1216
|
+
file = fs.readFileSync(filePath);
|
|
1217
|
+
}
|
|
1218
|
+
catch (error) {
|
|
1219
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1220
|
+
throw new UsageError(`Failed to read contract PDF: ${message}`);
|
|
1221
|
+
}
|
|
1222
|
+
if (file.length === 0 || file.length > 6 * 1024 * 1024 || file.subarray(0, 4).toString() !== '%PDF') {
|
|
1223
|
+
throw new UsageError('Contract must be a PDF no larger than 6 MB');
|
|
1224
|
+
}
|
|
1225
|
+
return file;
|
|
1226
|
+
}
|
|
1026
1227
|
function buildTransactionsQuery(filters) {
|
|
1027
1228
|
const params = new URLSearchParams();
|
|
1028
1229
|
if (filters.uncategorized !== undefined) {
|
|
@@ -1040,8 +1241,6 @@ function buildTransactionsQuery(filters) {
|
|
|
1040
1241
|
params.set('q', filters.q);
|
|
1041
1242
|
if (filters.accountRef !== undefined)
|
|
1042
1243
|
params.set('accountRef', filters.accountRef);
|
|
1043
|
-
if (filters.accountId !== undefined)
|
|
1044
|
-
params.set('accountId', filters.accountId);
|
|
1045
1244
|
if (filters.categoryId !== undefined)
|
|
1046
1245
|
params.set('categoryId', filters.categoryId);
|
|
1047
1246
|
if (filters.lineItemId !== undefined)
|
|
@@ -1258,6 +1457,43 @@ export async function runCli(argv = process.argv.slice(2), options = {}) {
|
|
|
1258
1457
|
});
|
|
1259
1458
|
return 0;
|
|
1260
1459
|
}
|
|
1460
|
+
const notificationRulePayload = parsed.command === 'rules-set'
|
|
1461
|
+
? validateNotificationRulePayload(readNotificationRuleFile(parsed.input))
|
|
1462
|
+
: undefined;
|
|
1463
|
+
const notificationRuleEndpoint = (parsed.command === 'rules-get'
|
|
1464
|
+
|| parsed.command === 'rules-set'
|
|
1465
|
+
|| parsed.command === 'rules-delete')
|
|
1466
|
+
? `${baseUrl}/api/agent/v1/notification-rules/for-transaction?${new URLSearchParams({
|
|
1467
|
+
transactionRef: parsed.transactionRef,
|
|
1468
|
+
}).toString()}`
|
|
1469
|
+
: undefined;
|
|
1470
|
+
if (parsed.command === 'rules-set' && !parsed.apply) {
|
|
1471
|
+
writeJson(writeStdout, {
|
|
1472
|
+
dryRun: true,
|
|
1473
|
+
endpoint: notificationRuleEndpoint,
|
|
1474
|
+
method: 'PUT',
|
|
1475
|
+
payload: { transactionRef: parsed.transactionRef, ...notificationRulePayload },
|
|
1476
|
+
});
|
|
1477
|
+
return 0;
|
|
1478
|
+
}
|
|
1479
|
+
if (parsed.command === 'rules-delete' && !parsed.apply) {
|
|
1480
|
+
writeJson(writeStdout, {
|
|
1481
|
+
dryRun: true,
|
|
1482
|
+
endpoint: notificationRuleEndpoint,
|
|
1483
|
+
method: 'DELETE',
|
|
1484
|
+
});
|
|
1485
|
+
return 0;
|
|
1486
|
+
}
|
|
1487
|
+
if (parsed.command === 'rules-scan-contract' && !parsed.apply) {
|
|
1488
|
+
const contract = readContractPdf(parsed.contract);
|
|
1489
|
+
writeJson(writeStdout, {
|
|
1490
|
+
dryRun: true,
|
|
1491
|
+
endpoint: `${baseUrl}/api/agent/v1/notification-rules/extract-renewal`,
|
|
1492
|
+
method: 'POST',
|
|
1493
|
+
contract: { bytes: contract.length, mimeType: 'application/pdf' },
|
|
1494
|
+
});
|
|
1495
|
+
return 0;
|
|
1496
|
+
}
|
|
1261
1497
|
const credential = await resolveCredential(environment, baseUrl, getCredentialStore);
|
|
1262
1498
|
token = credential.token;
|
|
1263
1499
|
const headers = requestHeaders(token);
|
|
@@ -1457,6 +1693,51 @@ export async function runCli(argv = process.argv.slice(2), options = {}) {
|
|
|
1457
1693
|
writeJson(writeStdout, data);
|
|
1458
1694
|
return hasFailures(data) ? 1 : 0;
|
|
1459
1695
|
}
|
|
1696
|
+
if (parsed.command === 'rules-list') {
|
|
1697
|
+
const response = await fetchImplementation(`${baseUrl}/api/agent/v1/notification-rules`, {
|
|
1698
|
+
method: 'GET', headers, signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
1699
|
+
});
|
|
1700
|
+
writeJson(writeStdout, parseApiResponse('rules-list', await parseHttpResponse(response, token)));
|
|
1701
|
+
return 0;
|
|
1702
|
+
}
|
|
1703
|
+
if (parsed.command === 'rules-get') {
|
|
1704
|
+
const response = await fetchImplementation(notificationRuleEndpoint, {
|
|
1705
|
+
method: 'GET', headers, signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
1706
|
+
});
|
|
1707
|
+
writeJson(writeStdout, parseApiResponse('rules-get', await parseHttpResponse(response, token)));
|
|
1708
|
+
return 0;
|
|
1709
|
+
}
|
|
1710
|
+
if (parsed.command === 'rules-set') {
|
|
1711
|
+
const response = await fetchImplementation(notificationRuleEndpoint, {
|
|
1712
|
+
method: 'PUT',
|
|
1713
|
+
headers: { ...headers, 'Content-Type': 'application/json' },
|
|
1714
|
+
body: JSON.stringify({ transactionRef: parsed.transactionRef, ...notificationRulePayload }),
|
|
1715
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
1716
|
+
});
|
|
1717
|
+
writeJson(writeStdout, parseApiResponse('rules-set', await parseHttpResponse(response, token)));
|
|
1718
|
+
return 0;
|
|
1719
|
+
}
|
|
1720
|
+
if (parsed.command === 'rules-delete') {
|
|
1721
|
+
const response = await fetchImplementation(notificationRuleEndpoint, {
|
|
1722
|
+
method: 'DELETE', headers, signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
1723
|
+
});
|
|
1724
|
+
writeJson(writeStdout, parseApiResponse('rules-delete', await parseHttpResponse(response, token)));
|
|
1725
|
+
return 0;
|
|
1726
|
+
}
|
|
1727
|
+
if (parsed.command === 'rules-scan-contract') {
|
|
1728
|
+
const contract = readContractPdf(parsed.contract);
|
|
1729
|
+
const response = await fetchImplementation(`${baseUrl}/api/agent/v1/notification-rules/extract-renewal`, {
|
|
1730
|
+
method: 'POST',
|
|
1731
|
+
headers: { ...headers, 'Content-Type': 'application/json' },
|
|
1732
|
+
body: JSON.stringify({
|
|
1733
|
+
fileBase64: contract.toString('base64'),
|
|
1734
|
+
mimeType: 'application/pdf',
|
|
1735
|
+
}),
|
|
1736
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
1737
|
+
});
|
|
1738
|
+
writeJson(writeStdout, parseApiResponse('rules-scan-contract', await parseHttpResponse(response, token)));
|
|
1739
|
+
return 0;
|
|
1740
|
+
}
|
|
1460
1741
|
if (parsed.command === 'ask-partner') {
|
|
1461
1742
|
const response = await fetchImplementation(`${baseUrl}/api/agent/v1/transaction-explanation-requests`, {
|
|
1462
1743
|
method: 'POST',
|
package/dist/contracts.js
CHANGED
|
@@ -27,6 +27,102 @@ function rejectUnknownFields(value, allowed, label) {
|
|
|
27
27
|
if (unknown)
|
|
28
28
|
throw new UsageError(`${label} contains unknown field: ${unknown}`);
|
|
29
29
|
}
|
|
30
|
+
export function validateNotificationRulePayload(value) {
|
|
31
|
+
const payload = requireObject(value, 'notification rule payload');
|
|
32
|
+
rejectUnknownFields(payload, new Set(['amountChange', 'renewalReminder', 'delivery']), 'notification rule payload');
|
|
33
|
+
const amountChange = requireObject(payload.amountChange, 'amountChange');
|
|
34
|
+
const renewalReminder = requireObject(payload.renewalReminder, 'renewalReminder');
|
|
35
|
+
const delivery = requireObject(payload.delivery, 'delivery');
|
|
36
|
+
rejectUnknownFields(amountChange, new Set(['enabled', 'comparison', 'baselinePence']), 'amountChange');
|
|
37
|
+
rejectUnknownFields(renewalReminder, new Set(['enabled', 'renewalDate', 'leadDays']), 'renewalReminder');
|
|
38
|
+
rejectUnknownFields(delivery, new Set(['inApp', 'email']), 'delivery');
|
|
39
|
+
if (typeof amountChange.enabled !== 'boolean')
|
|
40
|
+
throw new UsageError('amountChange.enabled must be true or false');
|
|
41
|
+
if (amountChange.comparison !== 'increase' && amountChange.comparison !== 'any') {
|
|
42
|
+
throw new UsageError('amountChange.comparison must be increase or any');
|
|
43
|
+
}
|
|
44
|
+
if (!Number.isSafeInteger(amountChange.baselinePence) || Number(amountChange.baselinePence) <= 0) {
|
|
45
|
+
throw new UsageError('amountChange.baselinePence must be a positive integer');
|
|
46
|
+
}
|
|
47
|
+
if (typeof renewalReminder.enabled !== 'boolean')
|
|
48
|
+
throw new UsageError('renewalReminder.enabled must be true or false');
|
|
49
|
+
if (renewalReminder.renewalDate !== null && !isIsoDate(renewalReminder.renewalDate)) {
|
|
50
|
+
throw new UsageError('renewalReminder.renewalDate must be YYYY-MM-DD or null');
|
|
51
|
+
}
|
|
52
|
+
if (renewalReminder.enabled && renewalReminder.renewalDate === null) {
|
|
53
|
+
throw new UsageError('renewalReminder.renewalDate is required when enabled');
|
|
54
|
+
}
|
|
55
|
+
if (!Number.isSafeInteger(renewalReminder.leadDays) || Number(renewalReminder.leadDays) < 0 || Number(renewalReminder.leadDays) > 365) {
|
|
56
|
+
throw new UsageError('renewalReminder.leadDays must be an integer from 0 to 365');
|
|
57
|
+
}
|
|
58
|
+
if (delivery.inApp !== true || typeof delivery.email !== 'boolean') {
|
|
59
|
+
throw new UsageError('delivery must include inApp: true and an email boolean');
|
|
60
|
+
}
|
|
61
|
+
if (!amountChange.enabled && !renewalReminder.enabled) {
|
|
62
|
+
throw new UsageError('At least one notification rule must be enabled');
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
amountChange: {
|
|
66
|
+
enabled: amountChange.enabled,
|
|
67
|
+
comparison: amountChange.comparison,
|
|
68
|
+
baselinePence: Number(amountChange.baselinePence),
|
|
69
|
+
},
|
|
70
|
+
renewalReminder: {
|
|
71
|
+
enabled: renewalReminder.enabled,
|
|
72
|
+
renewalDate: renewalReminder.renewalDate,
|
|
73
|
+
leadDays: Number(renewalReminder.leadDays),
|
|
74
|
+
},
|
|
75
|
+
delivery: { inApp: true, email: delivery.email },
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function isNotificationRule(value) {
|
|
79
|
+
if (!isObject(value))
|
|
80
|
+
return false;
|
|
81
|
+
try {
|
|
82
|
+
validateNotificationRulePayload({
|
|
83
|
+
amountChange: value.amountChange,
|
|
84
|
+
renewalReminder: value.renewalReminder,
|
|
85
|
+
delivery: value.delivery,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
return hasOnlyFields(value, [
|
|
92
|
+
'id', 'transactionRef', 'merchantName', 'currency', 'sourceAmountPence', 'amountChange',
|
|
93
|
+
'renewalReminder', 'delivery', 'createdAt', 'updatedAt',
|
|
94
|
+
])
|
|
95
|
+
&& typeof value.id === 'string'
|
|
96
|
+
&& typeof value.transactionRef === 'string'
|
|
97
|
+
&& typeof value.merchantName === 'string'
|
|
98
|
+
&& typeof value.currency === 'string'
|
|
99
|
+
&& Number.isSafeInteger(value.sourceAmountPence)
|
|
100
|
+
&& typeof value.createdAt === 'string'
|
|
101
|
+
&& typeof value.updatedAt === 'string';
|
|
102
|
+
}
|
|
103
|
+
function isNotificationRuleResponse(value) {
|
|
104
|
+
return isObject(value)
|
|
105
|
+
&& hasOnlyFields(value, ['rule'])
|
|
106
|
+
&& (value.rule === null || isNotificationRule(value.rule));
|
|
107
|
+
}
|
|
108
|
+
function isNotificationRuleListResponse(value) {
|
|
109
|
+
return isObject(value)
|
|
110
|
+
&& hasOnlyFields(value, ['rules'])
|
|
111
|
+
&& Array.isArray(value.rules)
|
|
112
|
+
&& value.rules.every(isNotificationRule);
|
|
113
|
+
}
|
|
114
|
+
function isNotificationRuleDeleteResponse(value) {
|
|
115
|
+
return isObject(value)
|
|
116
|
+
&& hasOnlyFields(value, ['deleted', 'transactionRef'])
|
|
117
|
+
&& value.deleted === true
|
|
118
|
+
&& typeof value.transactionRef === 'string';
|
|
119
|
+
}
|
|
120
|
+
function isRenewalExtractionResponse(value) {
|
|
121
|
+
return isObject(value)
|
|
122
|
+
&& hasOnlyFields(value, ['renewalDate', 'confidence'])
|
|
123
|
+
&& isIsoDate(value.renewalDate)
|
|
124
|
+
&& (value.confidence === 'high' || value.confidence === 'medium' || value.confidence === 'low');
|
|
125
|
+
}
|
|
30
126
|
function validateSplit(value, index, splitIndex) {
|
|
31
127
|
const label = `assignments[${index}].categorySplits[${splitIndex}]`;
|
|
32
128
|
const split = requireObject(value, label);
|
|
@@ -254,9 +350,6 @@ function isTransaction(value) {
|
|
|
254
350
|
&& typeof value.date === 'string'
|
|
255
351
|
&& value.status === 'booked'
|
|
256
352
|
&& isAccountRef(value.accountRef)
|
|
257
|
-
&& typeof value.accountId === 'string'
|
|
258
|
-
&& typeof value.accountDocId === 'string'
|
|
259
|
-
&& typeof value.requisitionId === 'string'
|
|
260
353
|
&& (value.scope === 'personal' || value.scope === 'joint')
|
|
261
354
|
&& (value.categoryId === null || typeof value.categoryId === 'string')
|
|
262
355
|
&& (value.lineItemId === null || typeof value.lineItemId === 'string')
|
|
@@ -749,15 +842,23 @@ export function parseApiResponse(command, value) {
|
|
|
749
842
|
? isLineItemMutationResponse(value)
|
|
750
843
|
: command === 'transactions'
|
|
751
844
|
? isTransactionsResponse(value)
|
|
752
|
-
: command === '
|
|
753
|
-
?
|
|
754
|
-
: command === '
|
|
755
|
-
?
|
|
756
|
-
: command === '
|
|
757
|
-
?
|
|
758
|
-
: command === '
|
|
759
|
-
?
|
|
760
|
-
:
|
|
845
|
+
: command === 'rules-list'
|
|
846
|
+
? isNotificationRuleListResponse(value)
|
|
847
|
+
: command === 'rules-get' || command === 'rules-set'
|
|
848
|
+
? isNotificationRuleResponse(value)
|
|
849
|
+
: command === 'rules-delete'
|
|
850
|
+
? isNotificationRuleDeleteResponse(value)
|
|
851
|
+
: command === 'rules-scan-contract'
|
|
852
|
+
? isRenewalExtractionResponse(value)
|
|
853
|
+
: command === 'assign'
|
|
854
|
+
? isAssignmentResponse(value)
|
|
855
|
+
: command === 'ask-partner'
|
|
856
|
+
? isPartnerResponse(value)
|
|
857
|
+
: command === 'goals-list'
|
|
858
|
+
? isGoalsResponse(value)
|
|
859
|
+
: command === 'goals-delete'
|
|
860
|
+
? isGoalDeleteResponse(value)
|
|
861
|
+
: isGoalMutationResponse(value);
|
|
761
862
|
if (!valid) {
|
|
762
863
|
const label = command === 'assign' ? 'assignment' : command;
|
|
763
864
|
throw new ApiError(`Invalid ${label} response from the Agent API`);
|