@slothmoney/agent-cli 0.14.1 → 0.16.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 +29 -0
- package/README.md +101 -5
- package/dist/args.js +157 -7
- package/dist/cli.js +453 -7
- package/dist/contracts.js +212 -12
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.16.0 - 2026-08-21
|
|
4
|
+
|
|
5
|
+
- Add receipt image extraction plus read, preview-by-default attach, and remove
|
|
6
|
+
commands for reviewed receipt items.
|
|
7
|
+
- Keep receipt images and extraction drafts transient; only reviewed JSON is
|
|
8
|
+
sent to the confirmed evidence endpoint.
|
|
9
|
+
- Keep reviewed receipt rows simple: `id`, `label`, and a signed amount, with
|
|
10
|
+
discounts represented as negative rows.
|
|
11
|
+
- Match notification-rule writes to the Agent API by sending only the optional
|
|
12
|
+
email choice while keeping in-app delivery server-owned and always enabled.
|
|
13
|
+
- Accept computed renewal reminder dates and nullable rule timestamps in rule
|
|
14
|
+
responses, and accept a null extracted date when a PDF contains no usable
|
|
15
|
+
renewal date.
|
|
16
|
+
- Send contract PDFs with the required filename and content fields, and align
|
|
17
|
+
the local 6 MB limit with the API's encoded-content limit.
|
|
18
|
+
|
|
19
|
+
## 0.15.0 - 2026-08-21
|
|
20
|
+
|
|
21
|
+
- Add `rules` commands to list, read, preview, apply, and delete notification
|
|
22
|
+
rules anchored to existing transaction references.
|
|
23
|
+
- Add optional one-time PDF renewal-date extraction. Preview mode keeps the
|
|
24
|
+
file local; apply mode sends it for extraction and the service discards it
|
|
25
|
+
without storing the contract.
|
|
26
|
+
- Keep recurring predictions and transaction creation outside the Rules
|
|
27
|
+
contract.
|
|
28
|
+
- Remove the legacy transaction `--account-id` filter and stop exposing
|
|
29
|
+
internal account and connection locator fields in transaction responses.
|
|
30
|
+
Use the opaque `accountRef` returned by `sloth-agent accounts`.
|
|
31
|
+
|
|
3
32
|
## 0.14.1 - 2026-08-21
|
|
4
33
|
|
|
5
34
|
- 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.16.0 -- sloth-agent --help
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
## Authenticate
|
|
@@ -112,7 +112,11 @@ 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
|
|
117
|
+
sloth-agent receipts --help
|
|
118
|
+
sloth-agent receipts extract --help
|
|
119
|
+
sloth-agent receipts attach --help
|
|
116
120
|
sloth-agent goals create --help
|
|
117
121
|
sloth-agent goals update --help
|
|
118
122
|
sloth-agent goals mark-spent --help
|
|
@@ -120,6 +124,99 @@ sloth-agent goals restore --help
|
|
|
120
124
|
sloth-agent ask-partner --help
|
|
121
125
|
```
|
|
122
126
|
|
|
127
|
+
### Add a transaction notification rule
|
|
128
|
+
|
|
129
|
+
Rules watch future payments that match an existing transaction. They can alert
|
|
130
|
+
you when the amount changes or before a renewal date. They do not create
|
|
131
|
+
transactions or recurring predictions.
|
|
132
|
+
|
|
133
|
+
First, run `sloth-agent transactions` and copy the exact `transactionRef` into
|
|
134
|
+
`rule.json` alongside this rule definition:
|
|
135
|
+
|
|
136
|
+
```json
|
|
137
|
+
{
|
|
138
|
+
"amountChange": {
|
|
139
|
+
"enabled": true,
|
|
140
|
+
"comparison": "increase",
|
|
141
|
+
"baselinePence": 3184
|
|
142
|
+
},
|
|
143
|
+
"renewalReminder": {
|
|
144
|
+
"enabled": true,
|
|
145
|
+
"renewalDate": "2027-07-30",
|
|
146
|
+
"leadDays": 30
|
|
147
|
+
},
|
|
148
|
+
"delivery": {
|
|
149
|
+
"email": true
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
In-app notifications are always included. Set `delivery.email` to choose
|
|
155
|
+
whether Sloth also sends an email. `leadDays` accepts an integer from 1 to 365.
|
|
156
|
+
|
|
157
|
+
Preview the write locally, then apply the same validated file:
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
sloth-agent rules set \
|
|
161
|
+
--transaction-ref PASTE_THE_EXACT_TRANSACTION_REF_HERE \
|
|
162
|
+
--input rule.json
|
|
163
|
+
|
|
164
|
+
sloth-agent rules set \
|
|
165
|
+
--transaction-ref PASTE_THE_EXACT_TRANSACTION_REF_HERE \
|
|
166
|
+
--input rule.json \
|
|
167
|
+
--apply
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
To extract a renewal date first, scan a PDF no larger than 6 MB:
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
sloth-agent rules scan-contract --contract contract.pdf --apply
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
The PDF is discarded after extraction and is not stored. Scanning only returns
|
|
177
|
+
the date and confidence; use `rules set` to save the resulting reminder.
|
|
178
|
+
|
|
179
|
+
### Attach receipt items end to end
|
|
180
|
+
|
|
181
|
+
Receipt items are evidence attached to a booked transaction. They do not
|
|
182
|
+
change its category, sharing, partner balance, or budget treatment.
|
|
183
|
+
|
|
184
|
+
1. Copy the exact `transactionRef` from `sloth-agent transactions`, then
|
|
185
|
+
extract a transient draft from a JPEG, PNG, or WebP image up to 8 MB:
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
sloth-agent receipts extract --image /path/to/receipt.jpg > receipt-draft.json
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
The image and draft are not saved by Sloth. Review the JSON, remove the outer
|
|
192
|
+
`draft` key and `warnings`, and keep `schemaVersion`, `currency`, and the
|
|
193
|
+
reviewed `receiptItems` in `receipt.json`. Each item contains only `id`, `label`,
|
|
194
|
+
and a signed integer `amountPence`. Purchases and added charges are positive;
|
|
195
|
+
discounts are negative. Do not add tax already included in other prices as a
|
|
196
|
+
separate row because the signed rows should sum to the printed receipt total.
|
|
197
|
+
|
|
198
|
+
2. Preview the exact write without contacting the API:
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
sloth-agent receipts attach \
|
|
202
|
+
--transaction-ref PASTE_THE_EXACT_TRANSACTION_REF_HERE \
|
|
203
|
+
--input receipt.json
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
3. Apply the reviewed JSON with a write-enabled token:
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
sloth-agent receipts attach \
|
|
210
|
+
--transaction-ref PASTE_THE_EXACT_TRANSACTION_REF_HERE \
|
|
211
|
+
--input receipt.json \
|
|
212
|
+
--apply
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Use `sloth-agent receipts get --transaction-ref REF` to read the saved
|
|
216
|
+
revision. Pass `--expected-revision N` when replacing it, or use
|
|
217
|
+
`sloth-agent receipts remove --transaction-ref REF --revision N --apply` to
|
|
218
|
+
remove it.
|
|
219
|
+
|
|
123
220
|
### How transaction categorisation is represented
|
|
124
221
|
|
|
125
222
|
Personal and joint category assignments are separate. A personal assignment
|
|
@@ -482,9 +579,8 @@ sloth-agent transactions \
|
|
|
482
579
|
--limit 50
|
|
483
580
|
```
|
|
484
581
|
|
|
485
|
-
Copy the value from `sloth-agent accounts`.
|
|
486
|
-
|
|
487
|
-
`--account-ref`. Do not provide both filters in one command.
|
|
582
|
+
Copy the value from `sloth-agent accounts`. Account references are the public
|
|
583
|
+
account identifier for transaction filtering.
|
|
488
584
|
|
|
489
585
|
Account changes are previews unless `--apply` is present. Connected accounts
|
|
490
586
|
support only goal-savings membership. Manual current accounts support their
|
package/dist/args.js
CHANGED
|
@@ -93,6 +93,93 @@ function parseGoalPriority(value) {
|
|
|
93
93
|
}
|
|
94
94
|
return priority;
|
|
95
95
|
}
|
|
96
|
+
function parsePositiveRevision(value, name) {
|
|
97
|
+
if (!/^[1-9]\d*$/.test(value)) {
|
|
98
|
+
throw new UsageError(`${name} must be a positive whole number`);
|
|
99
|
+
}
|
|
100
|
+
const revision = Number(value);
|
|
101
|
+
if (!Number.isSafeInteger(revision)) {
|
|
102
|
+
throw new UsageError(`${name} must be a positive whole number`);
|
|
103
|
+
}
|
|
104
|
+
return revision;
|
|
105
|
+
}
|
|
106
|
+
function parseReceipts(args, baseUrl) {
|
|
107
|
+
const subcommand = args.shift();
|
|
108
|
+
if (!subcommand || !['extract', 'get', 'attach', 'remove'].includes(subcommand)) {
|
|
109
|
+
throw new UsageError('receipts requires extract, get, attach, or remove');
|
|
110
|
+
}
|
|
111
|
+
let transactionRef;
|
|
112
|
+
let image;
|
|
113
|
+
let input;
|
|
114
|
+
let expectedRevision;
|
|
115
|
+
let revision;
|
|
116
|
+
let apply = false;
|
|
117
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
118
|
+
const argument = args[index];
|
|
119
|
+
const read = (name) => {
|
|
120
|
+
const value = readOptionValue(args, index, name);
|
|
121
|
+
index += 1;
|
|
122
|
+
return value;
|
|
123
|
+
};
|
|
124
|
+
if (argument === '--apply') {
|
|
125
|
+
if (apply)
|
|
126
|
+
throw new UsageError('--apply may only be provided once');
|
|
127
|
+
apply = true;
|
|
128
|
+
}
|
|
129
|
+
else if (argument === '--transaction-ref') {
|
|
130
|
+
transactionRef = setOnce(transactionRef, read('--transaction-ref'), '--transaction-ref');
|
|
131
|
+
}
|
|
132
|
+
else if (argument === '--image') {
|
|
133
|
+
image = setOnce(image, read('--image'), '--image');
|
|
134
|
+
}
|
|
135
|
+
else if (argument === '--input') {
|
|
136
|
+
input = setOnce(input, read('--input'), '--input');
|
|
137
|
+
}
|
|
138
|
+
else if (argument === '--expected-revision') {
|
|
139
|
+
expectedRevision = setOnce(expectedRevision, parsePositiveRevision(read('--expected-revision'), '--expected-revision'), '--expected-revision');
|
|
140
|
+
}
|
|
141
|
+
else if (argument === '--revision') {
|
|
142
|
+
revision = setOnce(revision, parsePositiveRevision(read('--revision'), '--revision'), '--revision');
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
throw new UsageError(`Unknown receipts ${subcommand} option: ${argument}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (subcommand === 'extract') {
|
|
149
|
+
if (!image)
|
|
150
|
+
throw new UsageError('receipts extract requires --image <file>');
|
|
151
|
+
if (transactionRef || input || expectedRevision || revision || apply) {
|
|
152
|
+
throw new UsageError('receipts extract accepts only --image');
|
|
153
|
+
}
|
|
154
|
+
return withBaseUrl({ command: 'receipts-extract', image }, baseUrl);
|
|
155
|
+
}
|
|
156
|
+
if (!transactionRef)
|
|
157
|
+
throw new UsageError(`receipts ${subcommand} requires --transaction-ref <ref>`);
|
|
158
|
+
if (subcommand === 'get') {
|
|
159
|
+
if (image || input || expectedRevision || revision || apply) {
|
|
160
|
+
throw new UsageError('receipts get accepts only --transaction-ref');
|
|
161
|
+
}
|
|
162
|
+
return withBaseUrl({ command: 'receipts-get', transactionRef }, baseUrl);
|
|
163
|
+
}
|
|
164
|
+
if (subcommand === 'attach') {
|
|
165
|
+
if (!input)
|
|
166
|
+
throw new UsageError('receipts attach requires --input <file>');
|
|
167
|
+
if (image || revision)
|
|
168
|
+
throw new UsageError('receipts attach received an unsupported option');
|
|
169
|
+
return withBaseUrl({
|
|
170
|
+
command: 'receipts-attach',
|
|
171
|
+
transactionRef,
|
|
172
|
+
input,
|
|
173
|
+
...(expectedRevision === undefined ? {} : { expectedRevision }),
|
|
174
|
+
apply,
|
|
175
|
+
}, baseUrl);
|
|
176
|
+
}
|
|
177
|
+
if (revision === undefined)
|
|
178
|
+
throw new UsageError('receipts remove requires --revision <number>');
|
|
179
|
+
if (image || input || expectedRevision)
|
|
180
|
+
throw new UsageError('receipts remove received an unsupported option');
|
|
181
|
+
return withBaseUrl({ command: 'receipts-remove', transactionRef, revision, apply }, baseUrl);
|
|
182
|
+
}
|
|
96
183
|
function parseGoalType(value) {
|
|
97
184
|
if (!isGoalType(value)) {
|
|
98
185
|
throw new UsageError('--type must be keep or spend');
|
|
@@ -177,7 +264,6 @@ function parseTransactions(args) {
|
|
|
177
264
|
'--end-date',
|
|
178
265
|
'--q',
|
|
179
266
|
'--account-ref',
|
|
180
|
-
'--account-id',
|
|
181
267
|
'--category-id',
|
|
182
268
|
'--line-item-id',
|
|
183
269
|
'--assignment-scope',
|
|
@@ -213,9 +299,6 @@ function parseTransactions(args) {
|
|
|
213
299
|
else if (name === '--account-ref') {
|
|
214
300
|
filters.accountRef = setOnce(filters.accountRef, parseAccountRef(value), name);
|
|
215
301
|
}
|
|
216
|
-
else if (name === '--account-id') {
|
|
217
|
-
filters.accountId = setOnce(filters.accountId, value, name);
|
|
218
|
-
}
|
|
219
302
|
else if (name === '--category-id') {
|
|
220
303
|
filters.categoryId = setOnce(filters.categoryId, value, name);
|
|
221
304
|
}
|
|
@@ -237,9 +320,6 @@ function parseTransactions(args) {
|
|
|
237
320
|
&& filters.endDate < filters.startDate) {
|
|
238
321
|
throw new UsageError('--end-date must not be before --start-date');
|
|
239
322
|
}
|
|
240
|
-
if (filters.accountRef !== undefined && filters.accountId !== undefined) {
|
|
241
|
-
throw new UsageError('Use either --account-ref or --account-id, not both');
|
|
242
|
-
}
|
|
243
323
|
return filters;
|
|
244
324
|
}
|
|
245
325
|
function parseNamedOptions(args, commandLabel, allowed) {
|
|
@@ -752,6 +832,48 @@ function parseGoals(args, baseUrl) {
|
|
|
752
832
|
}
|
|
753
833
|
throw new UsageError(`Unknown goals command: ${subcommand}`);
|
|
754
834
|
}
|
|
835
|
+
function parseRules(args, baseUrl) {
|
|
836
|
+
const subcommand = args.shift();
|
|
837
|
+
if (subcommand === undefined || subcommand === 'list') {
|
|
838
|
+
if (args.length > 0) {
|
|
839
|
+
throw new UsageError(`Unknown rules list option: ${args[0]}`);
|
|
840
|
+
}
|
|
841
|
+
return withBaseUrl({ command: 'rules-list' }, baseUrl);
|
|
842
|
+
}
|
|
843
|
+
if (subcommand === 'get' || subcommand === 'delete') {
|
|
844
|
+
const allowed = new Set(['--transaction-ref']);
|
|
845
|
+
const { values, apply } = parseNamedOptions(args, `rules ${subcommand}`, allowed);
|
|
846
|
+
const transactionRef = requiredOption(values, '--transaction-ref', `rules ${subcommand}`);
|
|
847
|
+
if (subcommand === 'get') {
|
|
848
|
+
if (apply)
|
|
849
|
+
throw new UsageError('rules get does not accept --apply');
|
|
850
|
+
return withBaseUrl({ command: 'rules-get', transactionRef }, baseUrl);
|
|
851
|
+
}
|
|
852
|
+
return withBaseUrl({
|
|
853
|
+
command: 'rules-delete',
|
|
854
|
+
transactionRef,
|
|
855
|
+
apply,
|
|
856
|
+
}, baseUrl);
|
|
857
|
+
}
|
|
858
|
+
if (subcommand === 'set') {
|
|
859
|
+
const { values, apply } = parseNamedOptions(args, 'rules set', new Set(['--transaction-ref', '--input']));
|
|
860
|
+
return withBaseUrl({
|
|
861
|
+
command: 'rules-set',
|
|
862
|
+
transactionRef: requiredOption(values, '--transaction-ref', 'rules set'),
|
|
863
|
+
input: requiredOption(values, '--input', 'rules set'),
|
|
864
|
+
apply,
|
|
865
|
+
}, baseUrl);
|
|
866
|
+
}
|
|
867
|
+
if (subcommand === 'scan-contract') {
|
|
868
|
+
const { values, apply } = parseNamedOptions(args, 'rules scan-contract', new Set(['--contract']));
|
|
869
|
+
return withBaseUrl({
|
|
870
|
+
command: 'rules-scan-contract',
|
|
871
|
+
contract: requiredOption(values, '--contract', 'rules scan-contract'),
|
|
872
|
+
apply,
|
|
873
|
+
}, baseUrl);
|
|
874
|
+
}
|
|
875
|
+
throw new UsageError(`Unknown rules command: ${subcommand}`);
|
|
876
|
+
}
|
|
755
877
|
function helpTopic(argv) {
|
|
756
878
|
const positionals = [];
|
|
757
879
|
for (let index = 0; index < argv.length; index += 1) {
|
|
@@ -791,6 +913,17 @@ function helpTopic(argv) {
|
|
|
791
913
|
return 'goals-delete';
|
|
792
914
|
return 'goals';
|
|
793
915
|
}
|
|
916
|
+
if (command === 'rules') {
|
|
917
|
+
if (subcommand === 'get')
|
|
918
|
+
return 'rules-get';
|
|
919
|
+
if (subcommand === 'set')
|
|
920
|
+
return 'rules-set';
|
|
921
|
+
if (subcommand === 'delete')
|
|
922
|
+
return 'rules-delete';
|
|
923
|
+
if (subcommand === 'scan-contract')
|
|
924
|
+
return 'rules-scan-contract';
|
|
925
|
+
return 'rules';
|
|
926
|
+
}
|
|
794
927
|
if (command === 'categories') {
|
|
795
928
|
if (subcommand === 'create')
|
|
796
929
|
return 'categories-create';
|
|
@@ -814,6 +947,17 @@ function helpTopic(argv) {
|
|
|
814
947
|
return 'budget-move';
|
|
815
948
|
return 'budget';
|
|
816
949
|
}
|
|
950
|
+
if (command === 'receipts') {
|
|
951
|
+
if (subcommand === 'extract')
|
|
952
|
+
return 'receipts-extract';
|
|
953
|
+
if (subcommand === 'get')
|
|
954
|
+
return 'receipts-get';
|
|
955
|
+
if (subcommand === 'attach')
|
|
956
|
+
return 'receipts-attach';
|
|
957
|
+
if (subcommand === 'remove')
|
|
958
|
+
return 'receipts-remove';
|
|
959
|
+
return 'receipts';
|
|
960
|
+
}
|
|
817
961
|
if (command === 'accounts'
|
|
818
962
|
|| command === 'transactions'
|
|
819
963
|
|| command === 'assign'
|
|
@@ -845,6 +989,9 @@ export function parseArgs(argv) {
|
|
|
845
989
|
if (command === 'goals') {
|
|
846
990
|
return parseGoals(args, baseUrl);
|
|
847
991
|
}
|
|
992
|
+
if (command === 'rules') {
|
|
993
|
+
return parseRules(args, baseUrl);
|
|
994
|
+
}
|
|
848
995
|
if (command === 'categories') {
|
|
849
996
|
return parseCategories(args, baseUrl);
|
|
850
997
|
}
|
|
@@ -860,6 +1007,9 @@ export function parseArgs(argv) {
|
|
|
860
1007
|
if (command === 'budget') {
|
|
861
1008
|
return parseBudget(args, baseUrl);
|
|
862
1009
|
}
|
|
1010
|
+
if (command === 'receipts') {
|
|
1011
|
+
return parseReceipts(args, baseUrl);
|
|
1012
|
+
}
|
|
863
1013
|
if (command === 'transactions') {
|
|
864
1014
|
return withBaseUrl({ command, filters: parseTransactions(args) }, baseUrl);
|
|
865
1015
|
}
|
package/dist/cli.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
|
+
import nodePath from 'node:path';
|
|
2
3
|
import { parseArgs, resolveBaseUrl, } from './args.js';
|
|
3
4
|
import { ICON_KEYS } from './category-metadata.js';
|
|
4
|
-
import { parseApiResponse, validateAssignmentPayload, validateBudgetMovementResponse, validateBudgetUpdatePayload, } from './contracts.js';
|
|
5
|
+
import { parseApiResponse, validateAssignmentPayload, validateBudgetMovementResponse, validateBudgetUpdatePayload, validateNotificationRulePayload, validateReceiptConfirmation, } from './contracts.js';
|
|
5
6
|
import { createSystemCredentialStore, secureStorageUnavailableError, } from './credential-store.js';
|
|
6
7
|
import { ApiError, CliError, ConfigError, UsageError, } from './errors.js';
|
|
7
|
-
export const CLI_VERSION = '0.
|
|
8
|
+
export const CLI_VERSION = '0.16.0';
|
|
8
9
|
const REQUEST_TIMEOUT_MS = 60_000;
|
|
10
|
+
const MAX_CONTRACT_PDF_BYTES = 6_000_000;
|
|
9
11
|
const API_ORIGIN_HELP_LINES = [
|
|
10
12
|
'',
|
|
11
13
|
'API origin:',
|
|
@@ -42,9 +44,18 @@ export function usageText() {
|
|
|
42
44
|
' sloth-agent line-items rename --scope personal|joint --category-id ID --line-item-id ID --name NAME [--apply]',
|
|
43
45
|
' sloth-agent transactions [--uncategorized[=true|false]] [--shared[=true|false]] [--limit N]',
|
|
44
46
|
' [--start-date YYYY-MM-DD] [--end-date YYYY-MM-DD] [--q TEXT]',
|
|
45
|
-
' [--account-ref REF] [--
|
|
47
|
+
' [--account-ref REF] [--category-id ID] [--line-item-id ID]',
|
|
46
48
|
' [--cursor CURSOR] [--base-url URL]',
|
|
47
49
|
' sloth-agent assign --input assignments.json [--apply] [--base-url URL]',
|
|
50
|
+
' sloth-agent rules [list] [--base-url URL]',
|
|
51
|
+
' sloth-agent rules get --transaction-ref REF [--base-url URL]',
|
|
52
|
+
' sloth-agent rules set --transaction-ref REF --input rule.json [--apply]',
|
|
53
|
+
' sloth-agent rules delete --transaction-ref REF [--apply]',
|
|
54
|
+
' sloth-agent rules scan-contract --contract FILE.pdf [--apply]',
|
|
55
|
+
' sloth-agent receipts extract --image FILE [--base-url URL]',
|
|
56
|
+
' sloth-agent receipts get --transaction-ref REF [--base-url URL]',
|
|
57
|
+
' sloth-agent receipts attach --transaction-ref REF --input receipt.json [--expected-revision N] [--apply]',
|
|
58
|
+
' sloth-agent receipts remove --transaction-ref REF --revision N [--apply]',
|
|
48
59
|
' sloth-agent goals [list] [--base-url URL]',
|
|
49
60
|
' sloth-agent goals create --name NAME --target-amount AMOUNT',
|
|
50
61
|
' --type keep|spend [--target-month YYYY-MM] [--apply] [--base-url URL]',
|
|
@@ -589,7 +600,6 @@ export function transactionsHelpText() {
|
|
|
589
600
|
' --q TEXT Optional. Search transactions by text.',
|
|
590
601
|
' --account-ref REF Optional. Filter by the opaque accountRef from sloth-agent accounts.',
|
|
591
602
|
' Copy the exact sloth_account_v1_... value.',
|
|
592
|
-
' --account-id ID Optional legacy filter by provider or stored account ID.',
|
|
593
603
|
' --category-id ID Optional. Filter by category ID.',
|
|
594
604
|
' --line-item-id ID Optional. Filter primary or split assignments by line-item ID.',
|
|
595
605
|
' --assignment-scope SCOPE Optional. Filter assignments by personal or joint.',
|
|
@@ -601,7 +611,6 @@ export function transactionsHelpText() {
|
|
|
601
611
|
'',
|
|
602
612
|
'Constraints:',
|
|
603
613
|
' All filters are omitted by default.',
|
|
604
|
-
' Use either --account-ref or --account-id, not both.',
|
|
605
614
|
' --end-date must not be before --start-date.',
|
|
606
615
|
' The first transaction read each UTC day may refresh linked bank data.',
|
|
607
616
|
' Refresh remotely persists booked transactions and account balances.',
|
|
@@ -936,6 +945,214 @@ export function askPartnerHelpText() {
|
|
|
936
945
|
' JSON containing requestId, publicUrl, message, expiresAt, and status.',
|
|
937
946
|
].join('\n');
|
|
938
947
|
}
|
|
948
|
+
export function rulesHelpText() {
|
|
949
|
+
return [
|
|
950
|
+
'Sloth Agent CLI — rules',
|
|
951
|
+
'',
|
|
952
|
+
'Manage notifications for future payments that match an existing transaction.',
|
|
953
|
+
'Rules do not create transactions or recurring predictions.',
|
|
954
|
+
'',
|
|
955
|
+
'Commands:',
|
|
956
|
+
' rules list List every saved notification rule.',
|
|
957
|
+
' rules get Read the rule for one transaction.',
|
|
958
|
+
' rules set Preview or save a rule for one transaction.',
|
|
959
|
+
' rules delete Preview or remove a rule.',
|
|
960
|
+
' rules scan-contract Validate or scan a PDF for its renewal date.',
|
|
961
|
+
'',
|
|
962
|
+
'Use the exact transactionRef returned by sloth-agent transactions.',
|
|
963
|
+
...API_ORIGIN_HELP_LINES,
|
|
964
|
+
].join('\n');
|
|
965
|
+
}
|
|
966
|
+
export function rulesGetHelpText() {
|
|
967
|
+
return [
|
|
968
|
+
'Sloth Agent CLI — rules get',
|
|
969
|
+
'',
|
|
970
|
+
'Read the saved notification rule for one existing transaction.',
|
|
971
|
+
'',
|
|
972
|
+
'Usage:',
|
|
973
|
+
' sloth-agent rules get --transaction-ref REF',
|
|
974
|
+
'',
|
|
975
|
+
'Required:',
|
|
976
|
+
' --transaction-ref REF The exact transactionRef from sloth-agent transactions.',
|
|
977
|
+
'',
|
|
978
|
+
'Behavior:',
|
|
979
|
+
' This command is read-only and requires an agent:read token.',
|
|
980
|
+
'',
|
|
981
|
+
'Output:',
|
|
982
|
+
' JSON containing the saved rule, or rule: null when no rule exists.',
|
|
983
|
+
'',
|
|
984
|
+
'Example:',
|
|
985
|
+
' sloth-agent rules get --transaction-ref PASTE_THE_EXACT_TRANSACTION_REF_HERE',
|
|
986
|
+
...API_ORIGIN_HELP_LINES,
|
|
987
|
+
].join('\n');
|
|
988
|
+
}
|
|
989
|
+
export function receiptsHelpText() {
|
|
990
|
+
return [
|
|
991
|
+
'Sloth Agent CLI — receipts',
|
|
992
|
+
'',
|
|
993
|
+
'Extract, review, read, attach, or remove receipt items for a booked transaction.',
|
|
994
|
+
'',
|
|
995
|
+
'Commands:',
|
|
996
|
+
' sloth-agent receipts extract Extract a transient draft from an image',
|
|
997
|
+
' sloth-agent receipts get Read saved receipt items',
|
|
998
|
+
' sloth-agent receipts attach Preview or save reviewed JSON',
|
|
999
|
+
' sloth-agent receipts remove Preview or remove saved receipt items',
|
|
1000
|
+
...API_ORIGIN_HELP_LINES,
|
|
1001
|
+
'',
|
|
1002
|
+
'Receipt items are evidence only. They do not change categories, sharing, or budgets.',
|
|
1003
|
+
].join('\n');
|
|
1004
|
+
}
|
|
1005
|
+
export function receiptsExtractHelpText() {
|
|
1006
|
+
return [
|
|
1007
|
+
'Sloth Agent CLI — receipts extract',
|
|
1008
|
+
'',
|
|
1009
|
+
'Usage:',
|
|
1010
|
+
' sloth-agent receipts extract --image FILE [--base-url URL]',
|
|
1011
|
+
'',
|
|
1012
|
+
'Required input:',
|
|
1013
|
+
' --image FILE JPEG, PNG, or WebP image up to 8 MB.',
|
|
1014
|
+
'',
|
|
1015
|
+
'This sends the image for one extraction request and does not save it or the draft.',
|
|
1016
|
+
'Review the returned JSON before using receipts attach.',
|
|
1017
|
+
...API_ORIGIN_HELP_LINES,
|
|
1018
|
+
'',
|
|
1019
|
+
'Output:',
|
|
1020
|
+
' JSON containing draft.currency, draft.receiptItems, and draft.warnings.',
|
|
1021
|
+
].join('\n');
|
|
1022
|
+
}
|
|
1023
|
+
export function receiptsGetHelpText() {
|
|
1024
|
+
return [
|
|
1025
|
+
'Sloth Agent CLI — receipts get',
|
|
1026
|
+
'',
|
|
1027
|
+
'Usage:',
|
|
1028
|
+
' sloth-agent receipts get --transaction-ref REF [--base-url URL]',
|
|
1029
|
+
'',
|
|
1030
|
+
' --transaction-ref REF Exact transactionRef from transactions output.',
|
|
1031
|
+
'',
|
|
1032
|
+
'This command is read-only. Output contains receipt or null.',
|
|
1033
|
+
...API_ORIGIN_HELP_LINES,
|
|
1034
|
+
].join('\n');
|
|
1035
|
+
}
|
|
1036
|
+
export function rulesSetHelpText() {
|
|
1037
|
+
return [
|
|
1038
|
+
'Sloth Agent CLI — rules set',
|
|
1039
|
+
'',
|
|
1040
|
+
'Preview or save notification conditions for one existing transaction.',
|
|
1041
|
+
'',
|
|
1042
|
+
'Usage:',
|
|
1043
|
+
' sloth-agent rules set --transaction-ref REF --input FILE [--apply]',
|
|
1044
|
+
'',
|
|
1045
|
+
'Required:',
|
|
1046
|
+
' --transaction-ref REF The exact transactionRef from sloth-agent transactions.',
|
|
1047
|
+
' --input FILE A JSON rule file such as rule.json.',
|
|
1048
|
+
'',
|
|
1049
|
+
'Input:',
|
|
1050
|
+
' {',
|
|
1051
|
+
' "amountChange": {',
|
|
1052
|
+
' "enabled": true,',
|
|
1053
|
+
' "comparison": "increase",',
|
|
1054
|
+
' "baselinePence": 3184',
|
|
1055
|
+
' },',
|
|
1056
|
+
' "renewalReminder": {',
|
|
1057
|
+
' "enabled": true,',
|
|
1058
|
+
' "renewalDate": "2027-07-30",',
|
|
1059
|
+
' "leadDays": 30',
|
|
1060
|
+
' },',
|
|
1061
|
+
' "delivery": { "email": true }',
|
|
1062
|
+
' }',
|
|
1063
|
+
'',
|
|
1064
|
+
' comparison accepts increase or any. baselinePence is a positive integer.',
|
|
1065
|
+
' renewalDate is YYYY-MM-DD or null when its reminder is disabled.',
|
|
1066
|
+
' leadDays is an integer from 1 to 365. At least one condition must be enabled.',
|
|
1067
|
+
' In-app notifications are always included. delivery.email adds email delivery.',
|
|
1068
|
+
'',
|
|
1069
|
+
'Write behavior:',
|
|
1070
|
+
' Without --apply, Sloth validates the file and prints a local preview.',
|
|
1071
|
+
' With --apply, Sloth replaces the saved rule using a write-enabled token.',
|
|
1072
|
+
'',
|
|
1073
|
+
'Example:',
|
|
1074
|
+
' sloth-agent rules set --transaction-ref PASTE_THE_EXACT_TRANSACTION_REF_HERE \\',
|
|
1075
|
+
' --input rule.json --apply',
|
|
1076
|
+
...API_ORIGIN_HELP_LINES,
|
|
1077
|
+
].join('\n');
|
|
1078
|
+
}
|
|
1079
|
+
export function receiptsAttachHelpText() {
|
|
1080
|
+
return [
|
|
1081
|
+
'Sloth Agent CLI — receipts attach',
|
|
1082
|
+
'',
|
|
1083
|
+
'Usage:',
|
|
1084
|
+
' sloth-agent receipts attach --transaction-ref REF --input FILE [--expected-revision N] [--apply]',
|
|
1085
|
+
'',
|
|
1086
|
+
'Required inputs:',
|
|
1087
|
+
' --transaction-ref REF Exact transactionRef from transactions output.',
|
|
1088
|
+
' --input FILE Reviewed JSON with schemaVersion, currency, and receiptItems.',
|
|
1089
|
+
' Each item has id, label, and signed amountPence; use negative for discounts.',
|
|
1090
|
+
' --expected-revision N Required when replacing saved evidence; omit for a new receipt.',
|
|
1091
|
+
'',
|
|
1092
|
+
'Write behavior:',
|
|
1093
|
+
' Without --apply, prints the exact request and makes no API call.',
|
|
1094
|
+
' With --apply, saves only the reviewed JSON. Images and extraction drafts are not saved.',
|
|
1095
|
+
...API_ORIGIN_HELP_LINES,
|
|
1096
|
+
].join('\n');
|
|
1097
|
+
}
|
|
1098
|
+
export function rulesDeleteHelpText() {
|
|
1099
|
+
return [
|
|
1100
|
+
'Sloth Agent CLI — rules delete',
|
|
1101
|
+
'',
|
|
1102
|
+
'Preview or remove the notification rule for one existing transaction.',
|
|
1103
|
+
'',
|
|
1104
|
+
'Usage:',
|
|
1105
|
+
' sloth-agent rules delete --transaction-ref REF [--apply]',
|
|
1106
|
+
'',
|
|
1107
|
+
'Required:',
|
|
1108
|
+
' --transaction-ref REF The exact transactionRef from sloth-agent transactions.',
|
|
1109
|
+
'',
|
|
1110
|
+
'Write behavior:',
|
|
1111
|
+
' Without --apply, Sloth prints a local deletion preview.',
|
|
1112
|
+
' With --apply, Sloth removes the rule using a write-enabled token.',
|
|
1113
|
+
...API_ORIGIN_HELP_LINES,
|
|
1114
|
+
].join('\n');
|
|
1115
|
+
}
|
|
1116
|
+
export function rulesScanContractHelpText() {
|
|
1117
|
+
return [
|
|
1118
|
+
'Sloth Agent CLI — rules scan-contract',
|
|
1119
|
+
'',
|
|
1120
|
+
'Validate or scan a contract PDF for its renewal date.',
|
|
1121
|
+
'',
|
|
1122
|
+
'Usage:',
|
|
1123
|
+
' sloth-agent rules scan-contract --contract FILE.pdf [--apply]',
|
|
1124
|
+
'',
|
|
1125
|
+
'Required:',
|
|
1126
|
+
' --contract FILE.pdf A PDF no larger than 6 MB.',
|
|
1127
|
+
'',
|
|
1128
|
+
'Behavior:',
|
|
1129
|
+
' Without --apply, the PDF is validated locally and is not sent anywhere.',
|
|
1130
|
+
' With --apply, the PDF is sent using a write-enabled token.',
|
|
1131
|
+
'',
|
|
1132
|
+
'Contract privacy:',
|
|
1133
|
+
' Sloth extracts a renewal date and then discards the file. It is not stored.',
|
|
1134
|
+
'',
|
|
1135
|
+
'Output:',
|
|
1136
|
+
' JSON containing renewalDate and confidence: high, medium, or low.',
|
|
1137
|
+
'',
|
|
1138
|
+
'Example:',
|
|
1139
|
+
' sloth-agent rules scan-contract --contract contract.pdf --apply',
|
|
1140
|
+
...API_ORIGIN_HELP_LINES,
|
|
1141
|
+
].join('\n');
|
|
1142
|
+
}
|
|
1143
|
+
export function receiptsRemoveHelpText() {
|
|
1144
|
+
return [
|
|
1145
|
+
'Sloth Agent CLI — receipts remove',
|
|
1146
|
+
'',
|
|
1147
|
+
'Usage:',
|
|
1148
|
+
' sloth-agent receipts remove --transaction-ref REF --revision N [--apply]',
|
|
1149
|
+
'',
|
|
1150
|
+
' --revision N Current positive revision from receipts get.',
|
|
1151
|
+
'',
|
|
1152
|
+
'Without --apply, prints a preview. With --apply, removes saved receipt items.',
|
|
1153
|
+
...API_ORIGIN_HELP_LINES,
|
|
1154
|
+
].join('\n');
|
|
1155
|
+
}
|
|
939
1156
|
export function commandHelpText(topic) {
|
|
940
1157
|
const helpByTopic = {
|
|
941
1158
|
auth: authHelpText,
|
|
@@ -958,6 +1175,16 @@ export function commandHelpText(topic) {
|
|
|
958
1175
|
'line-items-rename': lineItemsRenameHelpText,
|
|
959
1176
|
transactions: transactionsHelpText,
|
|
960
1177
|
assign: assignHelpText,
|
|
1178
|
+
rules: rulesHelpText,
|
|
1179
|
+
'rules-get': rulesGetHelpText,
|
|
1180
|
+
'rules-set': rulesSetHelpText,
|
|
1181
|
+
'rules-delete': rulesDeleteHelpText,
|
|
1182
|
+
'rules-scan-contract': rulesScanContractHelpText,
|
|
1183
|
+
receipts: receiptsHelpText,
|
|
1184
|
+
'receipts-extract': receiptsExtractHelpText,
|
|
1185
|
+
'receipts-get': receiptsGetHelpText,
|
|
1186
|
+
'receipts-attach': receiptsAttachHelpText,
|
|
1187
|
+
'receipts-remove': receiptsRemoveHelpText,
|
|
961
1188
|
goals: goalsHelpText,
|
|
962
1189
|
'goals-list': goalsListHelpText,
|
|
963
1190
|
'goals-create': goalsCreateHelpText,
|
|
@@ -1064,6 +1291,64 @@ function readBudgetFile(filePath) {
|
|
|
1064
1291
|
throw new UsageError(`Failed to read budget JSON: ${message}`);
|
|
1065
1292
|
}
|
|
1066
1293
|
}
|
|
1294
|
+
function readNotificationRuleFile(filePath) {
|
|
1295
|
+
try {
|
|
1296
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
1297
|
+
}
|
|
1298
|
+
catch (error) {
|
|
1299
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1300
|
+
throw new UsageError(`Failed to read notification rule JSON: ${message}`);
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
function readContractPdf(filePath) {
|
|
1304
|
+
let file;
|
|
1305
|
+
try {
|
|
1306
|
+
file = fs.readFileSync(filePath);
|
|
1307
|
+
}
|
|
1308
|
+
catch (error) {
|
|
1309
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1310
|
+
throw new UsageError(`Failed to read contract PDF: ${message}`);
|
|
1311
|
+
}
|
|
1312
|
+
if (file.length === 0 || file.length > MAX_CONTRACT_PDF_BYTES || file.subarray(0, 4).toString() !== '%PDF') {
|
|
1313
|
+
throw new UsageError('Contract must be a PDF no larger than 6 MB');
|
|
1314
|
+
}
|
|
1315
|
+
return file;
|
|
1316
|
+
}
|
|
1317
|
+
function readReceiptFile(filePath) {
|
|
1318
|
+
try {
|
|
1319
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
1320
|
+
}
|
|
1321
|
+
catch (error) {
|
|
1322
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1323
|
+
throw new UsageError(`Failed to read receipt JSON: ${message}`);
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
function readReceiptImage(filePath) {
|
|
1327
|
+
const extension = filePath.toLowerCase().split('.').pop();
|
|
1328
|
+
const mimeType = extension === 'jpg' || extension === 'jpeg'
|
|
1329
|
+
? 'image/jpeg'
|
|
1330
|
+
: extension === 'png'
|
|
1331
|
+
? 'image/png'
|
|
1332
|
+
: extension === 'webp'
|
|
1333
|
+
? 'image/webp'
|
|
1334
|
+
: null;
|
|
1335
|
+
if (!mimeType)
|
|
1336
|
+
throw new UsageError('Receipt image must be JPEG, PNG, or WebP');
|
|
1337
|
+
try {
|
|
1338
|
+
const body = fs.readFileSync(filePath);
|
|
1339
|
+
if (body.length === 0)
|
|
1340
|
+
throw new UsageError('Receipt image is empty');
|
|
1341
|
+
if (body.length > 8 * 1024 * 1024)
|
|
1342
|
+
throw new UsageError('Receipt image must be 8 MB or smaller');
|
|
1343
|
+
return { body, mimeType };
|
|
1344
|
+
}
|
|
1345
|
+
catch (error) {
|
|
1346
|
+
if (error instanceof UsageError)
|
|
1347
|
+
throw error;
|
|
1348
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1349
|
+
throw new UsageError(`Failed to read receipt image: ${message}`);
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1067
1352
|
function buildTransactionsQuery(filters) {
|
|
1068
1353
|
const params = new URLSearchParams();
|
|
1069
1354
|
if (filters.uncategorized !== undefined) {
|
|
@@ -1081,8 +1366,6 @@ function buildTransactionsQuery(filters) {
|
|
|
1081
1366
|
params.set('q', filters.q);
|
|
1082
1367
|
if (filters.accountRef !== undefined)
|
|
1083
1368
|
params.set('accountRef', filters.accountRef);
|
|
1084
|
-
if (filters.accountId !== undefined)
|
|
1085
|
-
params.set('accountId', filters.accountId);
|
|
1086
1369
|
if (filters.categoryId !== undefined)
|
|
1087
1370
|
params.set('categoryId', filters.categoryId);
|
|
1088
1371
|
if (filters.lineItemId !== undefined)
|
|
@@ -1299,9 +1582,126 @@ export async function runCli(argv = process.argv.slice(2), options = {}) {
|
|
|
1299
1582
|
});
|
|
1300
1583
|
return 0;
|
|
1301
1584
|
}
|
|
1585
|
+
const receiptConfirmation = parsed.command === 'receipts-attach'
|
|
1586
|
+
? validateReceiptConfirmation(readReceiptFile(parsed.input))
|
|
1587
|
+
: undefined;
|
|
1588
|
+
if (parsed.command === 'receipts-attach' && !parsed.apply) {
|
|
1589
|
+
writeJson(writeStdout, {
|
|
1590
|
+
dryRun: true,
|
|
1591
|
+
endpoint: `${baseUrl}/api/agent/v1/receipts/confirmed`,
|
|
1592
|
+
method: 'PUT',
|
|
1593
|
+
payload: {
|
|
1594
|
+
transactionRef: parsed.transactionRef,
|
|
1595
|
+
expectedRevision: parsed.expectedRevision ?? null,
|
|
1596
|
+
receipt: receiptConfirmation,
|
|
1597
|
+
},
|
|
1598
|
+
});
|
|
1599
|
+
return 0;
|
|
1600
|
+
}
|
|
1601
|
+
if (parsed.command === 'receipts-remove' && !parsed.apply) {
|
|
1602
|
+
writeJson(writeStdout, {
|
|
1603
|
+
dryRun: true,
|
|
1604
|
+
endpoint: `${baseUrl}/api/agent/v1/receipts/confirmed`,
|
|
1605
|
+
method: 'DELETE',
|
|
1606
|
+
payload: {
|
|
1607
|
+
transactionRef: parsed.transactionRef,
|
|
1608
|
+
expectedRevision: parsed.revision,
|
|
1609
|
+
},
|
|
1610
|
+
});
|
|
1611
|
+
return 0;
|
|
1612
|
+
}
|
|
1613
|
+
const notificationRulePayload = parsed.command === 'rules-set'
|
|
1614
|
+
? validateNotificationRulePayload(readNotificationRuleFile(parsed.input))
|
|
1615
|
+
: undefined;
|
|
1616
|
+
const notificationRuleEndpoint = (parsed.command === 'rules-get'
|
|
1617
|
+
|| parsed.command === 'rules-set'
|
|
1618
|
+
|| parsed.command === 'rules-delete')
|
|
1619
|
+
? `${baseUrl}/api/agent/v1/notification-rules/for-transaction?${new URLSearchParams({
|
|
1620
|
+
transactionRef: parsed.transactionRef,
|
|
1621
|
+
}).toString()}`
|
|
1622
|
+
: undefined;
|
|
1623
|
+
if (parsed.command === 'rules-set' && !parsed.apply) {
|
|
1624
|
+
writeJson(writeStdout, {
|
|
1625
|
+
dryRun: true,
|
|
1626
|
+
endpoint: notificationRuleEndpoint,
|
|
1627
|
+
method: 'PUT',
|
|
1628
|
+
payload: { transactionRef: parsed.transactionRef, ...notificationRulePayload },
|
|
1629
|
+
});
|
|
1630
|
+
return 0;
|
|
1631
|
+
}
|
|
1632
|
+
if (parsed.command === 'rules-delete' && !parsed.apply) {
|
|
1633
|
+
writeJson(writeStdout, {
|
|
1634
|
+
dryRun: true,
|
|
1635
|
+
endpoint: notificationRuleEndpoint,
|
|
1636
|
+
method: 'DELETE',
|
|
1637
|
+
});
|
|
1638
|
+
return 0;
|
|
1639
|
+
}
|
|
1640
|
+
if (parsed.command === 'rules-scan-contract' && !parsed.apply) {
|
|
1641
|
+
const contract = readContractPdf(parsed.contract);
|
|
1642
|
+
writeJson(writeStdout, {
|
|
1643
|
+
dryRun: true,
|
|
1644
|
+
endpoint: `${baseUrl}/api/agent/v1/notification-rules/extract-renewal`,
|
|
1645
|
+
method: 'POST',
|
|
1646
|
+
contract: { bytes: contract.length, mimeType: 'application/pdf' },
|
|
1647
|
+
});
|
|
1648
|
+
return 0;
|
|
1649
|
+
}
|
|
1302
1650
|
const credential = await resolveCredential(environment, baseUrl, getCredentialStore);
|
|
1303
1651
|
token = credential.token;
|
|
1304
1652
|
const headers = requestHeaders(token);
|
|
1653
|
+
if (parsed.command === 'receipts-extract') {
|
|
1654
|
+
const image = readReceiptImage(parsed.image);
|
|
1655
|
+
const response = await fetchImplementation(`${baseUrl}/api/agent/v1/receipts/extract`, {
|
|
1656
|
+
method: 'POST',
|
|
1657
|
+
headers: { ...headers, 'Content-Type': image.mimeType },
|
|
1658
|
+
body: image.body,
|
|
1659
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
1660
|
+
});
|
|
1661
|
+
const data = parseApiResponse(parsed.command, await parseHttpResponse(response, token));
|
|
1662
|
+
writeJson(writeStdout, data);
|
|
1663
|
+
return 0;
|
|
1664
|
+
}
|
|
1665
|
+
if (parsed.command === 'receipts-get') {
|
|
1666
|
+
const query = new URLSearchParams({ transactionRef: parsed.transactionRef });
|
|
1667
|
+
const response = await fetchImplementation(`${baseUrl}/api/agent/v1/receipts/confirmed?${query.toString()}`, {
|
|
1668
|
+
method: 'GET',
|
|
1669
|
+
headers,
|
|
1670
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
1671
|
+
});
|
|
1672
|
+
const data = parseApiResponse(parsed.command, await parseHttpResponse(response, token));
|
|
1673
|
+
writeJson(writeStdout, data);
|
|
1674
|
+
return 0;
|
|
1675
|
+
}
|
|
1676
|
+
if (parsed.command === 'receipts-attach') {
|
|
1677
|
+
const response = await fetchImplementation(`${baseUrl}/api/agent/v1/receipts/confirmed`, {
|
|
1678
|
+
method: 'PUT',
|
|
1679
|
+
headers: { ...headers, 'Content-Type': 'application/json' },
|
|
1680
|
+
body: JSON.stringify({
|
|
1681
|
+
transactionRef: parsed.transactionRef,
|
|
1682
|
+
expectedRevision: parsed.expectedRevision ?? null,
|
|
1683
|
+
receipt: receiptConfirmation,
|
|
1684
|
+
}),
|
|
1685
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
1686
|
+
});
|
|
1687
|
+
const data = parseApiResponse(parsed.command, await parseHttpResponse(response, token));
|
|
1688
|
+
writeJson(writeStdout, data);
|
|
1689
|
+
return 0;
|
|
1690
|
+
}
|
|
1691
|
+
if (parsed.command === 'receipts-remove') {
|
|
1692
|
+
const response = await fetchImplementation(`${baseUrl}/api/agent/v1/receipts/confirmed`, {
|
|
1693
|
+
method: 'DELETE',
|
|
1694
|
+
headers: { ...headers, 'Content-Type': 'application/json' },
|
|
1695
|
+
body: JSON.stringify({
|
|
1696
|
+
transactionRef: parsed.transactionRef,
|
|
1697
|
+
expectedRevision: parsed.revision,
|
|
1698
|
+
}),
|
|
1699
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
1700
|
+
});
|
|
1701
|
+
const data = parseApiResponse(parsed.command, await parseHttpResponse(response, token));
|
|
1702
|
+
writeJson(writeStdout, data);
|
|
1703
|
+
return 0;
|
|
1704
|
+
}
|
|
1305
1705
|
if (parsed.command === 'accounts-update') {
|
|
1306
1706
|
const endpoint = `${baseUrl}/api/agent/v1/accounts/${encodeURIComponent(parsed.accountRef)}`;
|
|
1307
1707
|
const response = await fetchImplementation(endpoint, {
|
|
@@ -1498,6 +1898,52 @@ export async function runCli(argv = process.argv.slice(2), options = {}) {
|
|
|
1498
1898
|
writeJson(writeStdout, data);
|
|
1499
1899
|
return hasFailures(data) ? 1 : 0;
|
|
1500
1900
|
}
|
|
1901
|
+
if (parsed.command === 'rules-list') {
|
|
1902
|
+
const response = await fetchImplementation(`${baseUrl}/api/agent/v1/notification-rules`, {
|
|
1903
|
+
method: 'GET', headers, signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
1904
|
+
});
|
|
1905
|
+
writeJson(writeStdout, parseApiResponse('rules-list', await parseHttpResponse(response, token)));
|
|
1906
|
+
return 0;
|
|
1907
|
+
}
|
|
1908
|
+
if (parsed.command === 'rules-get') {
|
|
1909
|
+
const response = await fetchImplementation(notificationRuleEndpoint, {
|
|
1910
|
+
method: 'GET', headers, signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
1911
|
+
});
|
|
1912
|
+
writeJson(writeStdout, parseApiResponse('rules-get', await parseHttpResponse(response, token)));
|
|
1913
|
+
return 0;
|
|
1914
|
+
}
|
|
1915
|
+
if (parsed.command === 'rules-set') {
|
|
1916
|
+
const response = await fetchImplementation(notificationRuleEndpoint, {
|
|
1917
|
+
method: 'PUT',
|
|
1918
|
+
headers: { ...headers, 'Content-Type': 'application/json' },
|
|
1919
|
+
body: JSON.stringify({ transactionRef: parsed.transactionRef, ...notificationRulePayload }),
|
|
1920
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
1921
|
+
});
|
|
1922
|
+
writeJson(writeStdout, parseApiResponse('rules-set', await parseHttpResponse(response, token)));
|
|
1923
|
+
return 0;
|
|
1924
|
+
}
|
|
1925
|
+
if (parsed.command === 'rules-delete') {
|
|
1926
|
+
const response = await fetchImplementation(notificationRuleEndpoint, {
|
|
1927
|
+
method: 'DELETE', headers, signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
1928
|
+
});
|
|
1929
|
+
writeJson(writeStdout, parseApiResponse('rules-delete', await parseHttpResponse(response, token)));
|
|
1930
|
+
return 0;
|
|
1931
|
+
}
|
|
1932
|
+
if (parsed.command === 'rules-scan-contract') {
|
|
1933
|
+
const contract = readContractPdf(parsed.contract);
|
|
1934
|
+
const response = await fetchImplementation(`${baseUrl}/api/agent/v1/notification-rules/extract-renewal`, {
|
|
1935
|
+
method: 'POST',
|
|
1936
|
+
headers: { ...headers, 'Content-Type': 'application/json' },
|
|
1937
|
+
body: JSON.stringify({
|
|
1938
|
+
filename: nodePath.basename(parsed.contract),
|
|
1939
|
+
mimeType: 'application/pdf',
|
|
1940
|
+
contentBase64: contract.toString('base64'),
|
|
1941
|
+
}),
|
|
1942
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
1943
|
+
});
|
|
1944
|
+
writeJson(writeStdout, parseApiResponse('rules-scan-contract', await parseHttpResponse(response, token)));
|
|
1945
|
+
return 0;
|
|
1946
|
+
}
|
|
1501
1947
|
if (parsed.command === 'ask-partner') {
|
|
1502
1948
|
const response = await fetchImplementation(`${baseUrl}/api/agent/v1/transaction-explanation-requests`, {
|
|
1503
1949
|
method: 'POST',
|
package/dist/contracts.js
CHANGED
|
@@ -27,6 +27,114 @@ 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(['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) < 1 || Number(renewalReminder.leadDays) > 365) {
|
|
56
|
+
throw new UsageError('renewalReminder.leadDays must be an integer from 1 to 365');
|
|
57
|
+
}
|
|
58
|
+
if (typeof delivery.email !== 'boolean') {
|
|
59
|
+
throw new UsageError('delivery.email must be true or false');
|
|
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: { email: delivery.email },
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function isNotificationRule(value) {
|
|
79
|
+
if (!isObject(value))
|
|
80
|
+
return false;
|
|
81
|
+
const renewalReminder = value.renewalReminder;
|
|
82
|
+
const delivery = value.delivery;
|
|
83
|
+
if (!isObject(renewalReminder) || !isObject(delivery))
|
|
84
|
+
return false;
|
|
85
|
+
try {
|
|
86
|
+
validateNotificationRulePayload({
|
|
87
|
+
amountChange: value.amountChange,
|
|
88
|
+
renewalReminder: {
|
|
89
|
+
enabled: renewalReminder.enabled,
|
|
90
|
+
renewalDate: renewalReminder.renewalDate,
|
|
91
|
+
leadDays: renewalReminder.leadDays,
|
|
92
|
+
},
|
|
93
|
+
delivery: { email: delivery.email },
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
return hasOnlyFields(value, [
|
|
100
|
+
'id', 'transactionRef', 'merchantName', 'currency', 'sourceAmountPence', 'amountChange',
|
|
101
|
+
'renewalReminder', 'delivery', 'createdAt', 'updatedAt',
|
|
102
|
+
])
|
|
103
|
+
&& typeof value.id === 'string'
|
|
104
|
+
&& typeof value.transactionRef === 'string'
|
|
105
|
+
&& typeof value.merchantName === 'string'
|
|
106
|
+
&& typeof value.currency === 'string'
|
|
107
|
+
&& Number.isSafeInteger(value.sourceAmountPence)
|
|
108
|
+
&& hasOnlyFields(renewalReminder, ['enabled', 'renewalDate', 'leadDays', 'remindOn'])
|
|
109
|
+
&& (renewalReminder.remindOn === null || isIsoDate(renewalReminder.remindOn))
|
|
110
|
+
&& hasOnlyFields(delivery, ['inApp', 'email'])
|
|
111
|
+
&& delivery.inApp === true
|
|
112
|
+
&& (value.createdAt === null || isIsoDateTime(value.createdAt))
|
|
113
|
+
&& (value.updatedAt === null || isIsoDateTime(value.updatedAt));
|
|
114
|
+
}
|
|
115
|
+
function isNotificationRuleResponse(value) {
|
|
116
|
+
return isObject(value)
|
|
117
|
+
&& hasOnlyFields(value, ['rule'])
|
|
118
|
+
&& (value.rule === null || isNotificationRule(value.rule));
|
|
119
|
+
}
|
|
120
|
+
function isNotificationRuleListResponse(value) {
|
|
121
|
+
return isObject(value)
|
|
122
|
+
&& hasOnlyFields(value, ['rules'])
|
|
123
|
+
&& Array.isArray(value.rules)
|
|
124
|
+
&& value.rules.every(isNotificationRule);
|
|
125
|
+
}
|
|
126
|
+
function isNotificationRuleDeleteResponse(value) {
|
|
127
|
+
return isObject(value)
|
|
128
|
+
&& hasOnlyFields(value, ['deleted', 'transactionRef'])
|
|
129
|
+
&& value.deleted === true
|
|
130
|
+
&& typeof value.transactionRef === 'string';
|
|
131
|
+
}
|
|
132
|
+
function isRenewalExtractionResponse(value) {
|
|
133
|
+
return isObject(value)
|
|
134
|
+
&& hasOnlyFields(value, ['renewalDate', 'confidence'])
|
|
135
|
+
&& (value.renewalDate === null || isIsoDate(value.renewalDate))
|
|
136
|
+
&& (value.confidence === 'high' || value.confidence === 'medium' || value.confidence === 'low');
|
|
137
|
+
}
|
|
30
138
|
function validateSplit(value, index, splitIndex) {
|
|
31
139
|
const label = `assignments[${index}].categorySplits[${splitIndex}]`;
|
|
32
140
|
const split = requireObject(value, label);
|
|
@@ -193,6 +301,36 @@ export function validateBudgetUpdatePayload(value) {
|
|
|
193
301
|
});
|
|
194
302
|
return { allocations };
|
|
195
303
|
}
|
|
304
|
+
export function validateReceiptConfirmation(value) {
|
|
305
|
+
const receipt = requireObject(value, 'receipt');
|
|
306
|
+
rejectUnknownFields(receipt, new Set(['schemaVersion', 'currency', 'receiptItems']), 'receipt');
|
|
307
|
+
if (receipt.schemaVersion !== 1)
|
|
308
|
+
throw new UsageError('receipt.schemaVersion must be 1');
|
|
309
|
+
if (typeof receipt.currency !== 'string' || !/^[A-Z]{3}$/.test(receipt.currency)) {
|
|
310
|
+
throw new UsageError('receipt.currency must be a three-letter uppercase code');
|
|
311
|
+
}
|
|
312
|
+
if (!Array.isArray(receipt.receiptItems) || receipt.receiptItems.length < 1 || receipt.receiptItems.length > 200) {
|
|
313
|
+
throw new UsageError('receipt.receiptItems must contain between 1 and 200 items');
|
|
314
|
+
}
|
|
315
|
+
const receiptItems = receipt.receiptItems.map((value, index) => {
|
|
316
|
+
const label = `receipt.receiptItems[${index}]`;
|
|
317
|
+
const item = requireObject(value, label);
|
|
318
|
+
rejectUnknownFields(item, new Set(['id', 'label', 'amountPence']), label);
|
|
319
|
+
const id = requireString(item.id, `${label}.id`);
|
|
320
|
+
const itemLabel = requireString(item.label, `${label}.label`);
|
|
321
|
+
if (id.length > 80 || itemLabel.length > 160)
|
|
322
|
+
throw new UsageError(`${label} is too long`);
|
|
323
|
+
if (!Number.isSafeInteger(item.amountPence)) {
|
|
324
|
+
throw new UsageError(`${label}.amountPence must be a signed safe integer`);
|
|
325
|
+
}
|
|
326
|
+
return {
|
|
327
|
+
id,
|
|
328
|
+
label: itemLabel,
|
|
329
|
+
amountPence: Number(item.amountPence),
|
|
330
|
+
};
|
|
331
|
+
});
|
|
332
|
+
return { schemaVersion: 1, currency: receipt.currency, receiptItems };
|
|
333
|
+
}
|
|
196
334
|
function isLineItemMap(value) {
|
|
197
335
|
if (!isObject(value))
|
|
198
336
|
return false;
|
|
@@ -254,9 +392,6 @@ function isTransaction(value) {
|
|
|
254
392
|
&& typeof value.date === 'string'
|
|
255
393
|
&& value.status === 'booked'
|
|
256
394
|
&& isAccountRef(value.accountRef)
|
|
257
|
-
&& typeof value.accountId === 'string'
|
|
258
|
-
&& typeof value.accountDocId === 'string'
|
|
259
|
-
&& typeof value.requisitionId === 'string'
|
|
260
395
|
&& (value.scope === 'personal' || value.scope === 'joint')
|
|
261
396
|
&& (value.categoryId === null || typeof value.categoryId === 'string')
|
|
262
397
|
&& (value.lineItemId === null || typeof value.lineItemId === 'string')
|
|
@@ -726,6 +861,55 @@ function isGoalDeleteResponse(value) {
|
|
|
726
861
|
&& typeof value.deletedGoalId === 'string'
|
|
727
862
|
&& value.deletedGoalId.trim().length > 0);
|
|
728
863
|
}
|
|
864
|
+
function isReceiptConfirmation(value) {
|
|
865
|
+
try {
|
|
866
|
+
validateReceiptConfirmation(value);
|
|
867
|
+
return true;
|
|
868
|
+
}
|
|
869
|
+
catch {
|
|
870
|
+
return false;
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
function isReceiptEvidence(value) {
|
|
874
|
+
if (!isObject(value))
|
|
875
|
+
return false;
|
|
876
|
+
const { revision, receiptTotalPence, confirmedAt, sourceSurface, ...confirmation } = value;
|
|
877
|
+
return (hasOnlyFields(value, [
|
|
878
|
+
'schemaVersion',
|
|
879
|
+
'currency',
|
|
880
|
+
'receiptItems',
|
|
881
|
+
'revision',
|
|
882
|
+
'receiptTotalPence',
|
|
883
|
+
'confirmedAt',
|
|
884
|
+
'sourceSurface',
|
|
885
|
+
])
|
|
886
|
+
&& isReceiptConfirmation(confirmation)
|
|
887
|
+
&& Number.isSafeInteger(revision)
|
|
888
|
+
&& Number(revision) > 0
|
|
889
|
+
&& Number.isSafeInteger(receiptTotalPence)
|
|
890
|
+
&& isIsoDateTime(confirmedAt)
|
|
891
|
+
&& (sourceSurface === 'web' || sourceSurface === 'agent_api'));
|
|
892
|
+
}
|
|
893
|
+
function isReceiptExtractResponse(value) {
|
|
894
|
+
if (!isObject(value) || !hasOnlyFields(value, ['draft']) || !isObject(value.draft))
|
|
895
|
+
return false;
|
|
896
|
+
const { warnings, ...confirmation } = value.draft;
|
|
897
|
+
return (isReceiptConfirmation(confirmation)
|
|
898
|
+
&& Array.isArray(warnings)
|
|
899
|
+
&& warnings.length <= 20
|
|
900
|
+
&& warnings.every((warning) => (warning === 'currency_unclear' || warning === 'total_unclear' || warning === 'item_unclear')));
|
|
901
|
+
}
|
|
902
|
+
function isReceiptLookupResponse(value) {
|
|
903
|
+
return isObject(value)
|
|
904
|
+
&& hasOnlyFields(value, ['receipt'])
|
|
905
|
+
&& (value.receipt === null || isReceiptEvidence(value.receipt));
|
|
906
|
+
}
|
|
907
|
+
function isReceiptMutationResponse(value) {
|
|
908
|
+
return isObject(value) && hasOnlyFields(value, ['receipt']) && isReceiptEvidence(value.receipt);
|
|
909
|
+
}
|
|
910
|
+
function isReceiptDeleteResponse(value) {
|
|
911
|
+
return isObject(value) && hasOnlyFields(value, ['deleted']) && value.deleted === true;
|
|
912
|
+
}
|
|
729
913
|
export function parseApiResponse(command, value) {
|
|
730
914
|
const valid = command === 'accounts'
|
|
731
915
|
? isAccountsResponse(value)
|
|
@@ -749,15 +933,31 @@ export function parseApiResponse(command, value) {
|
|
|
749
933
|
? isLineItemMutationResponse(value)
|
|
750
934
|
: command === 'transactions'
|
|
751
935
|
? isTransactionsResponse(value)
|
|
752
|
-
: command === '
|
|
753
|
-
?
|
|
754
|
-
: command === '
|
|
755
|
-
?
|
|
756
|
-
: command === '
|
|
757
|
-
?
|
|
758
|
-
: command === '
|
|
759
|
-
?
|
|
760
|
-
:
|
|
936
|
+
: command === 'rules-list'
|
|
937
|
+
? isNotificationRuleListResponse(value)
|
|
938
|
+
: command === 'rules-get' || command === 'rules-set'
|
|
939
|
+
? isNotificationRuleResponse(value)
|
|
940
|
+
: command === 'rules-delete'
|
|
941
|
+
? isNotificationRuleDeleteResponse(value)
|
|
942
|
+
: command === 'rules-scan-contract'
|
|
943
|
+
? isRenewalExtractionResponse(value)
|
|
944
|
+
: command === 'receipts-extract'
|
|
945
|
+
? isReceiptExtractResponse(value)
|
|
946
|
+
: command === 'receipts-get'
|
|
947
|
+
? isReceiptLookupResponse(value)
|
|
948
|
+
: command === 'receipts-attach'
|
|
949
|
+
? isReceiptMutationResponse(value)
|
|
950
|
+
: command === 'receipts-remove'
|
|
951
|
+
? isReceiptDeleteResponse(value)
|
|
952
|
+
: command === 'assign'
|
|
953
|
+
? isAssignmentResponse(value)
|
|
954
|
+
: command === 'ask-partner'
|
|
955
|
+
? isPartnerResponse(value)
|
|
956
|
+
: command === 'goals-list'
|
|
957
|
+
? isGoalsResponse(value)
|
|
958
|
+
: command === 'goals-delete'
|
|
959
|
+
? isGoalDeleteResponse(value)
|
|
960
|
+
: isGoalMutationResponse(value);
|
|
761
961
|
if (!valid) {
|
|
762
962
|
const label = command === 'assign' ? 'assignment' : command;
|
|
763
963
|
throw new ApiError(`Invalid ${label} response from the Agent API`);
|