actual-mcp-server 0.15.0 → 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/README.md CHANGED
@@ -774,7 +774,7 @@ The software is provided **as-is**, without warranty of any kind. The author acc
774
774
 
775
775
  ---
776
776
 
777
- **Version:** 0.15.0 | **Tool Count:** 74 (verified LibreChat-compatible)
777
+ **Version:** 0.16.0 | **Tool Count:** 74 (verified LibreChat-compatible)
778
778
 
779
779
  ## Sponsor
780
780
 
@@ -66,6 +66,18 @@ export const tagIdSchema = z
66
66
  .string()
67
67
  .regex(UUID_PATTERN, 'Invalid tag ID format (expected UUID)')
68
68
  .describe('Tag UUID');
69
+ /**
70
+ * Schedule UUID validation
71
+ * Used for: schedule management operations (update, delete)
72
+ *
73
+ * #380: added because `schedules_update` and `schedules_delete` were inlining
74
+ * `UUID_PATTERN` directly, which is the same rule expressed twice and is how a fifth way
75
+ * of describing an id appears.
76
+ */
77
+ export const scheduleIdSchema = z
78
+ .string()
79
+ .regex(UUID_PATTERN, 'Invalid schedule ID format (expected UUID)')
80
+ .describe('Schedule UUID');
69
81
  // ============================================================================
70
82
  // DATE SCHEMAS
71
83
  // ============================================================================
@@ -214,6 +226,7 @@ export const CommonSchemas = {
214
226
  payeeId: payeeIdSchema,
215
227
  ruleId: ruleIdSchema,
216
228
  tagId: tagIdSchema,
229
+ scheduleId: scheduleIdSchema,
217
230
  // Dates
218
231
  date: dateSchema,
219
232
  monthYear: monthYearSchema,
@@ -1,21 +1,19 @@
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
4
5
  .object({
5
- id: z.string().min(1).max(64).describe('Account ID to close'),
6
+ id: CommonSchemas.accountId.describe('Account ID to close'),
6
7
  // #357: documented by the Actual API reference as required when the account's
7
8
  // balance is non-zero, and previously not exposed at all, which made such an
8
9
  // account impossible to close through this server.
9
- transferAccountId: z
10
- .string()
11
- .min(1)
12
- .max(64)
10
+ // #380: the same entity type as `id` at the top of this object, so it gets the same schema. It was
11
+ // left on a bounded string because the drift guard could not SEE it: its detector
12
+ // required `z.` on the field's own line, and this declaration breaks after `z`.
13
+ transferAccountId: CommonSchemas.accountId
13
14
  .optional()
14
15
  .describe('Account ID to move the remaining balance to. REQUIRED when the account has a non-zero balance.'),
15
- transferCategoryId: z
16
- .string()
17
- .min(1)
18
- .max(64)
16
+ transferCategoryId: CommonSchemas.categoryId
19
17
  .optional()
20
18
  .describe('Category to assign the balancing transaction to. Optional, used with transferAccountId.'),
21
19
  })
@@ -6,12 +6,19 @@ export default createTool({
6
6
  name: 'actual_accounts_create',
7
7
  description: 'Create a new account in Actual Budget',
8
8
  schema: z.object({
9
- id: z.string().optional(),
9
+ // #380: the `id` field was REMOVED rather than tightened. Upstream ignores it:
10
+ // `handlers['api/account-create']` destructures only name, offbudget, closed and the
11
+ // balance, so `insertWithUUID` always mints a fresh UUID (verified against the
12
+ // @actual-app/api source shipped in dist/index.js.map). A caller who supplied an id got
13
+ // `{ success: true }` and an account with a DIFFERENT id, then a not-found from
14
+ // actual_accounts_get_balance. That is the same success-lie family as #350, so
15
+ // publishing the field at all was the defect; typing it would only have made an
16
+ // unusable field look deliberate.
10
17
  name: CommonSchemas.name,
11
18
  balance: CommonSchemas.optionalAmountCents
12
19
  }),
13
20
  handler: async (input) => {
14
- const accountPayload = { id: input.id, name: input.name, balance: input.balance };
21
+ const accountPayload = { name: input.name, balance: input.balance };
15
22
  return await adapter.createAccount(accountPayload, input.balance);
16
23
  },
17
24
  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';
3
4
  const InputSchema = z.object({
4
- id: z.string().describe('Account ID to delete'),
5
+ id: CommonSchemas.accountId.describe('Account ID to delete'),
5
6
  });
6
7
  /**
7
8
  * #347: this tool used to return `{success: true}` unconditionally, which was a
@@ -1,8 +1,9 @@
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
  import { notFoundMsg } from '../lib/errors.js';
4
5
  const InputSchema = z.object({
5
- id: z.string().min(1, 'Account ID is required').describe('The UUID of the account'),
6
+ id: CommonSchemas.accountId.describe('The UUID of the account'),
6
7
  cutoff: z.string().optional().describe('Optional cutoff date (YYYY-MM-DD format)')
7
8
  }).strict();
8
9
  const tool = {
@@ -1,10 +1,13 @@
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
- // Bounded, because the id is echoed into the error message and into the structured
5
- // logs. Not tightened to CommonSchemas.accountId (a UUID) here: several fixtures use
6
- // short non-UUID ids, and that sweep is tracked separately alongside #365.
7
- id: z.string().min(1).max(64).describe('Account ID to reopen'),
5
+ // #380: tightened to the shared account schema. This comment previously said the
6
+ // opposite ("not tightened here: several fixtures use short non-UUID ids"), which was
7
+ // true until that sweep happened and would have read as an invitation to revert. The
8
+ // echo-into-logs concern it raised is still real and is now served by the UUID pattern,
9
+ // which is strictly tighter than the length bound it replaced.
10
+ id: CommonSchemas.accountId.describe('Account ID to reopen'),
8
11
  });
9
12
  /**
10
13
  * #358. The guard lives in `adapter.reopenAccount`, which reads, writes and re-reads inside
@@ -1,11 +1,12 @@
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
  import api from '@actual-app/api';
4
5
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
5
6
  const { setBudgetAmount: rawSetBudgetAmount, setBudgetCarryover: rawSetBudgetCarryover } = api;
6
7
  const BudgetOperationSchema = z.object({
7
8
  month: z.string().regex(/^\d{4}-(0[1-9]|1[0-2])$/, 'month must be in YYYY-MM format (e.g. 2025-01)').describe('Budget month in YYYY-MM format'),
8
- categoryId: z.string().describe('Category ID'),
9
+ categoryId: CommonSchemas.categoryId.describe('Category ID'),
9
10
  amount: z.number().optional().describe('Budget amount in cents (if setting amount)'),
10
11
  carryover: z.boolean().optional().describe('Carryover flag (if setting carryover)'),
11
12
  });
@@ -1,12 +1,13 @@
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
  import { isPreflightRefusal } from '../lib/errors.js';
4
5
  const InputSchema = z.object({
5
6
  // #361: this was a bare z.string().min(1), so 'banana' parsed and reached the API. The
6
7
  // regex matches actual_budgets_holdForNextMonth and the other month-taking tools; the
7
8
  // adapter additionally checks the month is one this budget actually has.
8
9
  month: z.string().regex(/^\d{4}-(0[1-9]|1[0-2])$/, 'month must be in YYYY-MM format'),
9
- categoryId: z.string().min(1).max(64),
10
+ categoryId: CommonSchemas.categoryId,
10
11
  amount: z.number().int('amount must be an integer (cents)'),
11
12
  });
12
13
  const tool = {
@@ -1,8 +1,9 @@
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
  month: z.string().describe('Budget month in YYYY-MM format'),
5
- categoryId: z.string().describe('Category ID'),
6
+ categoryId: CommonSchemas.categoryId.describe('Category ID'),
6
7
  flag: z.boolean().describe('Whether to carry over unused budget to next month (true) or not (false)'),
7
8
  });
8
9
  const tool = {
@@ -1,14 +1,13 @@
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
  month: z.string()
5
6
  .regex(/^\d{4}-\d{2}$/, 'month must be YYYY-MM')
6
7
  .describe('Month in YYYY-MM format'),
7
- fromCategoryId: z.string()
8
- .min(1, 'fromCategoryId is required')
8
+ fromCategoryId: CommonSchemas.categoryId
9
9
  .describe('Source category ID to transfer from'),
10
- toCategoryId: z.string()
11
- .min(1, 'toCategoryId is required')
10
+ toCategoryId: CommonSchemas.categoryId
12
11
  .describe('Target category ID to transfer to'),
13
12
  amount: z.number()
14
13
  .int('amount must be an integer (cents)')
@@ -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';
3
4
  const InputSchema = z.object({
4
- id: z.string().describe('Category ID to delete'),
5
+ id: CommonSchemas.categoryId.describe('Category ID to delete'),
5
6
  });
6
7
  const tool = {
7
8
  name: 'actual_categories_delete',
@@ -1,10 +1,11 @@
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
- id: z.string().describe('Category ID to update'),
5
+ id: CommonSchemas.categoryId.describe('Category ID to update'),
5
6
  fields: z.object({
6
7
  name: z.string().optional().describe('New category name'),
7
- group_id: z.string().optional().describe('Category group ID to move this category to'),
8
+ group_id: CommonSchemas.categoryGroupId.optional().describe('Category group ID to move this category to'),
8
9
  is_income: z.boolean().optional().describe('Whether this is an income category'),
9
10
  hidden: z.boolean().optional().describe('Whether this category is hidden'),
10
11
  }).describe('Fields to update'),
@@ -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';
3
4
  const InputSchema = z.object({
4
- id: z.string().describe('Category group ID to delete'),
5
+ id: CommonSchemas.categoryGroupId.describe('Category group ID to delete'),
5
6
  });
6
7
  /**
7
8
  * #376: the existence guard lives in `adapter.deleteCategoryGroup`, which reads and writes
@@ -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';
3
4
  const InputSchema = z.object({
4
- id: z.string().describe('Category group ID to update'),
5
+ id: CommonSchemas.categoryGroupId.describe('Category group ID to update'),
5
6
  fields: z.object({
6
7
  name: z.string().optional().describe('New group name'),
7
8
  is_income: z.boolean().optional().describe('Whether this is an income category group'),
@@ -1,8 +1,9 @@
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
  import { notFoundMsg } from '../lib/errors.js';
4
5
  const InputSchema = z.object({
5
- payeeId: z.string().describe('ID of the payee to get rules for'),
6
+ payeeId: CommonSchemas.payeeId.describe('ID of the payee to get rules for'),
6
7
  });
7
8
  const tool = {
8
9
  name: 'actual_payee_rules_get',
@@ -1,9 +1,12 @@
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
- // Bounded for the same reason as the payee ids in #356: this value is echoed into the
5
- // not-found message, into the schedule-owned refusal, and into logger.error.
6
- id: z.string().min(1).max(64).describe('Rule ID to delete'),
5
+ // #380: the shared rule schema. The reason from #356 still holds and is why it is not
6
+ // simply a bare string: this value is echoed into the not-found message, into the
7
+ // schedule-owned refusal, and into logger.error, so it must be bounded. The UUID pattern
8
+ // bounds it more tightly than the .max(64) it replaced.
9
+ id: CommonSchemas.ruleId.describe('Rule ID to delete'),
7
10
  });
8
11
  /**
9
12
  * #355 fixed two defects here (a silent no-op on an unknown id, and success reported for a
@@ -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
  // Define the schema for rule conditions and actions (same as create)
4
5
  const ConditionSchema = z.object({
5
6
  field: z.string().describe('Field to match (e.g., "payee", "notes", "amount", "category")'),
@@ -26,7 +27,7 @@ const FIELD_OPERATORS = {
26
27
  'date': { type: 'date', operators: ['is', 'gte', 'lte', 'gt', 'lt'] },
27
28
  };
28
29
  const InputSchema = z.object({
29
- id: z.string().describe('Rule ID to update'),
30
+ id: CommonSchemas.ruleId.describe('Rule ID to update'),
30
31
  fields: z.object({
31
32
  // #342: null is Actual's DEFAULT stage; the literal "default" is rejected by
32
33
  // its validator. Deliberately NO .default() here: this is a partial update, so
@@ -1,13 +1,13 @@
1
1
  import { z } from 'zod';
2
2
  import adapter from '../lib/actual-adapter.js';
3
- import { UUID_PATTERN } from '../lib/constants.js';
3
+ import { CommonSchemas } from '../lib/schemas/common.js';
4
4
  import { RecurConfigSchema } from '../lib/schemas/recur.js';
5
5
  const InputSchema = z.object({
6
6
  name: z.string().optional()
7
7
  .describe('Unique display name for the schedule'),
8
- payee: z.string().regex(UUID_PATTERN, 'Invalid UUID format').optional()
8
+ payee: CommonSchemas.payeeId.optional()
9
9
  .describe('Payee UUID (from actual_payees_get)'),
10
- account: z.string().regex(UUID_PATTERN, 'Invalid UUID format').optional()
10
+ account: CommonSchemas.accountId.optional()
11
11
  .describe('Account UUID (from actual_accounts_list). If omitted the schedule is not tied to an account'),
12
12
  amount: z.number().int().optional()
13
13
  .describe('Amount in cents. Negative = expense (e.g. -5000 = -$50.00), positive = income'),
@@ -1,9 +1,8 @@
1
1
  import { z } from 'zod';
2
2
  import adapter from '../lib/actual-adapter.js';
3
- import { UUID_PATTERN } from '../lib/constants.js';
3
+ import { CommonSchemas } from '../lib/schemas/common.js';
4
4
  const InputSchema = z.object({
5
- id: z.string().regex(UUID_PATTERN, 'Invalid UUID format')
6
- .describe('UUID of the schedule to delete (from actual_schedules_get)'),
5
+ id: CommonSchemas.scheduleId.describe('UUID of the schedule to delete (from actual_schedules_get)'),
7
6
  });
8
7
  /**
9
8
  * #376: the existence guard and the SQLite constraint-error translation live in
@@ -1,15 +1,14 @@
1
1
  import { z } from 'zod';
2
2
  import adapter from '../lib/actual-adapter.js';
3
- import { UUID_PATTERN } from '../lib/constants.js';
3
+ import { CommonSchemas } from '../lib/schemas/common.js';
4
4
  import { RecurConfigSchema } from '../lib/schemas/recur.js';
5
5
  const InputSchema = z.object({
6
- id: z.string().regex(UUID_PATTERN, 'Invalid UUID format')
7
- .describe('UUID of the schedule to update (from actual_schedules_get)'),
6
+ id: CommonSchemas.scheduleId.describe('UUID of the schedule to update (from actual_schedules_get)'),
8
7
  name: z.string().optional()
9
8
  .describe('New display name for the schedule'),
10
- payee: z.string().regex(UUID_PATTERN, 'Invalid UUID format').nullable().optional()
9
+ payee: CommonSchemas.payeeId.nullable().optional()
11
10
  .describe('New payee UUID, or null to clear'),
12
- account: z.string().regex(UUID_PATTERN, 'Invalid UUID format').nullable().optional()
11
+ account: CommonSchemas.accountId.nullable().optional()
13
12
  .describe('New account UUID, or null to clear'),
14
13
  amount: z.number().int().optional()
15
14
  .describe('New amount in cents. Negative = expense, positive = income'),
@@ -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';
3
4
  const InputSchema = z.object({
4
- id: z.string().describe('Transaction ID to delete'),
5
+ id: CommonSchemas.transactionId.describe('Transaction ID to delete'),
5
6
  });
6
7
  const tool = {
7
8
  name: 'actual_transactions_delete',
@@ -2,7 +2,15 @@ import { z } from 'zod';
2
2
  import adapter from '../lib/actual-adapter.js';
3
3
  import { CommonSchemas } from '../lib/schemas/common.js';
4
4
  const InputSchema = z.object({
5
- id: z.string().optional().describe('Transaction ID to update (optional for smoke tests, required for actual usage)'),
5
+ // #380: REQUIRED, and typed. It was `.optional()` with the describe "optional for smoke
6
+ // tests, required for actual usage": a published contract weakened to suit a test. The
7
+ // handler then returned `{ success: true }` for a call with no id, writing nothing, which
8
+ // is the #350 failure this project has spent three releases removing. A model that omitted
9
+ // the id was told its edit had succeeded.
10
+ //
11
+ // The smoke test does supply an id (generated_tools.smoke.test.js), so the escape hatch
12
+ // was not even load bearing when it was removed.
13
+ id: CommonSchemas.transactionId.describe('Transaction ID to update'),
6
14
  fields: z.object({
7
15
  account: z.string().nullable().optional().describe('Account ID'),
8
16
  date: z.string().nullable().optional().describe('Transaction date (YYYY-MM-DD)'),
@@ -32,10 +40,6 @@ const tool = {
32
40
  inputSchema: InputSchema,
33
41
  call: async (args, _meta) => {
34
42
  const input = InputSchema.parse(args || {});
35
- // For smoke tests, return early if no id provided
36
- if (!input.id) {
37
- return { success: true };
38
- }
39
43
  await adapter.updateTransaction(input.id, input.fields);
40
44
  return { success: true };
41
45
  },
@@ -13,6 +13,7 @@
13
13
  */
14
14
  import { z } from 'zod';
15
15
  import adapter from '../lib/actual-adapter.js';
16
+ import { CommonSchemas } from '../lib/schemas/common.js';
16
17
  const FieldsSchema = z.object({
17
18
  account: z.string().nullable().optional().describe('Account ID'),
18
19
  date: z.string().nullable().optional().describe('Transaction date (YYYY-MM-DD)'),
@@ -25,7 +26,7 @@ const FieldsSchema = z.object({
25
26
  cleared: z.boolean().nullable().optional().describe('Whether transaction is cleared'),
26
27
  });
27
28
  const UpdateItemSchema = z.object({
28
- id: z.string().describe('Transaction ID to update'),
29
+ id: CommonSchemas.transactionId.describe('Transaction ID to update'),
29
30
  fields: FieldsSchema.describe('Fields to update for this transaction'),
30
31
  });
31
32
  const InputSchema = z.object({
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.15.0",
4
+ "version": "0.16.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/e2e_spec_collection.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/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/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/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/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/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/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/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 && 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",
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/e2e_spec_collection.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/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/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/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/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/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/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/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 && 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",
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",