actual-mcp-server 0.8.16 → 0.9.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/README.md +6 -4
- package/dist/src/lib/actual-adapter.js +48 -7
- package/dist/src/lib/schemas/common.js +39 -0
- package/dist/src/tools/transactions_create.js +33 -3
- package/dist/src/tools/transactions_update.js +9 -0
- package/dist/src/tools/transactions_update_batch.js +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -326,11 +326,13 @@ For Claude Desktop (stdio), restart Claude after upgrading.
|
|
|
326
326
|
|------|-------------|
|
|
327
327
|
| `actual_transactions_get` | Get transactions for an account |
|
|
328
328
|
| `actual_transactions_filter` | Filter with advanced criteria |
|
|
329
|
-
| `actual_transactions_create` | Create new transaction(s) |
|
|
329
|
+
| `actual_transactions_create` | Create new transaction(s), including splits (pass a `subtransactions` array) |
|
|
330
330
|
| `actual_transactions_import` | Import and reconcile transactions |
|
|
331
|
-
| `actual_transactions_update` | Update a transaction |
|
|
331
|
+
| `actual_transactions_update` | Update a transaction, or edit the children of an existing split via `subtransactions` |
|
|
332
332
|
| `actual_transactions_delete` | Delete a transaction |
|
|
333
333
|
|
|
334
|
+
> **Split transactions:** pass a `subtransactions` array (each child needs an `amount` in integer cents; `category` and `notes` are optional). The child amounts must sum to the parent `amount` (the server does not enforce this, so the tool does). A split parent carries no category of its own: put categories on the children. `actual_transactions_update` can edit the children of a transaction that is ALREADY a split; converting a plain transaction into a split via update is not supported (create it as a split instead). Note: `actual_transactions_import` forwards a `subtransactions` array to the API but does NOT apply the sum check, so use `actual_transactions_create` for guaranteed-balanced splits.
|
|
335
|
+
|
|
334
336
|
**Utility (1)**
|
|
335
337
|
|
|
336
338
|
| Tool | Description |
|
|
@@ -447,7 +449,7 @@ All configuration is via environment variables. Copy `.env.example` to `.env` to
|
|
|
447
449
|
|----------|---------|----------|-------------|
|
|
448
450
|
| **Actual Budget Connection** ||||
|
|
449
451
|
| `ACTUAL_SERVER_URL` | _(none)_ | Yes | URL of your Actual Budget server. Use the same URL you type in your browser: `http://localhost:5006` (local), `http://192.168.1.x:5006` (network), `https://actual.yourdomain.com` (domain), or `http://actual:5006` (container name if on the same Docker network) |
|
|
450
|
-
| `ACTUAL_PASSWORD` | _(none)_ | Yes |
|
|
452
|
+
| `ACTUAL_PASSWORD` | _(none)_ | Yes | Login password for your Actual Budget **server**, the one you type on its own login screen. Not the same as `ACTUAL_BUDGET_PASSWORD` (which decrypts an E2E-encrypted budget) and unrelated to `AUTH_PROVIDER=oidc` (which controls how MCP *clients* authenticate to this server, not how this server authenticates to Actual). This server always signs in to Actual with a password, so upstream password login must stay enabled |
|
|
451
453
|
| `ACTUAL_BUDGET_SYNC_ID` | _(none)_ | Yes | Budget Sync ID from Actual (Settings then Sync ID) |
|
|
452
454
|
| `ACTUAL_BUDGET_PASSWORD` | _(none)_ | No | Optional encryption password for encrypted budgets |
|
|
453
455
|
| `ALLOW_INSECURE_UPSTREAM` | `false` | No | Allow an `http://` upstream even when `ACTUAL_BUDGET_PASSWORD` is set (#161). Off by default so a plaintext upstream plus an encryption password is refused |
|
|
@@ -727,4 +729,4 @@ The software is provided **as-is**, without warranty of any kind. The author acc
|
|
|
727
729
|
|
|
728
730
|
---
|
|
729
731
|
|
|
730
|
-
**Version:** 0.
|
|
732
|
+
**Version:** 0.9.0 | **Tool Count:** 71 (verified LibreChat-compatible)
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { subtransactionsSum } from './schemas/common.js';
|
|
1
2
|
import api from '@actual-app/api';
|
|
2
3
|
// @actual-app/api is a CJS package (no "type" field). In NodeNext/ESM context TypeScript
|
|
3
4
|
// cannot expose its named exports via static import syntax. At runtime the default import
|
|
@@ -1075,16 +1076,45 @@ export async function updateTransaction(id, fields) {
|
|
|
1075
1076
|
return queueWriteOperation(async () => {
|
|
1076
1077
|
// Pre-flight existence check (#212): the raw API silently no-ops on a missing id,
|
|
1077
1078
|
// so an update that changed nothing would otherwise be reported as success. A
|
|
1078
|
-
// targeted ActualQL query by id keeps this cheap (indexed lookup).
|
|
1079
|
+
// targeted ActualQL query by id keeps this cheap (indexed lookup). #305 extends
|
|
1080
|
+
// the select to is_parent + amount so the split guards below can run off the
|
|
1081
|
+
// same read (single source; no second out-of-queue read).
|
|
1079
1082
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1080
1083
|
const { q } = (await import('@actual-app/api'));
|
|
1081
|
-
const
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
+
const rows = await withConcurrency(() => retry(async () => {
|
|
1085
|
+
// #305: `.options({ splits: 'all' })` is REQUIRED. The default transactions
|
|
1086
|
+
// query excludes split PARENT rows, so a plain filter(id) returns 0 rows for
|
|
1087
|
+
// a split parent and this pre-flight would wrongly report it "not found"
|
|
1088
|
+
// (which is exactly what broke editing a split). `splits: 'all'` returns
|
|
1089
|
+
// regular transactions, split parents, and split children as flat rows, so
|
|
1090
|
+
// the existence + is_parent + amount read is correct for every kind.
|
|
1091
|
+
const res = (await rawRunQuery(q('transactions').options({ splits: 'all' }).filter({ id }).select(['id', 'is_parent', 'amount'])));
|
|
1092
|
+
return Array.isArray(res?.data) ? res.data : [];
|
|
1084
1093
|
}, { retries: 2, backoffMs: 200 }));
|
|
1085
|
-
if (
|
|
1094
|
+
if (rows.length === 0) {
|
|
1086
1095
|
throw new Error(`Transaction "${id}" not found. Use actual_transactions_get to list transactions.`);
|
|
1087
1096
|
}
|
|
1097
|
+
// #305: split-edit guards, BEFORE the raw write. Two rules:
|
|
1098
|
+
// (a) subtransactions may only edit a transaction that is ALREADY a split;
|
|
1099
|
+
// converting a plain transaction into a split via updateTransaction is
|
|
1100
|
+
// broken in @actual-app/api 26.7.0 (it strands orphan children), so it
|
|
1101
|
+
// is rejected here rather than silently corrupting data.
|
|
1102
|
+
// (b) child amounts must sum to the effective parent amount. The API does
|
|
1103
|
+
// not enforce this; the ground truth is the caller's new `amount` if
|
|
1104
|
+
// supplied, else the stored amount read above.
|
|
1105
|
+
const subs = fields?.subtransactions;
|
|
1106
|
+
if (subs) {
|
|
1107
|
+
const row = rows[0];
|
|
1108
|
+
if (row.is_parent !== true) {
|
|
1109
|
+
throw new Error(`Transaction "${id}" is not a split. Converting a plain transaction into a split is not supported here; create the split with actual_transactions_create instead.`);
|
|
1110
|
+
}
|
|
1111
|
+
const providedAmount = fields.amount;
|
|
1112
|
+
const parentAmount = providedAmount != null ? providedAmount : row.amount;
|
|
1113
|
+
const sum = subtransactionsSum(subs);
|
|
1114
|
+
if (sum !== parentAmount) {
|
|
1115
|
+
throw new Error(`Subtransactions must sum to the parent amount. Expected ${parentAmount}, got ${sum}.`);
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1088
1118
|
await withConcurrency(() => retry(() => rawUpdateTransaction(id, fields), { retries: 0, backoffMs: 200, isRetryable: isRetryableError }));
|
|
1089
1119
|
});
|
|
1090
1120
|
}
|
|
@@ -1105,7 +1135,13 @@ export async function updateTransactionBatch(updates) {
|
|
|
1105
1135
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1106
1136
|
const { q } = (await import('@actual-app/api'));
|
|
1107
1137
|
const existing = await withConcurrency(() => retry(async () => {
|
|
1108
|
-
|
|
1138
|
+
// `.options({ splits: 'all' })` for the same reason as the single-update and
|
|
1139
|
+
// delete pre-flights (#305): the default query omits split PARENT rows, so a
|
|
1140
|
+
// batch touching a split parent would route a perfectly valid id to failed[]
|
|
1141
|
+
// as "not found". Splits themselves stay unsupported in batch (FieldsSchema
|
|
1142
|
+
// strips `subtransactions`); this only ensures a split parent's OTHER fields
|
|
1143
|
+
// are updatable here.
|
|
1144
|
+
const res = (await rawRunQuery(q('transactions').options({ splits: 'all' }).filter({ id: { $oneof: ids } }).select(['id'])));
|
|
1109
1145
|
return new Set(Array.isArray(res?.data) ? res.data.map((r) => r.id) : []);
|
|
1110
1146
|
}, { retries: 2, backoffMs: 200 }));
|
|
1111
1147
|
const succeeded = [];
|
|
@@ -1136,7 +1172,12 @@ export async function deleteTransaction(id) {
|
|
|
1136
1172
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1137
1173
|
const { q } = (await import('@actual-app/api'));
|
|
1138
1174
|
const found = await withConcurrency(() => retry(async () => {
|
|
1139
|
-
|
|
1175
|
+
// `.options({ splits: 'all' })` is REQUIRED, same as the update pre-flight
|
|
1176
|
+
// above (#305): the default transactions query excludes split PARENT rows,
|
|
1177
|
+
// so without it a split parent reads as non-existent and this check throws
|
|
1178
|
+
// "not found" before the raw delete is ever reached. The effect was that a
|
|
1179
|
+
// split could be created through the tools but never deleted through them.
|
|
1180
|
+
const res = (await rawRunQuery(q('transactions').options({ splits: 'all' }).filter({ id }).select(['id'])));
|
|
1140
1181
|
return Array.isArray(res?.data) && res.data.length > 0;
|
|
1141
1182
|
}, { retries: 2, backoffMs: 200 }));
|
|
1142
1183
|
if (!found) {
|
|
@@ -163,6 +163,42 @@ export const offBudgetSchema = z
|
|
|
163
163
|
.optional()
|
|
164
164
|
.describe('Whether the account is off-budget');
|
|
165
165
|
// ============================================================================
|
|
166
|
+
// SPLIT (SUBTRANSACTION) SCHEMAS (#305)
|
|
167
|
+
// ============================================================================
|
|
168
|
+
/**
|
|
169
|
+
* A single split child. Only `amount` is required; `category` and `notes` are
|
|
170
|
+
* optional. Child `account`/`date`/`cleared`/`reconciled` are ALWAYS derived
|
|
171
|
+
* from the parent by @actual-app/api's `makeChild` (26.7.0), and `payee_name`
|
|
172
|
+
* is not resolved on children, so extra fields are rejected here (`.strict()`)
|
|
173
|
+
* to keep the contract honest rather than silently ignored.
|
|
174
|
+
*/
|
|
175
|
+
export const subtransactionSchema = z
|
|
176
|
+
.object({
|
|
177
|
+
amount: amountCentsSchema,
|
|
178
|
+
category: categoryIdSchema.optional(),
|
|
179
|
+
notes: notesSchema,
|
|
180
|
+
})
|
|
181
|
+
.strict();
|
|
182
|
+
/**
|
|
183
|
+
* The `subtransactions` array on a split. Bounded per the project convention of
|
|
184
|
+
* bounding every array input (cf. transactions_update_batch, entities_search
|
|
185
|
+
* `.max(100)`, transactions_uncategorized `.max(1000)`).
|
|
186
|
+
*/
|
|
187
|
+
export const subtransactionsArraySchema = z
|
|
188
|
+
.array(subtransactionSchema)
|
|
189
|
+
.min(1, 'A split needs at least one subtransaction')
|
|
190
|
+
.max(100, 'A split cannot exceed 100 subtransactions')
|
|
191
|
+
.describe('Split children; each amount is integer cents and they must sum to the parent amount');
|
|
192
|
+
/**
|
|
193
|
+
* Pure sum helper: the single source of CALCULATION for the split amount
|
|
194
|
+
* invariant (#305). Enforced at two sites (create tool at the Zod layer where
|
|
195
|
+
* the parent amount is in the input; update adapter against
|
|
196
|
+
* `fields.amount ?? dbAmount`) so the arithmetic cannot drift between them.
|
|
197
|
+
*/
|
|
198
|
+
export function subtransactionsSum(subs) {
|
|
199
|
+
return subs.reduce((total, s) => total + s.amount, 0);
|
|
200
|
+
}
|
|
201
|
+
// ============================================================================
|
|
166
202
|
// COMPOSITE SCHEMAS
|
|
167
203
|
// ============================================================================
|
|
168
204
|
/**
|
|
@@ -192,6 +228,9 @@ export const CommonSchemas = {
|
|
|
192
228
|
reconciled: reconciledSchema,
|
|
193
229
|
closed: closedSchema,
|
|
194
230
|
offBudget: offBudgetSchema,
|
|
231
|
+
// Splits (#305)
|
|
232
|
+
subtransaction: subtransactionSchema,
|
|
233
|
+
subtransactions: subtransactionsArraySchema,
|
|
195
234
|
};
|
|
196
235
|
// ============================================================================
|
|
197
236
|
// EXAMPLES
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import adapter from '../lib/actual-adapter.js';
|
|
3
|
-
import { CommonSchemas } from '../lib/schemas/common.js';
|
|
4
|
-
const InputSchema = z
|
|
3
|
+
import { CommonSchemas, subtransactionsSum } from '../lib/schemas/common.js';
|
|
4
|
+
const InputSchema = z
|
|
5
|
+
.object({
|
|
5
6
|
account: CommonSchemas.accountId,
|
|
6
7
|
date: CommonSchemas.date,
|
|
7
8
|
amount: CommonSchemas.amountCents,
|
|
@@ -11,6 +12,31 @@ const InputSchema = z.object({
|
|
|
11
12
|
category: CommonSchemas.categoryId.optional(),
|
|
12
13
|
cleared: CommonSchemas.cleared,
|
|
13
14
|
imported_id: z.string().optional().describe('Original imported transaction ID'),
|
|
15
|
+
subtransactions: CommonSchemas.subtransactions
|
|
16
|
+
.optional()
|
|
17
|
+
.describe('Split this transaction: child amounts (integer cents) must sum to `amount`. Put categories on the children, not the parent.'),
|
|
18
|
+
})
|
|
19
|
+
// #305: cross-field split rules. The API does NOT enforce the sum invariant
|
|
20
|
+
// (a mismatch is stored and only flagged in the app), and a split parent
|
|
21
|
+
// carries no category (it moves to the children), so both are enforced here.
|
|
22
|
+
.superRefine((val, ctx) => {
|
|
23
|
+
if (!val.subtransactions)
|
|
24
|
+
return;
|
|
25
|
+
if (val.category != null) {
|
|
26
|
+
ctx.addIssue({
|
|
27
|
+
code: 'custom',
|
|
28
|
+
path: ['category'],
|
|
29
|
+
message: 'A split parent carries no category; put categories on the subtransactions instead.',
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
const sum = subtransactionsSum(val.subtransactions);
|
|
33
|
+
if (sum !== val.amount) {
|
|
34
|
+
ctx.addIssue({
|
|
35
|
+
code: 'custom',
|
|
36
|
+
path: ['subtransactions'],
|
|
37
|
+
message: `Subtransactions must sum to the parent amount. Expected ${val.amount}, got ${sum}.`,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
14
40
|
});
|
|
15
41
|
const tool = {
|
|
16
42
|
name: 'actual_transactions_create',
|
|
@@ -18,11 +44,15 @@ const tool = {
|
|
|
18
44
|
inputSchema: InputSchema,
|
|
19
45
|
call: async (args, _meta) => {
|
|
20
46
|
const input = InputSchema.parse(args || {});
|
|
47
|
+
// #305: a split parent must carry is_parent so @actual-app/api links the
|
|
48
|
+
// children (without it the subtransactions are ignored). The sum invariant
|
|
49
|
+
// and the no-parent-category rule were already enforced by the schema above.
|
|
50
|
+
const payload = input.subtransactions ? { ...input, is_parent: true } : input;
|
|
21
51
|
try {
|
|
22
52
|
// Use addTransactions - it reliably creates transactions.
|
|
23
53
|
// Note: API may return "ok" string instead of a UUID depending on server version.
|
|
24
54
|
// "ok" is a valid success indicator — the transaction WAS created.
|
|
25
|
-
const result = await adapter.addTransactions(
|
|
55
|
+
const result = await adapter.addTransactions(payload);
|
|
26
56
|
if (!result || result.length === 0) {
|
|
27
57
|
return {
|
|
28
58
|
success: false,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import adapter from '../lib/actual-adapter.js';
|
|
3
|
+
import { CommonSchemas } from '../lib/schemas/common.js';
|
|
3
4
|
const InputSchema = z.object({
|
|
4
5
|
id: z.string().optional().describe('Transaction ID to update (optional for smoke tests, required for actual usage)'),
|
|
5
6
|
fields: z.object({
|
|
@@ -15,6 +16,14 @@ const InputSchema = z.object({
|
|
|
15
16
|
transfer_id: z.string().nullable().optional().describe('Transfer transaction ID if this is a transfer'),
|
|
16
17
|
cleared: z.boolean().nullable().optional().describe('Whether transaction is cleared'),
|
|
17
18
|
reconciled: z.boolean().nullable().optional().describe('Whether transaction is reconciled'),
|
|
19
|
+
// #305: edit the children of an EXISTING split. The child amounts must sum
|
|
20
|
+
// to the parent amount; the target must already be a split. Both are
|
|
21
|
+
// enforced in the adapter pre-flight (it reads is_parent + amount), because
|
|
22
|
+
// the parent amount is not part of this input. Converting a plain
|
|
23
|
+
// transaction into a split here is rejected (unsupported by the API).
|
|
24
|
+
subtransactions: CommonSchemas.subtransactions
|
|
25
|
+
.optional()
|
|
26
|
+
.describe('Replace the children of an existing split; amounts must sum to the parent amount. To create a split, use actual_transactions_create.'),
|
|
18
27
|
}).describe('Fields to update'),
|
|
19
28
|
});
|
|
20
29
|
const tool = {
|
|
@@ -36,7 +36,7 @@ const InputSchema = z.object({
|
|
|
36
36
|
});
|
|
37
37
|
const tool = {
|
|
38
38
|
name: 'actual_transactions_update_batch',
|
|
39
|
-
description: `Update multiple transactions in a single call. Accepts up to 50 {id, fields} pairs. Each update is applied independently: partial failures are reported per-item so you know exactly which succeeded and which failed.
|
|
39
|
+
description: `Update multiple transactions in a single call. Accepts up to 50 {id, fields} pairs. Each update is applied independently: partial failures are reported per-item so you know exactly which succeeded and which failed. Splits are not supported here: a subtransactions field is ignored in batch. Use actual_transactions_update to edit an existing split's children.
|
|
40
40
|
|
|
41
41
|
Returns: { succeeded: [{id}], failed: [{id, error}], total, successCount, failureCount }
|
|
42
42
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "actual-mcp-server",
|
|
3
3
|
"displayName": "Actual MCP Server",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.9.0",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=22.0.0",
|
|
7
7
|
"npm": ">=10.0.0"
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"verify-tools": "npm run build && node scripts/verify-tools.js",
|
|
31
31
|
"check:coverage": "node scripts/list-actual-api-methods.mjs",
|
|
32
32
|
"direct-sync": "node scripts/direct-sync/bank-sync-direct.mjs",
|
|
33
|
-
"test:unit-js": "node tests/unit/transactions_create.test.js && node tests/unit/generated_tools.smoke.test.js && node tests/unit/schema_validation.test.js && node tests/unit/schema_json_openai_compat.test.js && node tests/unit/config_https_validation.test.js && node tests/unit/config_data_dir.test.js && node tests/unit/config_drift.test.js && node tests/unit/port_alignment.test.js && node tests/unit/dockerfile_data_dir_alignment.test.js && node tests/unit/unraid_template_alignment.test.js && node tests/unit/entrypoint_invariants.test.js && node tests/unit/compose_profile_sync.test.js && node tests/unit/advertised_tools_sync.test.js && node tests/unit/knip_config.test.js && node tests/unit/readme_stats_sync.test.js && node tests/unit/config_insecure_upstream.test.js && node tests/unit/workflow_release_guards.test.js && node tests/unit/bot_target_branch.test.js && node tests/unit/auth_posture.test.js && node tests/unit/auth_posture_wiring.test.js && node tests/unit/auth-acl.test.js && node tests/unit/bug76.test.js && node tests/unit/budgets_setAmount.test.js && node tests/unit/budgets_transfer.test.js && node tests/unit/transactions_uncategorized.test.js && node tests/unit/httpServer_session_init.test.js && node tests/unit/httpServer_session_not_found.test.js && node tests/unit/manual_mcp_client_retry.test.js && node tests/unit/manual_mcp_client_session.test.js && node tests/unit/manual_mcp_client_circuit.test.js && node tests/unit/manual_runner_killswitch.test.js && node tests/unit/adapter_auth_rate_limit.test.js && node tests/unit/adapter_session_reuse.test.js && node tests/unit/retry_classifier.test.js && node tests/unit/adapter_module_surface.test.js && node tests/unit/adapter_nonidempotent_no_retry.test.js && node tests/unit/pool_liveness.test.js && node tests/unit/pool_shutdown_all.test.js && node tests/unit/query_where_operators.test.js && node tests/unit/query_run_validation.test.js && node tests/unit/adapter_with_write_session.test.js && node tests/unit/category_groups_delete.test.js && node tests/unit/rules_delete.test.js && node tests/unit/schedules_delete.test.js && node tests/unit/payees_delete.test.js && node tests/unit/rules_create_or_update.test.js && node tests/unit/unhandled-rejection.test.js && node tests/unit/rejection-allowlist-purity.test.js && node tests/unit/httpServer_bearer_auth.test.js && node tests/unit/httpServer_oidc_audience.test.js && node tests/unit/httpServer_oidc_auth_verification.test.js && node tests/unit/httpServer_oidc_jwks_discovery.test.js && node tests/unit/httpServer_body_limit.test.js && node tests/unit/httpServer_bind_host.test.js && node tests/unit/adapter_write_pool_cooperation.test.js && node tests/unit/budget_acl_enforcement.test.js && node tests/unit/budget_preference_store.test.js && node tests/unit/budget_preference_restore.test.js && node tests/unit/tags_list.test.js && node tests/unit/tags_create.test.js && node tests/unit/tags_update.test.js && node tests/unit/tags_delete.test.js && node tests/unit/notes_get.test.js && node tests/unit/notes_update.test.js && node tests/unit/payees_common_list.test.js && node tests/unit/transactions_update_guard.test.js && node tests/unit/entities_search.test.js && node tests/unit/transactions_import_schema.test.js && node tests/unit/tool_schema_shape.test.js && node tests/unit/zod_error_format.test.js && node tests/unit/tool_count_sync.test.js && node tests/unit/stdio_transport.test.js && node tests/unit/logger_structured.test.js && node tests/unit/logger_redaction.test.js && node tests/unit/logger_correlation.test.js && node tests/unit/check_coverage.test.js && node tests/unit/adapter_op_timeout.test.js && node tests/unit/adapter_write_queue_wakeup.test.js && node tests/unit/ci_workflow_guards.test.js && node tests/unit/node_version_guard.test.js && node tests/unit/node_version_drift.test.js && node tests/unit/observability_smoke.test.js && node tests/unit/residue_pattern.test.js && node tests/unit/dual_transport_gate.test.js && node tests/unit/manual_assertions_use_helper.test.js && node tests/unit/server_version_guard.test.js && node tests/unit/payee_rules_reference_filter.test.js && node tests/unit/oidc_metadata_discovery.test.js && node tests/unit/httpServer_oauth_metadata_route.test.js && node tests/unit/httpServer_mcp_info_route.test.js && node tests/unit/residue_sweep_balance_account.test.js && node tests/unit/tool_factory.test.js && node tests/unit/budget_registry_parse.test.js",
|
|
33
|
+
"test:unit-js": "node tests/unit/transactions_create.test.js && node tests/unit/generated_tools.smoke.test.js && node tests/unit/schema_validation.test.js && node tests/unit/schema_json_openai_compat.test.js && node tests/unit/config_https_validation.test.js && node tests/unit/config_data_dir.test.js && node tests/unit/config_drift.test.js && node tests/unit/port_alignment.test.js && node tests/unit/dockerfile_data_dir_alignment.test.js && node tests/unit/unraid_template_alignment.test.js && node tests/unit/entrypoint_invariants.test.js && node tests/unit/compose_profile_sync.test.js && node tests/unit/advertised_tools_sync.test.js && node tests/unit/knip_config.test.js && node tests/unit/readme_stats_sync.test.js && node tests/unit/config_insecure_upstream.test.js && node tests/unit/workflow_release_guards.test.js && node tests/unit/bot_target_branch.test.js && node tests/unit/auth_posture.test.js && node tests/unit/auth_posture_wiring.test.js && node tests/unit/auth-acl.test.js && node tests/unit/bug76.test.js && node tests/unit/budgets_setAmount.test.js && node tests/unit/budgets_transfer.test.js && node tests/unit/transactions_uncategorized.test.js && node tests/unit/httpServer_session_init.test.js && node tests/unit/httpServer_session_not_found.test.js && node tests/unit/manual_mcp_client_retry.test.js && node tests/unit/manual_mcp_client_session.test.js && node tests/unit/manual_mcp_client_circuit.test.js && node tests/unit/manual_runner_killswitch.test.js && node tests/unit/adapter_auth_rate_limit.test.js && node tests/unit/adapter_session_reuse.test.js && node tests/unit/retry_classifier.test.js && node tests/unit/adapter_module_surface.test.js && node tests/unit/adapter_nonidempotent_no_retry.test.js && node tests/unit/pool_liveness.test.js && node tests/unit/pool_shutdown_all.test.js && node tests/unit/query_where_operators.test.js && node tests/unit/query_run_validation.test.js && node tests/unit/adapter_with_write_session.test.js && node tests/unit/category_groups_delete.test.js && node tests/unit/rules_delete.test.js && node tests/unit/schedules_delete.test.js && node tests/unit/payees_delete.test.js && node tests/unit/rules_create_or_update.test.js && node tests/unit/unhandled-rejection.test.js && node tests/unit/rejection-allowlist-purity.test.js && node tests/unit/httpServer_bearer_auth.test.js && node tests/unit/httpServer_oidc_audience.test.js && node tests/unit/httpServer_oidc_auth_verification.test.js && node tests/unit/httpServer_oidc_jwks_discovery.test.js && node tests/unit/httpServer_body_limit.test.js && node tests/unit/httpServer_bind_host.test.js && node tests/unit/adapter_write_pool_cooperation.test.js && node tests/unit/budget_acl_enforcement.test.js && node tests/unit/budget_preference_store.test.js && node tests/unit/budget_preference_restore.test.js && node tests/unit/tags_list.test.js && node tests/unit/tags_create.test.js && node tests/unit/tags_update.test.js && node tests/unit/tags_delete.test.js && node tests/unit/notes_get.test.js && node tests/unit/notes_update.test.js && node tests/unit/payees_common_list.test.js && node tests/unit/transactions_update_guard.test.js && node tests/unit/transactions_update_splits.test.js && node tests/unit/entities_search.test.js && node tests/unit/transactions_import_schema.test.js && node tests/unit/tool_schema_shape.test.js && node tests/unit/zod_error_format.test.js && node tests/unit/tool_count_sync.test.js && node tests/unit/stdio_transport.test.js && node tests/unit/logger_structured.test.js && node tests/unit/logger_redaction.test.js && node tests/unit/logger_correlation.test.js && node tests/unit/check_coverage.test.js && node tests/unit/adapter_op_timeout.test.js && node tests/unit/adapter_write_queue_wakeup.test.js && node tests/unit/ci_workflow_guards.test.js && node tests/unit/node_version_guard.test.js && node tests/unit/node_version_drift.test.js && node tests/unit/observability_smoke.test.js && node tests/unit/residue_pattern.test.js && node tests/unit/dual_transport_gate.test.js && node tests/unit/manual_assertions_use_helper.test.js && node tests/unit/server_version_guard.test.js && node tests/unit/payee_rules_reference_filter.test.js && node tests/unit/oidc_metadata_discovery.test.js && node tests/unit/httpServer_oauth_metadata_route.test.js && node tests/unit/httpServer_mcp_info_route.test.js && node tests/unit/residue_sweep_balance_account.test.js && node tests/unit/tool_factory.test.js && node tests/unit/budget_registry_parse.test.js",
|
|
34
34
|
"test:adapter": "npm run build && node dist/src/tests_adapter_runner.js",
|
|
35
35
|
"test:e2e": "npx playwright test",
|
|
36
36
|
"test:e2e:docker": "./tests/e2e/run-docker-e2e.sh",
|