@slothmoney/agent-cli 0.14.1 → 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 +13 -0
- package/README.md +55 -5
- package/dist/args.js +56 -7
- package/dist/cli.js +247 -7
- package/dist/contracts.js +113 -12
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
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
|
+
|
|
3
16
|
## 0.14.1 - 2026-08-21
|
|
4
17
|
|
|
5
18
|
- Make every parent help page list its nested commands, and give
|
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
|
|
@@ -112,6 +112,7 @@ sloth-agent categories create --help
|
|
|
112
112
|
sloth-agent line-items --help
|
|
113
113
|
sloth-agent line-items create --help
|
|
114
114
|
sloth-agent transactions --help
|
|
115
|
+
sloth-agent rules --help
|
|
115
116
|
sloth-agent assign --help
|
|
116
117
|
sloth-agent goals create --help
|
|
117
118
|
sloth-agent goals update --help
|
|
@@ -120,6 +121,56 @@ sloth-agent goals restore --help
|
|
|
120
121
|
sloth-agent ask-partner --help
|
|
121
122
|
```
|
|
122
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
|
+
|
|
123
174
|
### How transaction categorisation is represented
|
|
124
175
|
|
|
125
176
|
Personal and joint category assignments are separate. A personal assignment
|
|
@@ -482,9 +533,8 @@ sloth-agent transactions \
|
|
|
482
533
|
--limit 50
|
|
483
534
|
```
|
|
484
535
|
|
|
485
|
-
Copy the value from `sloth-agent accounts`.
|
|
486
|
-
|
|
487
|
-
`--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.
|
|
488
538
|
|
|
489
539
|
Account changes are previews unless `--apply` is present. Connected accounts
|
|
490
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';
|
|
@@ -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]',
|
|
@@ -589,7 +594,6 @@ export function transactionsHelpText() {
|
|
|
589
594
|
' --q TEXT Optional. Search transactions by text.',
|
|
590
595
|
' --account-ref REF Optional. Filter by the opaque accountRef from sloth-agent accounts.',
|
|
591
596
|
' Copy the exact sloth_account_v1_... value.',
|
|
592
|
-
' --account-id ID Optional legacy filter by provider or stored account ID.',
|
|
593
597
|
' --category-id ID Optional. Filter by category ID.',
|
|
594
598
|
' --line-item-id ID Optional. Filter primary or split assignments by line-item ID.',
|
|
595
599
|
' --assignment-scope SCOPE Optional. Filter assignments by personal or joint.',
|
|
@@ -601,7 +605,6 @@ export function transactionsHelpText() {
|
|
|
601
605
|
'',
|
|
602
606
|
'Constraints:',
|
|
603
607
|
' All filters are omitted by default.',
|
|
604
|
-
' Use either --account-ref or --account-id, not both.',
|
|
605
608
|
' --end-date must not be before --start-date.',
|
|
606
609
|
' The first transaction read each UTC day may refresh linked bank data.',
|
|
607
610
|
' Refresh remotely persists booked transactions and account balances.',
|
|
@@ -936,6 +939,135 @@ export function askPartnerHelpText() {
|
|
|
936
939
|
' JSON containing requestId, publicUrl, message, expiresAt, and status.',
|
|
937
940
|
].join('\n');
|
|
938
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
|
+
}
|
|
939
1071
|
export function commandHelpText(topic) {
|
|
940
1072
|
const helpByTopic = {
|
|
941
1073
|
auth: authHelpText,
|
|
@@ -958,6 +1090,11 @@ export function commandHelpText(topic) {
|
|
|
958
1090
|
'line-items-rename': lineItemsRenameHelpText,
|
|
959
1091
|
transactions: transactionsHelpText,
|
|
960
1092
|
assign: assignHelpText,
|
|
1093
|
+
rules: rulesHelpText,
|
|
1094
|
+
'rules-get': rulesGetHelpText,
|
|
1095
|
+
'rules-set': rulesSetHelpText,
|
|
1096
|
+
'rules-delete': rulesDeleteHelpText,
|
|
1097
|
+
'rules-scan-contract': rulesScanContractHelpText,
|
|
961
1098
|
goals: goalsHelpText,
|
|
962
1099
|
'goals-list': goalsListHelpText,
|
|
963
1100
|
'goals-create': goalsCreateHelpText,
|
|
@@ -1064,6 +1201,29 @@ function readBudgetFile(filePath) {
|
|
|
1064
1201
|
throw new UsageError(`Failed to read budget JSON: ${message}`);
|
|
1065
1202
|
}
|
|
1066
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
|
+
}
|
|
1067
1227
|
function buildTransactionsQuery(filters) {
|
|
1068
1228
|
const params = new URLSearchParams();
|
|
1069
1229
|
if (filters.uncategorized !== undefined) {
|
|
@@ -1081,8 +1241,6 @@ function buildTransactionsQuery(filters) {
|
|
|
1081
1241
|
params.set('q', filters.q);
|
|
1082
1242
|
if (filters.accountRef !== undefined)
|
|
1083
1243
|
params.set('accountRef', filters.accountRef);
|
|
1084
|
-
if (filters.accountId !== undefined)
|
|
1085
|
-
params.set('accountId', filters.accountId);
|
|
1086
1244
|
if (filters.categoryId !== undefined)
|
|
1087
1245
|
params.set('categoryId', filters.categoryId);
|
|
1088
1246
|
if (filters.lineItemId !== undefined)
|
|
@@ -1299,6 +1457,43 @@ export async function runCli(argv = process.argv.slice(2), options = {}) {
|
|
|
1299
1457
|
});
|
|
1300
1458
|
return 0;
|
|
1301
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
|
+
}
|
|
1302
1497
|
const credential = await resolveCredential(environment, baseUrl, getCredentialStore);
|
|
1303
1498
|
token = credential.token;
|
|
1304
1499
|
const headers = requestHeaders(token);
|
|
@@ -1498,6 +1693,51 @@ export async function runCli(argv = process.argv.slice(2), options = {}) {
|
|
|
1498
1693
|
writeJson(writeStdout, data);
|
|
1499
1694
|
return hasFailures(data) ? 1 : 0;
|
|
1500
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
|
+
}
|
|
1501
1741
|
if (parsed.command === 'ask-partner') {
|
|
1502
1742
|
const response = await fetchImplementation(`${baseUrl}/api/agent/v1/transaction-explanation-requests`, {
|
|
1503
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`);
|