actual-mcp-server 0.22.37 → 0.22.40
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 +1 -1
- package/dist/src/actualToolsManager.js +20 -6
- package/dist/src/lib/actual-adapter.js +12 -7
- package/dist/src/lib/errors.js +25 -0
- package/dist/src/lib/retry.js +4 -3
- package/dist/src/lib/toolFactory.js +7 -5
- package/dist/src/server/httpServer.js +2 -1
- package/dist/src/tools/categories_create.js +3 -1
- package/dist/src/tools/query_run.js +11 -6
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -836,7 +836,7 @@ The software is provided **as-is**, without warranty of any kind. The author acc
|
|
|
836
836
|
|
|
837
837
|
---
|
|
838
838
|
|
|
839
|
-
**Version:** 0.22.
|
|
839
|
+
**Version:** 0.22.40 | **Tool Count:** 84 (verified LibreChat-compatible)
|
|
840
840
|
|
|
841
841
|
## Sponsor
|
|
842
842
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// src/actualToolsManager.ts
|
|
2
2
|
import logger from './logger.js';
|
|
3
3
|
import { formatZodError } from './lib/zod-error-format.js';
|
|
4
|
+
import { errorLogFields } from './lib/errors.js';
|
|
4
5
|
import config from './config.js';
|
|
5
6
|
import { createModuleLogger } from './lib/loggerFactory.js';
|
|
6
7
|
import { resolvePublishedToolNames, unavailableError } from './lib/toolsets.js';
|
|
@@ -211,26 +212,39 @@ class ActualToolsManager {
|
|
|
211
212
|
toolsetLog.warn('Refused a call to an unpublished tool', { tool: name, setting: unavailable.setting });
|
|
212
213
|
throw unavailable;
|
|
213
214
|
}
|
|
215
|
+
const startTime = Date.now();
|
|
214
216
|
try {
|
|
215
217
|
const result = await tool.call(args);
|
|
216
218
|
// Force-serialize/deserialize to ensure result is JSON-safe (no circular refs,
|
|
217
219
|
// Buffers, class instances, etc). Convert undefined -> null so callers always
|
|
218
220
|
// receive a valid JSON value.
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
+
const json = JSON.stringify(result === undefined ? null : result);
|
|
222
|
+
const safe = JSON.parse(json);
|
|
223
|
+
// Content-free by design (#556): tool arguments and results are the user's budget
|
|
224
|
+
// data and never reach the logs. This record is the one place content COULD be
|
|
225
|
+
// logged, so an opt-in for debugging (#557) belongs here and nowhere else.
|
|
226
|
+
// `durationMs`, not `ms`: the logger reserves a top-level `ms` key (src/logger.ts).
|
|
227
|
+
logger.info('[TOOL RESULT]', { tool: name, durationMs: Date.now() - startTime, outcome: 'ok', bytes: Buffer.byteLength(json, 'utf8') });
|
|
221
228
|
return safe;
|
|
222
229
|
}
|
|
223
230
|
catch (err) {
|
|
224
231
|
// Format Zod validation errors into one actionable, consistent message (#206).
|
|
225
232
|
// Duck-type on `.issues` so any structurally-compatible ZodError is handled.
|
|
233
|
+
// The failure record mirrors the success one (#556): tool, duration and outcome,
|
|
234
|
+
// never the arguments. It never carries the error MESSAGE either (#559): that text
|
|
235
|
+
// is for the client and can quote input or upstream content. errorLogFields gives
|
|
236
|
+
// the class, a refusal's kind, and the throw site's withLogMessage text if any.
|
|
237
|
+
const failure = { tool: name, durationMs: Date.now() - startTime, outcome: 'error' };
|
|
226
238
|
if (err && typeof err === 'object' && 'issues' in err) {
|
|
227
239
|
const formattedMsg = formatZodError(err, tool.inputSchema);
|
|
228
|
-
|
|
240
|
+
// The #206 text is for the client only: a schema's custom refine message can quote
|
|
241
|
+
// input (transactions_create echoes the split amounts), so the log gets the issue
|
|
242
|
+
// codes, which come from Zod and never from the arguments.
|
|
243
|
+
const issueCodes = [...new Set((err.issues ?? []).map((i) => String(i?.code)))];
|
|
244
|
+
logger.error(`[TOOL ERROR] ${name}`, { ...failure, errorClass: 'ZodError', issueCodes });
|
|
229
245
|
throw new Error(formattedMsg);
|
|
230
246
|
}
|
|
231
|
-
|
|
232
|
-
const msg = e && typeof e.message === 'string' ? e.message : String(err);
|
|
233
|
-
logger.error(`[TOOL ERROR] ${name}: ${msg}`);
|
|
247
|
+
logger.error(`[TOOL ERROR] ${name}`, { ...failure, ...errorLogFields(err) });
|
|
234
248
|
throw err;
|
|
235
249
|
}
|
|
236
250
|
}
|
|
@@ -13,7 +13,7 @@ import observability from '../observability.js';
|
|
|
13
13
|
import { retry, isRetryableError, isRateLimitError } from './retry.js';
|
|
14
14
|
import { withOpTimeout } from './opTimeout.js';
|
|
15
15
|
import { markInitFailure } from './init-failure.js';
|
|
16
|
-
import { NotFoundRefusal, OutOfRangeRefusal, constraintErrorMsg } from './errors.js';
|
|
16
|
+
import { NotFoundRefusal, OutOfRangeRefusal, constraintErrorMsg, errorLogFields } from './errors.js';
|
|
17
17
|
import { amountOpMismatch } from './schemas/schedule-amount.js';
|
|
18
18
|
import { findMatchingRule } from './rule-matching.js';
|
|
19
19
|
import logger from '../logger.js';
|
|
@@ -1198,7 +1198,9 @@ async function processWriteQueue() {
|
|
|
1198
1198
|
resolve(result);
|
|
1199
1199
|
}
|
|
1200
1200
|
catch (error) {
|
|
1201
|
-
|
|
1201
|
+
// Class and log-safe text only (#559): a refusal names the id the caller sent
|
|
1202
|
+
// (which can be a name, #388), and upstream text can quote the written values.
|
|
1203
|
+
logger.error('[WRITE QUEUE] Operation failed', errorLogFields(error));
|
|
1202
1204
|
// #419: remember an infrastructure-level failure so the batch-end
|
|
1203
1205
|
// shutdown self-heals (see drainForceFullShutdown above).
|
|
1204
1206
|
if (_shouldDropPoolOnError(error))
|
|
@@ -1656,7 +1658,8 @@ export async function createCategory(category) {
|
|
|
1656
1658
|
return normalizeToId(raw);
|
|
1657
1659
|
}
|
|
1658
1660
|
catch (error) {
|
|
1659
|
-
|
|
1661
|
+
// Upstream's message quotes the category name (#559), so the log gets the class only.
|
|
1662
|
+
logger.error('[CREATE CATEGORY] Error creating category', errorLogFields(error));
|
|
1660
1663
|
// Re-throw the error with proper context
|
|
1661
1664
|
if (error instanceof Error) {
|
|
1662
1665
|
throw error;
|
|
@@ -2672,7 +2675,8 @@ export async function updateRule(id, fields) {
|
|
|
2672
2675
|
// omitted conditions or actions takes the stored value, which contributes no new refs.
|
|
2673
2676
|
const addedRefs = newRuleReferences(rule, existingRule);
|
|
2674
2677
|
assertRuleReferencesExist(addedRefs, await loadRuleReferenceListings(addedRefs), RULE_REFUSAL_CHANGED);
|
|
2675
|
-
|
|
2678
|
+
// Rule id only: conditions and actions carry payee ids, notes and amounts (#556).
|
|
2679
|
+
logger.debug(`[UPDATE RULE] Updating rule ${id}`);
|
|
2676
2680
|
await withConcurrency(() => retry(() => rawUpdateRule(rule), { retries: 0, backoffMs: 200, isRetryable: isRetryableError }));
|
|
2677
2681
|
logger.debug(`[UPDATE RULE] Update completed for rule ${id}`);
|
|
2678
2682
|
}, { preservesListings: PRESERVES_ALL_ENTITY_LISTINGS });
|
|
@@ -3442,7 +3446,7 @@ export async function runQuery(queryString) {
|
|
|
3442
3446
|
catch (err) {
|
|
3443
3447
|
// Catch errors from the query execution to prevent unhandled rejections
|
|
3444
3448
|
const msg = err?.message || String(err);
|
|
3445
|
-
logger.error(
|
|
3449
|
+
logger.error('[ADAPTER] Query execution error', errorLogFields(err)); // #559: msg can quote the query
|
|
3446
3450
|
if (msg.includes('does not exist in table') || msg.includes('Field') || msg.includes('does not exist')) {
|
|
3447
3451
|
throw new Error(`Invalid field in query: ${msg}`);
|
|
3448
3452
|
}
|
|
@@ -3550,7 +3554,7 @@ export async function runQuery(queryString) {
|
|
|
3550
3554
|
catch (err) {
|
|
3551
3555
|
// Catch errors from the query execution to prevent unhandled rejections
|
|
3552
3556
|
const msg = err?.message || String(err);
|
|
3553
|
-
logger.error(
|
|
3557
|
+
logger.error('[ADAPTER] Query execution error', errorLogFields(err)); // #559: msg can quote the query
|
|
3554
3558
|
if (msg.includes('does not exist in table') || msg.includes('Field') || msg.includes('does not exist')) {
|
|
3555
3559
|
throw new Error(`Invalid field in query: ${msg}`);
|
|
3556
3560
|
}
|
|
@@ -3585,7 +3589,8 @@ export async function runQuery(queryString) {
|
|
|
3585
3589
|
catch (error) {
|
|
3586
3590
|
// Top-level catch to ensure no unhandled rejections escape
|
|
3587
3591
|
const errorMsg = error?.message || String(error);
|
|
3588
|
-
|
|
3592
|
+
// The message can embed the whole query (#559); the client still receives it below.
|
|
3593
|
+
logger.error('[ADAPTER] Query execution failed', errorLogFields(error));
|
|
3589
3594
|
// If the error already contains formatted validation errors with suggestions, preserve them
|
|
3590
3595
|
if (errorMsg.includes('Invalid SQL query:') && (errorMsg.includes('Available fields:') || errorMsg.includes('Available tables:'))) {
|
|
3591
3596
|
throw error; // Re-throw the well-formatted validation error without wrapping
|
package/dist/src/lib/errors.js
CHANGED
|
@@ -148,3 +148,28 @@ export function constraintErrorMsg(entityType, id, listTool) {
|
|
|
148
148
|
return `Failed to delete ${entityType.toLowerCase()} "${id}". ` +
|
|
149
149
|
`It may be referenced by other records. Use ${listTool} to verify it exists.`;
|
|
150
150
|
}
|
|
151
|
+
/**
|
|
152
|
+
* Log-safe error text (#559). An error's MESSAGE is for the client and may quote what the
|
|
153
|
+
* caller sent: a category name, a query, a rule value, or upstream text we do not control.
|
|
154
|
+
* The server never logs it. `withLogMessage` attaches a separate, content-free sentence
|
|
155
|
+
* that the `[TOOL ERROR]` record may log instead; set it where the error is thrown, and
|
|
156
|
+
* keep anything the caller supplied out of it.
|
|
157
|
+
*/
|
|
158
|
+
const LOG_MESSAGE = Symbol.for('actual-mcp-server.logMessage');
|
|
159
|
+
export function withLogMessage(error, logMessage) {
|
|
160
|
+
Object.defineProperty(error, LOG_MESSAGE, { value: logMessage, enumerable: false });
|
|
161
|
+
return error;
|
|
162
|
+
}
|
|
163
|
+
export function errorLogFields(error) {
|
|
164
|
+
const errorClass = error instanceof Error ? (Object.getPrototypeOf(error)?.constructor?.name || 'Error') : error === null ? 'null' : typeof error;
|
|
165
|
+
const fields = { errorClass };
|
|
166
|
+
if (isPreflightRefusal(error))
|
|
167
|
+
fields.refusalKind = error.refusalKind;
|
|
168
|
+
const code = typeof error === 'object' && error !== null ? error.code : undefined;
|
|
169
|
+
if (typeof code === 'string' && /^E[A-Z0-9_]{1,40}$/.test(code))
|
|
170
|
+
fields.code = code;
|
|
171
|
+
const logMessage = typeof error === 'object' && error !== null ? error[LOG_MESSAGE] : undefined;
|
|
172
|
+
if (typeof logMessage === 'string')
|
|
173
|
+
fields.logMessage = logMessage;
|
|
174
|
+
return fields;
|
|
175
|
+
}
|
package/dist/src/lib/retry.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { DEFAULT_RETRY_ATTEMPTS, DEFAULT_RETRY_BACKOFF_MS, MAX_RETRY_DELAY_MS } from './constants.js';
|
|
2
2
|
import { ModuleLoggers } from './loggerFactory.js';
|
|
3
|
+
import { errorLogFields } from './errors.js';
|
|
3
4
|
const log = ModuleLoggers.RETRY;
|
|
4
5
|
/**
|
|
5
6
|
* Error-message fragments that mark a TRANSIENT / infrastructure-level failure:
|
|
@@ -92,16 +93,16 @@ export async function retry(fn, opts) {
|
|
|
92
93
|
// (#177). With no classifier, behaviour is unchanged (retry until the
|
|
93
94
|
// attempt budget is exhausted), preserving every existing call site.
|
|
94
95
|
if (isRetryable && !isRetryable(err)) {
|
|
95
|
-
log.debug('Not retrying non-transient error',
|
|
96
|
+
log.debug('Not retrying non-transient error', errorLogFields(err));
|
|
96
97
|
throw err;
|
|
97
98
|
}
|
|
98
99
|
attempt++;
|
|
99
100
|
if (attempt > retries) {
|
|
100
|
-
log.error(`All retry attempts exhausted after ${retries} tries`, err);
|
|
101
|
+
log.error(`All retry attempts exhausted after ${retries} tries`, undefined, errorLogFields(err));
|
|
101
102
|
throw err;
|
|
102
103
|
}
|
|
103
104
|
const delay = Math.min(backoffMs * Math.pow(2, attempt - 1), MAX_RETRY_DELAY_MS);
|
|
104
|
-
log.debug(`Retry attempt ${attempt}/${retries} after ${delay}ms`,
|
|
105
|
+
log.debug(`Retry attempt ${attempt}/${retries} after ${delay}ms`, errorLogFields(err));
|
|
105
106
|
await new Promise(r => setTimeout(r, delay));
|
|
106
107
|
}
|
|
107
108
|
}
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { createModuleLogger } from './loggerFactory.js';
|
|
16
16
|
import observability from '../observability.js';
|
|
17
|
+
import { errorLogFields } from './errors.js';
|
|
17
18
|
const log = createModuleLogger('TOOLS');
|
|
18
19
|
/**
|
|
19
20
|
* Create a tool definition with automatic error handling and logging
|
|
@@ -54,23 +55,24 @@ export function createTool(config) {
|
|
|
54
55
|
call: async (args, meta) => {
|
|
55
56
|
const startTime = Date.now();
|
|
56
57
|
try {
|
|
58
|
+
// No args, input or result in these records (#556): they are budget data.
|
|
57
59
|
// Validate input against schema
|
|
58
|
-
log.debug(`Validating input for ${name}
|
|
60
|
+
log.debug(`Validating input for ${name}`);
|
|
59
61
|
const input = schema.parse(args || {});
|
|
60
62
|
// Execute handler
|
|
61
|
-
log.debug(`Executing ${name}
|
|
63
|
+
log.debug(`Executing ${name}`);
|
|
62
64
|
const result = await handler(input, meta);
|
|
63
65
|
// Log success
|
|
64
66
|
const duration = Date.now() - startTime;
|
|
65
|
-
log.debug(`${name} completed in ${duration}ms
|
|
67
|
+
log.debug(`${name} completed in ${duration}ms`);
|
|
66
68
|
// Track observability
|
|
67
69
|
await observability.incrementToolCall(name);
|
|
68
70
|
return { result };
|
|
69
71
|
}
|
|
70
72
|
catch (error) {
|
|
71
|
-
//
|
|
73
|
+
// Class and log-safe text only (#559): the message and stack can quote input.
|
|
72
74
|
const duration = Date.now() - startTime;
|
|
73
|
-
log.error(`${name} failed after ${duration}ms`,
|
|
75
|
+
log.error(`${name} failed after ${duration}ms`, undefined, { tool: name, durationMs: duration, outcome: 'error', ...errorLogFields(error) });
|
|
74
76
|
// Track observability (still increment even on failure)
|
|
75
77
|
await observability.incrementToolCall(name);
|
|
76
78
|
// Re-throw for MCP error handling
|
|
@@ -403,7 +403,8 @@ advertisedUrl) {
|
|
|
403
403
|
throw new Error('Tool name must be a string');
|
|
404
404
|
}
|
|
405
405
|
const name = rawName;
|
|
406
|
-
|
|
406
|
+
// Tool name only: arguments are budget data and never reach the logs (#556).
|
|
407
|
+
logger.debug(`[TOOL CALL] ${name}`);
|
|
407
408
|
// Prefer ActualMCPConnection executor if provided
|
|
408
409
|
if (typeof mcp.executeTool === 'function') {
|
|
409
410
|
const result = await mcp.executeTool(name, args ?? {});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import adapter from '../lib/actual-adapter.js';
|
|
3
3
|
import { CommonSchemas } from '../lib/schemas/common.js';
|
|
4
|
+
import { withLogMessage } from '../lib/errors.js';
|
|
4
5
|
const InputSchema = z.object({
|
|
5
6
|
name: CommonSchemas.name,
|
|
6
7
|
group_id: CommonSchemas.categoryGroupId,
|
|
@@ -36,7 +37,8 @@ const tool = {
|
|
|
36
37
|
throw new Error(`Failed to create category: Invalid or missing group_id. Use actual_category_groups_get to get available category groups first.`);
|
|
37
38
|
}
|
|
38
39
|
if (errorMessage.includes('already exists') || errorMessage.includes('duplicate')) {
|
|
39
|
-
|
|
40
|
+
// The client needs the name; the log gets content-free text (#559).
|
|
41
|
+
throw withLogMessage(new Error(`Failed to create category: A category named "${input.name}" already exists in this group.`), 'category name already exists in this group');
|
|
40
42
|
}
|
|
41
43
|
throw new Error(`Failed to create category: ${errorMessage}`);
|
|
42
44
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import adapter from '../lib/actual-adapter.js';
|
|
3
3
|
import { validateQueryShape } from '../lib/query-validator.js';
|
|
4
|
+
import { errorLogFields, withLogMessage } from '../lib/errors.js';
|
|
4
5
|
const InputSchema = z.object({
|
|
5
6
|
query: z.string().min(1).describe('ActualQL query string to execute'),
|
|
6
7
|
});
|
|
@@ -27,7 +28,8 @@ Tables:
|
|
|
27
28
|
const input = InputSchema.parse(args || {});
|
|
28
29
|
// Detect and reject GraphQL-like syntax with nested objects
|
|
29
30
|
if (input.query.trim().startsWith('query ') && input.query.includes('{') && input.query.includes('}')) {
|
|
30
|
-
|
|
31
|
+
// The client sees its own query echoed; the log gets content-free text (#559).
|
|
32
|
+
throw withLogMessage(new Error(`GraphQL syntax is not fully supported. Please use SQL instead.\n\nExample: SELECT id, date, amount, payee.name, category.name FROM transactions ORDER BY date DESC LIMIT 5\n\nYour query attempted: ${input.query.substring(0, 100)}...`), 'GraphQL-style query rejected');
|
|
31
33
|
}
|
|
32
34
|
// Read-only shape gate (#162): reject writes / schema changes / stacked
|
|
33
35
|
// statements before the query reaches the q() builder. Throws on violation.
|
|
@@ -38,21 +40,24 @@ Tables:
|
|
|
38
40
|
catch (error) {
|
|
39
41
|
// Provide helpful error messages
|
|
40
42
|
const errorMessage = error?.message || String(error);
|
|
43
|
+
// Each rewrap keeps the original's log-safe text (#559), e.g. the GraphQL rejection above.
|
|
44
|
+
const logSafe = errorLogFields(error).logMessage;
|
|
45
|
+
const carry = (e) => (logSafe ? withLogMessage(e, logSafe) : e);
|
|
41
46
|
// Check if error is about payee_name, category_name, account_name
|
|
42
47
|
if (errorMessage.includes('payee_name') || errorMessage.includes('category_name') || errorMessage.includes('account_name')) {
|
|
43
|
-
throw new Error(`Field name error: Use dot notation for joins.\n• Use payee.name (NOT payee_name)\n• Use category.name (NOT category_name)\n• Use account.name (NOT account_name)\n\nExample: SELECT id, date, amount, payee.name FROM transactions LIMIT 5\n\nOriginal error: ${errorMessage}`);
|
|
48
|
+
throw carry(new Error(`Field name error: Use dot notation for joins.\n• Use payee.name (NOT payee_name)\n• Use category.name (NOT category_name)\n• Use account.name (NOT account_name)\n\nExample: SELECT id, date, amount, payee.name FROM transactions LIMIT 5\n\nOriginal error: ${errorMessage}`));
|
|
44
49
|
}
|
|
45
50
|
if (errorMessage.includes('does not exist in the schema')) {
|
|
46
|
-
throw new Error(`Invalid table or field name. Available tables: transactions, accounts, categories, payees, category_groups, schedules, rules. Use dot notation for joins (e.g., category.name). Original error: ${errorMessage}`);
|
|
51
|
+
throw carry(new Error(`Invalid table or field name. Available tables: transactions, accounts, categories, payees, category_groups, schedules, rules. Use dot notation for joins (e.g., category.name). Original error: ${errorMessage}`));
|
|
47
52
|
}
|
|
48
53
|
else if (errorMessage.includes('ActualQL query builder not available')) {
|
|
49
|
-
throw new Error('ActualQL query builder is not available. The Actual Budget API may not be properly initialized.');
|
|
54
|
+
throw carry(new Error('ActualQL query builder is not available. The Actual Budget API may not be properly initialized.'));
|
|
50
55
|
}
|
|
51
56
|
else if (errorMessage.includes('parse') || errorMessage.includes('syntax')) {
|
|
52
|
-
throw new Error(`Query syntax error: ${errorMessage}\n\nRecommended: Use SQL syntax\nExample: SELECT * FROM transactions ORDER BY date DESC LIMIT 5\n\nSee the tool description for the supported WHERE operators and join syntax.`);
|
|
57
|
+
throw carry(new Error(`Query syntax error: ${errorMessage}\n\nRecommended: Use SQL syntax\nExample: SELECT * FROM transactions ORDER BY date DESC LIMIT 5\n\nSee the tool description for the supported WHERE operators and join syntax.`));
|
|
53
58
|
}
|
|
54
59
|
else {
|
|
55
|
-
throw new Error(`Query execution failed: ${errorMessage}`);
|
|
60
|
+
throw carry(new Error(`Query execution failed: ${errorMessage}`));
|
|
56
61
|
}
|
|
57
62
|
}
|
|
58
63
|
},
|
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.22.
|
|
4
|
+
"version": "0.22.40",
|
|
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/transactions_aggregate.test.js && node tests/unit/account_flow_summary.test.js && node tests/unit/recurring_expenses_summary.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/e2e_spec_collection.test.js && node tests/unit/compose_profile_sync.test.js && node tests/unit/advertised_tools_sync.test.js && node tests/unit/new_tool_completeness.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/report_train_failure.test.js && node tests/unit/unit_chain_membership.test.js && node tests/unit/api_surface_drift.test.js && node tests/unit/write_effect_audit_staleness.test.js && node tests/unit/write_effect_audit_membership.test.js && node tests/unit/tool_annotations.test.js && node tests/unit/tool_id_schema_drift.test.js && node tests/unit/filter_id_resolution.test.js && node tests/unit/filter_id_tool_wiring.test.js && node tests/unit/api_coverage_baseline.test.js && node tests/unit/train_preflight.test.js && node tests/unit/verify_published_artifacts.test.js && node tests/unit/report_train_stale.test.js && node tests/unit/stdio_framing_check.test.js && node tests/unit/server_info.test.js && node tests/unit/transfers_create.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/adapter_set_budget_batch.test.js && node tests/unit/budget_updates_batch.test.js && node tests/unit/rules_create.test.js && node tests/unit/adapter_rule_reference_guard.test.js && node tests/unit/rules_create_batch.test.js && node tests/unit/budgets_hold_for_next_month.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/httpServer_init_failure_report.test.js && node tests/unit/stdio_init_failure_report.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/accounts_delete.test.js && node tests/unit/update_tools_not_found.test.js && node tests/unit/accounts_close.test.js && node tests/unit/accounts_reopen.test.js && node tests/unit/payees_delete.test.js && node tests/unit/payees_merge.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/session_ownership.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/oidc_resource_identifier.test.js && node tests/unit/httpServer_oidc_resource_metadata.test.js && node tests/unit/oidc_error_cause.test.js && node tests/unit/httpServer_oidc_error_body.test.js && node tests/unit/oidc_token_source.test.js && node tests/unit/config_oidc_assertion_refines.test.js && node tests/unit/httpServer_oidc_assertion_mode.test.js && node tests/unit/httpServer_body_limit.test.js && node tests/unit/httpErrorHandler.test.js && node tests/unit/httpServer_error_handler_wiring.test.js && node tests/unit/httpServer_trust_proxy.test.js && node tests/unit/httpServer_batch_cap.test.js && node tests/unit/dotenv_stdout_quiet.test.js && node tests/unit/httpServer_bind_host.test.js && node tests/unit/adapter_write_pool_cooperation.test.js && node tests/unit/adapter_drain_listing_cache.test.js && node tests/unit/budget_acl_enforcement.test.js && node tests/unit/budget_selection_precondition.test.js && node tests/unit/budget_load_abandonment.test.js && node tests/unit/budget_loader_postcondition.test.js && node tests/unit/budget_loader_import_allowlist.test.js && node tests/unit/budget_import_tracking.test.js && node tests/unit/pool_shutdown_under_lock.test.js && node tests/unit/api_lock_affinity.test.js && node tests/unit/commit_message_closing_keyword.test.js && node tests/unit/write_queue_stranding.test.js && node tests/unit/write_queue_stranded_bound.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/transactions_update_split_convert.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/transactions_update_batch.test.js && node tests/unit/tool_count_sync.test.js && node tests/unit/stdio_transport.test.js && node tests/unit/stdio_budget_switch.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/playwright_version_drift.test.js && node tests/unit/observability_smoke.test.js && node tests/unit/residue_pattern.test.js && node tests/unit/batch_uncategorized_cleanup.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/server_version_probe_outside_op_timeout.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 && node tests/unit/preferences_get.test.js && node tests/unit/budgets_export.test.js && node tests/unit/budgets_import.test.js && node tests/unit/budgets_import_syncid.test.js && node tests/unit/budget_acl_dynamic.test.js && node tests/unit/budget_acl_identity_map.test.js && node tests/unit/budget_acl_userinfo.test.js && node tests/unit/acl_e2e_fixture_contract.test.js && node tests/unit/budget_acl_middleware.test.js && node tests/unit/rules_stage.test.js && node tests/unit/oidc_scope_list.test.js && node tests/unit/get_context.test.js && node tests/unit/contributor_docs.test.js && node tests/unit/toolsets.test.js && node tests/unit/toolsets_entrypoint.test.js && node tests/unit/tools_list_size_budget.test.js && node tests/unit/rules_update_schedules_schema.test.js && node tests/unit/schedules_update_amount_guard.test.js && node tests/unit/source_text_helper.test.js && node tests/unit/transport_onerror.test.js && node tests/unit/adapter_transaction_batch_abort.test.js && node tests/unit/transactions_merge_adapter.test.js && node tests/unit/schedules_dead_conditions.test.js && node tests/unit/session_ownership_express.test.js && node tests/unit/pool_orphan_sweep.test.js && node tests/unit/budget_acl_parse.test.js",
|
|
33
|
+
"test:unit-js": "node tests/unit/transactions_create.test.js && node tests/unit/transactions_aggregate.test.js && node tests/unit/account_flow_summary.test.js && node tests/unit/recurring_expenses_summary.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/e2e_spec_collection.test.js && node tests/unit/compose_profile_sync.test.js && node tests/unit/advertised_tools_sync.test.js && node tests/unit/new_tool_completeness.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/report_train_failure.test.js && node tests/unit/unit_chain_membership.test.js && node tests/unit/api_surface_drift.test.js && node tests/unit/write_effect_audit_staleness.test.js && node tests/unit/write_effect_audit_membership.test.js && node tests/unit/tool_annotations.test.js && node tests/unit/tool_id_schema_drift.test.js && node tests/unit/filter_id_resolution.test.js && node tests/unit/filter_id_tool_wiring.test.js && node tests/unit/api_coverage_baseline.test.js && node tests/unit/train_preflight.test.js && node tests/unit/verify_published_artifacts.test.js && node tests/unit/report_train_stale.test.js && node tests/unit/stdio_framing_check.test.js && node tests/unit/server_info.test.js && node tests/unit/transfers_create.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/adapter_set_budget_batch.test.js && node tests/unit/budget_updates_batch.test.js && node tests/unit/rules_create.test.js && node tests/unit/adapter_rule_reference_guard.test.js && node tests/unit/rules_create_batch.test.js && node tests/unit/budgets_hold_for_next_month.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/httpServer_init_failure_report.test.js && node tests/unit/stdio_init_failure_report.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/accounts_delete.test.js && node tests/unit/update_tools_not_found.test.js && node tests/unit/accounts_close.test.js && node tests/unit/accounts_reopen.test.js && node tests/unit/payees_delete.test.js && node tests/unit/payees_merge.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/session_ownership.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/oidc_resource_identifier.test.js && node tests/unit/httpServer_oidc_resource_metadata.test.js && node tests/unit/oidc_error_cause.test.js && node tests/unit/httpServer_oidc_error_body.test.js && node tests/unit/oidc_token_source.test.js && node tests/unit/config_oidc_assertion_refines.test.js && node tests/unit/httpServer_oidc_assertion_mode.test.js && node tests/unit/httpServer_body_limit.test.js && node tests/unit/httpErrorHandler.test.js && node tests/unit/httpServer_error_handler_wiring.test.js && node tests/unit/httpServer_trust_proxy.test.js && node tests/unit/httpServer_batch_cap.test.js && node tests/unit/dotenv_stdout_quiet.test.js && node tests/unit/httpServer_bind_host.test.js && node tests/unit/adapter_write_pool_cooperation.test.js && node tests/unit/adapter_drain_listing_cache.test.js && node tests/unit/budget_acl_enforcement.test.js && node tests/unit/budget_selection_precondition.test.js && node tests/unit/budget_load_abandonment.test.js && node tests/unit/budget_loader_postcondition.test.js && node tests/unit/budget_loader_import_allowlist.test.js && node tests/unit/budget_import_tracking.test.js && node tests/unit/pool_shutdown_under_lock.test.js && node tests/unit/api_lock_affinity.test.js && node tests/unit/commit_message_closing_keyword.test.js && node tests/unit/write_queue_stranding.test.js && node tests/unit/write_queue_stranded_bound.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/transactions_update_split_convert.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/transactions_update_batch.test.js && node tests/unit/tool_count_sync.test.js && node tests/unit/stdio_transport.test.js && node tests/unit/stdio_budget_switch.test.js && node tests/unit/logger_structured.test.js && node tests/unit/logger_redaction.test.js && node tests/unit/tool_content_logging.test.js && node tests/unit/adapter_error_logging.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/playwright_version_drift.test.js && node tests/unit/observability_smoke.test.js && node tests/unit/residue_pattern.test.js && node tests/unit/batch_uncategorized_cleanup.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/server_version_probe_outside_op_timeout.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 && node tests/unit/preferences_get.test.js && node tests/unit/budgets_export.test.js && node tests/unit/budgets_import.test.js && node tests/unit/budgets_import_syncid.test.js && node tests/unit/budget_acl_dynamic.test.js && node tests/unit/budget_acl_identity_map.test.js && node tests/unit/budget_acl_userinfo.test.js && node tests/unit/acl_e2e_fixture_contract.test.js && node tests/unit/budget_acl_middleware.test.js && node tests/unit/rules_stage.test.js && node tests/unit/oidc_scope_list.test.js && node tests/unit/get_context.test.js && node tests/unit/contributor_docs.test.js && node tests/unit/toolsets.test.js && node tests/unit/toolsets_entrypoint.test.js && node tests/unit/tools_list_size_budget.test.js && node tests/unit/rules_update_schedules_schema.test.js && node tests/unit/schedules_update_amount_guard.test.js && node tests/unit/source_text_helper.test.js && node tests/unit/transport_onerror.test.js && node tests/unit/adapter_transaction_batch_abort.test.js && node tests/unit/transactions_merge_adapter.test.js && node tests/unit/schedules_dead_conditions.test.js && node tests/unit/session_ownership_express.test.js && node tests/unit/pool_orphan_sweep.test.js && node tests/unit/budget_acl_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",
|