ynab-mcp-deluxe 0.1.9

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.
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Sync history persistence utilities.
3
+ *
4
+ * Every sync response is saved to disk, creating an automatic incremental backup trail.
5
+ * This replaces the auto-backup functionality with continuous, granular backups.
6
+ *
7
+ * Directory structure:
8
+ * ~/.config/ynab-mcp-deluxe/sync-history/[budgetId]/
9
+ * - 20260125T143022Z-full.json # Initial sync
10
+ * - 20260125T153022Z-delta.json # Delta sync
11
+ * - ...
12
+ */
13
+ import type { SyncType } from './types.js';
14
+ import type { BudgetDetail } from 'ynab';
15
+ /**
16
+ * Logger interface matching FastMCP's context log
17
+ */
18
+ interface ContextLog {
19
+ debug: (message: string, data?: unknown) => void;
20
+ error: (message: string, data?: unknown) => void;
21
+ info: (message: string, data?: unknown) => void;
22
+ warn: (message: string, data?: unknown) => void;
23
+ }
24
+ /**
25
+ * Get the base sync history directory path.
26
+ * ~/.config/ynab-mcp-deluxe/sync-history/
27
+ */
28
+ export declare function getSyncHistoryBaseDir(): string;
29
+ /**
30
+ * Validate that a budgetId is safe to use in file paths.
31
+ * YNAB budget IDs are UUIDs (alphanumeric with hyphens).
32
+ * This prevents path traversal attacks with malicious budgetId values.
33
+ *
34
+ * @param budgetId - The budget ID to validate
35
+ * @returns true if the budgetId is safe for use in paths
36
+ */
37
+ export declare function isValidBudgetIdForPath(budgetId: string): boolean;
38
+ /**
39
+ * Get the sync history directory for a specific budget.
40
+ * ~/.config/ynab-mcp-deluxe/sync-history/[budgetId]/
41
+ *
42
+ * @param budgetId - The budget ID (must be a valid UUID format)
43
+ * @throws Error if budgetId contains invalid characters (path traversal protection)
44
+ */
45
+ export declare function getSyncHistoryDir(budgetId: string): string;
46
+ /**
47
+ * Ensure the sync history directory exists for a budget.
48
+ * Creates the directory tree if it doesn't exist.
49
+ */
50
+ export declare function ensureSyncHistoryDir(budgetId: string): Promise<void>;
51
+ /**
52
+ * Generate sync history filename with timestamp and sync type.
53
+ * Format: YYYYMMDDTHHMMSSZ-[full|delta].json
54
+ *
55
+ * Uses compact ISO 8601 format for easy sorting and readability.
56
+ * Example: 20260125T143022Z-full.json
57
+ */
58
+ export declare function generateSyncFilename(syncType: SyncType): string;
59
+ /**
60
+ * Persist a sync response to disk.
61
+ *
62
+ * This function is designed to be fail-safe: if the write fails,
63
+ * it logs a warning but does not throw. The sync operation should
64
+ * continue even if persistence fails.
65
+ *
66
+ * @param budgetId - The budget ID
67
+ * @param syncType - 'full' or 'delta'
68
+ * @param budget - The budget data from YNAB API
69
+ * @param serverKnowledge - The server_knowledge from this sync
70
+ * @param previousServerKnowledge - For delta syncs, the previous server_knowledge
71
+ * @param log - Logger for debug/error output
72
+ * @returns The file path if successful, null if failed
73
+ */
74
+ export declare function persistSyncResponse(budgetId: string, syncType: SyncType, budget: BudgetDetail, serverKnowledge: number, previousServerKnowledge: number | null, log: ContextLog): Promise<string | null>;
75
+ /**
76
+ * Result of clearing sync history
77
+ */
78
+ export interface ClearSyncHistoryResult {
79
+ /** Budget IDs that were cleared (or 'all' if no specific budget) */
80
+ budgetsCleared: string[];
81
+ /** Any errors encountered (non-fatal) */
82
+ errors: string[];
83
+ /** Number of files deleted */
84
+ filesDeleted: number;
85
+ }
86
+ /**
87
+ * Clear sync history for a specific budget or all budgets.
88
+ *
89
+ * @param budgetId - Optional budget ID to clear. If not provided, clears all budgets.
90
+ * @param log - Logger for debug/error output
91
+ * @returns Summary of the clear operation
92
+ */
93
+ export declare function clearSyncHistory(budgetId: string | null, log: ContextLog): Promise<ClearSyncHistoryResult>;
94
+ export {};
95
+ //# sourceMappingURL=sync-history.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sync-history.d.ts","sourceRoot":"","sources":["../src/sync-history.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAmB,QAAQ,EAAC,MAAM,YAAY,CAAC;AAC3D,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,MAAM,CAAC;AAMvC;;GAEG;AACH,UAAU,UAAU;IAClB,KAAK,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC;IACjD,KAAK,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC;IACjD,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC;IAChD,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC;CACjD;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,IAAI,MAAM,CAE9C;AAED;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAKhE;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAO1D;AAED;;;GAGG;AACH,wBAAsB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAG1E;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,QAAQ,GAAG,MAAM,CAS/D;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,mBAAmB,CACvC,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,QAAQ,EAClB,MAAM,EAAE,YAAY,EACpB,eAAe,EAAE,MAAM,EACvB,uBAAuB,EAAE,MAAM,GAAG,IAAI,EACtC,GAAG,EAAE,UAAU,GACd,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CA4CxB;AAED;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACrC,oEAAoE;IACpE,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,yCAAyC;IACzC,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,8BAA8B;IAC9B,YAAY,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;GAMG;AACH,wBAAsB,gBAAgB,CACpC,QAAQ,EAAE,MAAM,GAAG,IAAI,EACvB,GAAG,EAAE,UAAU,GACd,OAAO,CAAC,sBAAsB,CAAC,CAmEjC"}
@@ -0,0 +1,205 @@
1
+ /**
2
+ * Sync history persistence utilities.
3
+ *
4
+ * Every sync response is saved to disk, creating an automatic incremental backup trail.
5
+ * This replaces the auto-backup functionality with continuous, granular backups.
6
+ *
7
+ * Directory structure:
8
+ * ~/.config/ynab-mcp-deluxe/sync-history/[budgetId]/
9
+ * - 20260125T143022Z-full.json # Initial sync
10
+ * - 20260125T153022Z-delta.json # Delta sync
11
+ * - ...
12
+ */
13
+ import { mkdir, readdir, rm, writeFile } from 'node:fs/promises';
14
+ import { homedir } from 'node:os';
15
+ import { join } from 'node:path';
16
+ /**
17
+ * Get the base sync history directory path.
18
+ * ~/.config/ynab-mcp-deluxe/sync-history/
19
+ */
20
+ export function getSyncHistoryBaseDir() {
21
+ return join(homedir(), '.config', 'ynab-mcp-deluxe', 'sync-history');
22
+ }
23
+ /**
24
+ * Validate that a budgetId is safe to use in file paths.
25
+ * YNAB budget IDs are UUIDs (alphanumeric with hyphens).
26
+ * This prevents path traversal attacks with malicious budgetId values.
27
+ *
28
+ * @param budgetId - The budget ID to validate
29
+ * @returns true if the budgetId is safe for use in paths
30
+ */
31
+ export function isValidBudgetIdForPath(budgetId) {
32
+ // YNAB budget IDs are UUIDs: alphanumeric characters and hyphens only
33
+ // Example: "12345678-1234-1234-1234-123456789abc"
34
+ // Reject empty strings, path separators, dots, and other special characters
35
+ return /^[a-zA-Z0-9-]+$/.test(budgetId) && budgetId.length > 0;
36
+ }
37
+ /**
38
+ * Get the sync history directory for a specific budget.
39
+ * ~/.config/ynab-mcp-deluxe/sync-history/[budgetId]/
40
+ *
41
+ * @param budgetId - The budget ID (must be a valid UUID format)
42
+ * @throws Error if budgetId contains invalid characters (path traversal protection)
43
+ */
44
+ export function getSyncHistoryDir(budgetId) {
45
+ if (!isValidBudgetIdForPath(budgetId)) {
46
+ throw new Error(`Invalid budgetId for file path: "${budgetId}". Budget IDs must contain only alphanumeric characters and hyphens.`);
47
+ }
48
+ return join(getSyncHistoryBaseDir(), budgetId);
49
+ }
50
+ /**
51
+ * Ensure the sync history directory exists for a budget.
52
+ * Creates the directory tree if it doesn't exist.
53
+ */
54
+ export async function ensureSyncHistoryDir(budgetId) {
55
+ const dir = getSyncHistoryDir(budgetId);
56
+ await mkdir(dir, { recursive: true });
57
+ }
58
+ /**
59
+ * Generate sync history filename with timestamp and sync type.
60
+ * Format: YYYYMMDDTHHMMSSZ-[full|delta].json
61
+ *
62
+ * Uses compact ISO 8601 format for easy sorting and readability.
63
+ * Example: 20260125T143022Z-full.json
64
+ */
65
+ export function generateSyncFilename(syncType) {
66
+ const now = new Date();
67
+ // Format: 20260125T143022Z (compact ISO 8601 UTC)
68
+ const timestamp = now
69
+ .toISOString()
70
+ .replace(/[-:]/g, '')
71
+ .replace(/\.\d+Z$/, 'Z');
72
+ return `${timestamp}-${syncType}.json`;
73
+ }
74
+ /**
75
+ * Persist a sync response to disk.
76
+ *
77
+ * This function is designed to be fail-safe: if the write fails,
78
+ * it logs a warning but does not throw. The sync operation should
79
+ * continue even if persistence fails.
80
+ *
81
+ * @param budgetId - The budget ID
82
+ * @param syncType - 'full' or 'delta'
83
+ * @param budget - The budget data from YNAB API
84
+ * @param serverKnowledge - The server_knowledge from this sync
85
+ * @param previousServerKnowledge - For delta syncs, the previous server_knowledge
86
+ * @param log - Logger for debug/error output
87
+ * @returns The file path if successful, null if failed
88
+ */
89
+ export async function persistSyncResponse(budgetId, syncType, budget, serverKnowledge, previousServerKnowledge, log) {
90
+ const startTime = performance.now();
91
+ try {
92
+ // Ensure directory exists
93
+ await ensureSyncHistoryDir(budgetId);
94
+ // Build the sync history entry
95
+ const entry = {
96
+ budget,
97
+ previousServerKnowledge,
98
+ serverKnowledge,
99
+ syncType,
100
+ syncedAt: new Date().toISOString(),
101
+ };
102
+ // Generate filename and path
103
+ const filename = generateSyncFilename(syncType);
104
+ const filePath = join(getSyncHistoryDir(budgetId), filename);
105
+ // Write to disk
106
+ await writeFile(filePath, JSON.stringify(entry, null, 2), 'utf-8');
107
+ const durationMs = Math.round(performance.now() - startTime);
108
+ log.debug('Sync history persisted', {
109
+ budgetId,
110
+ durationMs,
111
+ filePath,
112
+ serverKnowledge,
113
+ syncType,
114
+ });
115
+ return filePath;
116
+ }
117
+ catch (error) {
118
+ // Log warning but don't fail - persistence is a safety feature, not critical path
119
+ const durationMs = Math.round(performance.now() - startTime);
120
+ log.warn('Failed to persist sync history (continuing with sync)', {
121
+ budgetId,
122
+ durationMs,
123
+ error: error instanceof Error ? error.message : String(error),
124
+ syncType,
125
+ });
126
+ return null;
127
+ }
128
+ }
129
+ /**
130
+ * Clear sync history for a specific budget or all budgets.
131
+ *
132
+ * @param budgetId - Optional budget ID to clear. If not provided, clears all budgets.
133
+ * @param log - Logger for debug/error output
134
+ * @returns Summary of the clear operation
135
+ */
136
+ export async function clearSyncHistory(budgetId, log) {
137
+ const result = {
138
+ budgetsCleared: [],
139
+ errors: [],
140
+ filesDeleted: 0,
141
+ };
142
+ const baseDir = getSyncHistoryBaseDir();
143
+ try {
144
+ if (budgetId !== null) {
145
+ // Clear specific budget
146
+ const budgetDir = getSyncHistoryDir(budgetId);
147
+ try {
148
+ const files = await readdir(budgetDir);
149
+ result.filesDeleted = files.length;
150
+ await rm(budgetDir, { force: true, recursive: true });
151
+ result.budgetsCleared.push(budgetId);
152
+ log.info('Cleared sync history for budget', {
153
+ budgetId,
154
+ filesDeleted: result.filesDeleted,
155
+ });
156
+ }
157
+ catch (error) {
158
+ if (error.code === 'ENOENT') {
159
+ // Directory doesn't exist - nothing to clear
160
+ log.info('No sync history to clear for budget', { budgetId });
161
+ }
162
+ else {
163
+ throw error;
164
+ }
165
+ }
166
+ }
167
+ else {
168
+ // Clear all budgets
169
+ try {
170
+ const budgetDirs = await readdir(baseDir);
171
+ for (const dir of budgetDirs) {
172
+ const budgetDir = getSyncHistoryDir(dir);
173
+ try {
174
+ const files = await readdir(budgetDir);
175
+ result.filesDeleted += files.length;
176
+ await rm(budgetDir, { force: true, recursive: true });
177
+ result.budgetsCleared.push(dir);
178
+ }
179
+ catch (dirError) {
180
+ result.errors.push(`Failed to clear ${dir}: ${dirError instanceof Error ? dirError.message : String(dirError)}`);
181
+ }
182
+ }
183
+ log.info('Cleared all sync history', {
184
+ budgetsCleared: result.budgetsCleared.length,
185
+ filesDeleted: result.filesDeleted,
186
+ });
187
+ }
188
+ catch (error) {
189
+ if (error.code === 'ENOENT') {
190
+ // Base directory doesn't exist - nothing to clear
191
+ log.info('No sync history directory exists');
192
+ }
193
+ else {
194
+ throw error;
195
+ }
196
+ }
197
+ }
198
+ }
199
+ catch (error) {
200
+ const errorMessage = error instanceof Error ? error.message : String(error);
201
+ result.errors.push(errorMessage);
202
+ log.error('Failed to clear sync history', { budgetId, error: errorMessage });
203
+ }
204
+ return result;
205
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Sync providers for fetching budget data.
3
+ *
4
+ * SyncProvider is an abstraction that allows us to swap between:
5
+ * - ApiSyncProvider: Uses the YNAB API (production)
6
+ * - StaticSyncProvider: Loads from a JSON file (testing)
7
+ *
8
+ * This enables fast iteration and deterministic E2E tests.
9
+ */
10
+ import type { SyncProvider } from './types.js';
11
+ import type { BudgetDetail } from 'ynab';
12
+ /**
13
+ * API-based sync provider that fetches from the YNAB API.
14
+ * This is the default provider for production use.
15
+ */
16
+ export declare class ApiSyncProvider implements SyncProvider {
17
+ private api;
18
+ constructor(accessToken: string);
19
+ /**
20
+ * Perform a full sync - fetches the complete budget.
21
+ */
22
+ fullSync(budgetId: string): Promise<{
23
+ budget: BudgetDetail;
24
+ serverKnowledge: number;
25
+ }>;
26
+ /**
27
+ * Perform a delta sync - fetches only changes since lastKnowledge.
28
+ */
29
+ deltaSync(budgetId: string, lastKnowledge: number): Promise<{
30
+ budget: BudgetDetail;
31
+ serverKnowledge: number;
32
+ }>;
33
+ }
34
+ /**
35
+ * Static sync provider that loads from a JSON file.
36
+ * Used for testing to avoid hitting the YNAB API.
37
+ *
38
+ * Currently a stub - full implementation will be added later.
39
+ * See ROADMAP.md for "Static JSON Testing Mode" future enhancement.
40
+ */
41
+ export declare class StaticSyncProvider implements SyncProvider {
42
+ private budgetData;
43
+ private serverKnowledge;
44
+ constructor(budgetData: BudgetDetail, serverKnowledge?: number);
45
+ /**
46
+ * Full sync returns the static budget data.
47
+ */
48
+ fullSync(_budgetId: string): Promise<{
49
+ budget: BudgetDetail;
50
+ serverKnowledge: number;
51
+ }>;
52
+ /**
53
+ * Delta sync returns empty changes (no changes since static data is immutable).
54
+ * In the future, this could return changes from a mutations overlay.
55
+ */
56
+ deltaSync(_budgetId: string, _lastKnowledge: number): Promise<{
57
+ budget: BudgetDetail;
58
+ serverKnowledge: number;
59
+ }>;
60
+ }
61
+ /**
62
+ * Create the appropriate sync provider based on environment.
63
+ *
64
+ * If YNAB_STATIC_BUDGET_FILE is set, loads from that file.
65
+ * Otherwise, uses the YNAB API with YNAB_ACCESS_TOKEN.
66
+ *
67
+ * @returns The configured SyncProvider
68
+ * @throws Error if required environment variables are missing
69
+ */
70
+ export declare function createSyncProvider(): SyncProvider;
71
+ //# sourceMappingURL=sync-providers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sync-providers.d.ts","sourceRoot":"","sources":["../src/sync-providers.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,YAAY,CAAC;AAC7C,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,MAAM,CAAC;AAIvC;;;GAGG;AACH,qBAAa,eAAgB,YAAW,YAAY;IAClD,OAAO,CAAC,GAAG,CAAU;gBAET,WAAW,EAAE,MAAM;IAI/B;;OAEG;IACG,QAAQ,CACZ,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC;QAAC,MAAM,EAAE,YAAY,CAAC;QAAC,eAAe,EAAE,MAAM,CAAA;KAAC,CAAC;IAQ3D;;OAEG;IACG,SAAS,CACb,QAAQ,EAAE,MAAM,EAChB,aAAa,EAAE,MAAM,GACpB,OAAO,CAAC;QAAC,MAAM,EAAE,YAAY,CAAC;QAAC,eAAe,EAAE,MAAM,CAAA;KAAC,CAAC;CAU5D;AAED;;;;;;GAMG;AACH,qBAAa,kBAAmB,YAAW,YAAY;IACrD,OAAO,CAAC,UAAU,CAAe;IACjC,OAAO,CAAC,eAAe,CAAS;gBAEpB,UAAU,EAAE,YAAY,EAAE,eAAe,SAAI;IAKzD;;OAEG;IACH,QAAQ,CACN,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC;QAAC,MAAM,EAAE,YAAY,CAAC;QAAC,eAAe,EAAE,MAAM,CAAA;KAAC,CAAC;IAO3D;;;OAGG;IACH,SAAS,CACP,SAAS,EAAE,MAAM,EACjB,cAAc,EAAE,MAAM,GACrB,OAAO,CAAC;QAAC,MAAM,EAAE,YAAY,CAAC;QAAC,eAAe,EAAE,MAAM,CAAA;KAAC,CAAC;CAc5D;AAED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,IAAI,YAAY,CAqBjD"}
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Sync providers for fetching budget data.
3
+ *
4
+ * SyncProvider is an abstraction that allows us to swap between:
5
+ * - ApiSyncProvider: Uses the YNAB API (production)
6
+ * - StaticSyncProvider: Loads from a JSON file (testing)
7
+ *
8
+ * This enables fast iteration and deterministic E2E tests.
9
+ */
10
+ import { api as YnabApi } from 'ynab';
11
+ /**
12
+ * API-based sync provider that fetches from the YNAB API.
13
+ * This is the default provider for production use.
14
+ */
15
+ export class ApiSyncProvider {
16
+ api;
17
+ constructor(accessToken) {
18
+ this.api = new YnabApi(accessToken);
19
+ }
20
+ /**
21
+ * Perform a full sync - fetches the complete budget.
22
+ */
23
+ async fullSync(budgetId) {
24
+ const response = await this.api.budgets.getBudgetById(budgetId);
25
+ return {
26
+ budget: response.data.budget,
27
+ serverKnowledge: response.data.server_knowledge,
28
+ };
29
+ }
30
+ /**
31
+ * Perform a delta sync - fetches only changes since lastKnowledge.
32
+ */
33
+ async deltaSync(budgetId, lastKnowledge) {
34
+ const response = await this.api.budgets.getBudgetById(budgetId, lastKnowledge);
35
+ return {
36
+ budget: response.data.budget,
37
+ serverKnowledge: response.data.server_knowledge,
38
+ };
39
+ }
40
+ }
41
+ /**
42
+ * Static sync provider that loads from a JSON file.
43
+ * Used for testing to avoid hitting the YNAB API.
44
+ *
45
+ * Currently a stub - full implementation will be added later.
46
+ * See ROADMAP.md for "Static JSON Testing Mode" future enhancement.
47
+ */
48
+ export class StaticSyncProvider {
49
+ budgetData;
50
+ serverKnowledge;
51
+ constructor(budgetData, serverKnowledge = 1) {
52
+ this.budgetData = budgetData;
53
+ this.serverKnowledge = serverKnowledge;
54
+ }
55
+ /**
56
+ * Full sync returns the static budget data.
57
+ */
58
+ fullSync(_budgetId) {
59
+ return Promise.resolve({
60
+ budget: this.budgetData,
61
+ serverKnowledge: this.serverKnowledge,
62
+ });
63
+ }
64
+ /**
65
+ * Delta sync returns empty changes (no changes since static data is immutable).
66
+ * In the future, this could return changes from a mutations overlay.
67
+ */
68
+ deltaSync(_budgetId, _lastKnowledge) {
69
+ // Return an "empty" delta - same server knowledge, no changes
70
+ // The budget object will have empty/undefined arrays, indicating no changes
71
+ const emptyDelta = {
72
+ id: this.budgetData.id,
73
+ name: this.budgetData.name,
74
+ // All other fields are optional and undefined = no changes
75
+ };
76
+ return Promise.resolve({
77
+ budget: emptyDelta,
78
+ serverKnowledge: this.serverKnowledge,
79
+ });
80
+ }
81
+ }
82
+ /**
83
+ * Create the appropriate sync provider based on environment.
84
+ *
85
+ * If YNAB_STATIC_BUDGET_FILE is set, loads from that file.
86
+ * Otherwise, uses the YNAB API with YNAB_ACCESS_TOKEN.
87
+ *
88
+ * @returns The configured SyncProvider
89
+ * @throws Error if required environment variables are missing
90
+ */
91
+ export function createSyncProvider() {
92
+ const staticBudgetFile = process.env['YNAB_STATIC_BUDGET_FILE'];
93
+ if (staticBudgetFile !== undefined && staticBudgetFile !== '') {
94
+ // Static mode - load from JSON file
95
+ // Note: Full implementation will be added in future enhancement
96
+ throw new Error(`Static budget file support not yet implemented. ` +
97
+ `Set YNAB_STATIC_BUDGET_FILE is set to: ${staticBudgetFile}`);
98
+ }
99
+ // API mode - use YNAB API
100
+ const accessToken = process.env['YNAB_ACCESS_TOKEN'];
101
+ if (accessToken === undefined || accessToken === '') {
102
+ throw new Error('YNAB authentication failed. Check that YNAB_ACCESS_TOKEN environment variable is set.');
103
+ }
104
+ return new ApiSyncProvider(accessToken);
105
+ }