@fuzzle/opencode-accountant 0.3.0-next.1 → 0.3.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 +3 -24
- package/agent/accountant.md +7 -28
- package/dist/index.js +30 -315
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -201,10 +201,9 @@ The `import-pipeline` tool provides an atomic, safe import workflow using git wo
|
|
|
201
201
|
- Creates an isolated git worktree
|
|
202
202
|
- Syncs CSV files from main repo to worktree
|
|
203
203
|
- Classifies CSV files by provider/currency
|
|
204
|
-
- Extracts accounts from rules and creates declarations in year journal
|
|
205
204
|
- Validates all transactions have matching rules
|
|
206
205
|
- Imports transactions to the appropriate year journal
|
|
207
|
-
- Reconciles closing balance (
|
|
206
|
+
- Reconciles closing balance (if available in CSV metadata)
|
|
208
207
|
- Merges changes back to main branch with `--no-ff`
|
|
209
208
|
- Deletes processed CSV files from main repo's import/incoming
|
|
210
209
|
- Cleans up the worktree
|
|
@@ -238,30 +237,13 @@ The tool will use that rules file when processing `transactions.csv`.
|
|
|
238
237
|
|
|
239
238
|
See the hledger documentation for details on rules file format and syntax.
|
|
240
239
|
|
|
241
|
-
#### Account Declarations
|
|
242
|
-
|
|
243
|
-
The pipeline automatically manages account declarations:
|
|
244
|
-
|
|
245
|
-
- Scans rules files matched to the CSVs being imported
|
|
246
|
-
- Extracts all accounts (account1 and account2 directives)
|
|
247
|
-
- Creates or updates year journal files with sorted account declarations
|
|
248
|
-
- Ensures `hledger check --strict` validation passes
|
|
249
|
-
|
|
250
|
-
**No manual account setup required.** Account declarations are created proactively before import attempts.
|
|
251
|
-
|
|
252
240
|
#### Unknown Postings
|
|
253
241
|
|
|
254
242
|
When a transaction doesn't match any `if` pattern in the rules file, hledger assigns it to `income:unknown` or `expenses:unknown` depending on the transaction direction. The pipeline will fail at the validation step, reporting the unknown postings so you can add appropriate rules before retrying.
|
|
255
243
|
|
|
256
244
|
#### Closing Balance Reconciliation
|
|
257
245
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
1. **CSV Metadata Extraction**: For providers with closing balance in metadata (e.g., UBS)
|
|
261
|
-
2. **CSV Data Analysis**: Extracts balance from the last transaction row if metadata unavailable
|
|
262
|
-
3. **Manual Override**: Use the `closingBalance` parameter when automatic detection fails
|
|
263
|
-
|
|
264
|
-
Configure metadata extraction in `providers.yaml`:
|
|
246
|
+
For providers that include closing balance in CSV metadata (e.g., UBS), the tool automatically validates that the imported transactions result in the correct balance. Configure metadata extraction in `providers.yaml`:
|
|
265
247
|
|
|
266
248
|
```yaml
|
|
267
249
|
metadata:
|
|
@@ -276,10 +258,7 @@ metadata:
|
|
|
276
258
|
column: 1
|
|
277
259
|
```
|
|
278
260
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
- CSV has no balance information in metadata or data
|
|
282
|
-
- The auto-detected balance has low confidence
|
|
261
|
+
For providers without closing balance in metadata (e.g., Revolut), provide it manually via the `closingBalance` argument.
|
|
283
262
|
|
|
284
263
|
## Development
|
|
285
264
|
|
package/agent/accountant.md
CHANGED
|
@@ -89,10 +89,9 @@ The `import-pipeline` tool provides an **atomic, safe workflow** using git workt
|
|
|
89
89
|
3. **Automatic Processing**: The tool creates an isolated git worktree and:
|
|
90
90
|
- Syncs CSV files from main repo to worktree
|
|
91
91
|
- Classifies CSV files by provider/currency
|
|
92
|
-
- Extracts required accounts from rules files and updates year journal
|
|
93
92
|
- Validates all transactions have matching rules
|
|
94
93
|
- Imports transactions to the appropriate year journal
|
|
95
|
-
- Reconciles closing balance (
|
|
94
|
+
- Reconciles closing balance (if available in CSV metadata)
|
|
96
95
|
- Merges changes back to main branch with `--no-ff`
|
|
97
96
|
- Deletes processed CSV files from main repo's import/incoming
|
|
98
97
|
- Cleans up the worktree
|
|
@@ -109,25 +108,6 @@ The `import-pipeline` tool provides an **atomic, safe workflow** using git workt
|
|
|
109
108
|
- Use field names from the `fields` directive for matching
|
|
110
109
|
- Unknown account pattern: `income:unknown` (positive amounts) / `expenses:unknown` (negative amounts)
|
|
111
110
|
|
|
112
|
-
### Automatic Account Declarations
|
|
113
|
-
|
|
114
|
-
The import pipeline automatically:
|
|
115
|
-
|
|
116
|
-
- Scans matched rules files for all account references (account1, account2 directives)
|
|
117
|
-
- Creates/updates year journal files (e.g., ledger/2026.journal) with sorted account declarations
|
|
118
|
-
- Prevents `hledger check --strict` failures due to missing account declarations
|
|
119
|
-
- No manual account setup required
|
|
120
|
-
|
|
121
|
-
### Automatic Balance Detection
|
|
122
|
-
|
|
123
|
-
The reconciliation step attempts to extract closing balance from:
|
|
124
|
-
|
|
125
|
-
1. CSV header metadata (e.g., UBS "Closing balance:" row)
|
|
126
|
-
2. CSV data analysis (balance field in last transaction row)
|
|
127
|
-
3. Manual override via `closingBalance` parameter (fallback)
|
|
128
|
-
|
|
129
|
-
For most providers, manual balance input is no longer required.
|
|
130
|
-
|
|
131
111
|
## Tool Usage Reference
|
|
132
112
|
|
|
133
113
|
The following are MCP tools available to you. Always call these tools directly - do not attempt to replicate their behavior with shell commands.
|
|
@@ -158,13 +138,12 @@ The following are MCP tools available to you. Always call these tools directly -
|
|
|
158
138
|
1. Creates isolated git worktree
|
|
159
139
|
2. Syncs CSV files from main repo to worktree
|
|
160
140
|
3. Classifies CSV files (unless `skipClassify: true`)
|
|
161
|
-
4.
|
|
162
|
-
5.
|
|
163
|
-
6.
|
|
164
|
-
7.
|
|
165
|
-
8.
|
|
166
|
-
9.
|
|
167
|
-
10. Cleans up worktree
|
|
141
|
+
4. Validates all transactions have matching rules (dry run)
|
|
142
|
+
5. Imports transactions to year journal
|
|
143
|
+
6. Reconciles closing balance against CSV metadata or manual value
|
|
144
|
+
7. Merges to main with `--no-ff` commit
|
|
145
|
+
8. Deletes processed CSV files from main repo's import/incoming
|
|
146
|
+
9. Cleans up worktree
|
|
168
147
|
|
|
169
148
|
**Output:** Returns step-by-step results with success/failure for each phase
|
|
170
149
|
|
package/dist/index.js
CHANGED
|
@@ -24191,6 +24191,12 @@ function buildErrorResult4(params) {
|
|
|
24191
24191
|
...params
|
|
24192
24192
|
});
|
|
24193
24193
|
}
|
|
24194
|
+
function buildSuccessResult4(params) {
|
|
24195
|
+
return JSON.stringify({
|
|
24196
|
+
success: true,
|
|
24197
|
+
...params
|
|
24198
|
+
});
|
|
24199
|
+
}
|
|
24194
24200
|
function validateWorktree(directory, worktreeChecker) {
|
|
24195
24201
|
if (!worktreeChecker(directory)) {
|
|
24196
24202
|
return buildErrorResult4({
|
|
@@ -24229,7 +24235,7 @@ function findCsvToReconcile(doneDir, options) {
|
|
|
24229
24235
|
const relativePath = path11.relative(path11.dirname(path11.dirname(doneDir)), csvFile);
|
|
24230
24236
|
return { csvFile, relativePath };
|
|
24231
24237
|
}
|
|
24232
|
-
function determineClosingBalance(csvFile, config2, options, relativeCsvPath
|
|
24238
|
+
function determineClosingBalance(csvFile, config2, options, relativeCsvPath) {
|
|
24233
24239
|
let metadata;
|
|
24234
24240
|
try {
|
|
24235
24241
|
const content = fs11.readFileSync(csvFile, "utf-8");
|
|
@@ -24248,41 +24254,17 @@ function determineClosingBalance(csvFile, config2, options, relativeCsvPath, rul
|
|
|
24248
24254
|
}
|
|
24249
24255
|
}
|
|
24250
24256
|
if (!closingBalance) {
|
|
24251
|
-
const csvAnalysis = tryExtractClosingBalanceFromCSV(csvFile, rulesDir);
|
|
24252
|
-
if (csvAnalysis && csvAnalysis.confidence === "high") {
|
|
24253
|
-
closingBalance = csvAnalysis.balance;
|
|
24254
|
-
return { closingBalance, metadata, fromCSVAnalysis: true };
|
|
24255
|
-
}
|
|
24256
|
-
}
|
|
24257
|
-
if (!closingBalance) {
|
|
24258
|
-
const retryCmd = buildRetryCommand(options, "CHF 2324.79", options.account);
|
|
24259
24257
|
return {
|
|
24260
24258
|
error: buildErrorResult4({
|
|
24261
24259
|
csvFile: relativeCsvPath,
|
|
24262
|
-
error: "No closing balance found in CSV metadata
|
|
24263
|
-
hint:
|
|
24260
|
+
error: "No closing balance found in CSV metadata",
|
|
24261
|
+
hint: "Provide closingBalance parameter manually",
|
|
24264
24262
|
metadata
|
|
24265
24263
|
})
|
|
24266
24264
|
};
|
|
24267
24265
|
}
|
|
24268
24266
|
return { closingBalance, metadata };
|
|
24269
24267
|
}
|
|
24270
|
-
function buildRetryCommand(options, closingBalance, account) {
|
|
24271
|
-
const parts = ["import-pipeline"];
|
|
24272
|
-
if (options.provider) {
|
|
24273
|
-
parts.push(`--provider ${options.provider}`);
|
|
24274
|
-
}
|
|
24275
|
-
if (options.currency) {
|
|
24276
|
-
parts.push(`--currency ${options.currency}`);
|
|
24277
|
-
}
|
|
24278
|
-
if (closingBalance) {
|
|
24279
|
-
parts.push(`--closingBalance "${closingBalance}"`);
|
|
24280
|
-
}
|
|
24281
|
-
if (account) {
|
|
24282
|
-
parts.push(`--account "${account}"`);
|
|
24283
|
-
}
|
|
24284
|
-
return parts.join(" ");
|
|
24285
|
-
}
|
|
24286
24268
|
function determineAccount(csvFile, rulesDir, options, relativeCsvPath, metadata) {
|
|
24287
24269
|
let account = options.account;
|
|
24288
24270
|
if (!account) {
|
|
@@ -24295,7 +24277,7 @@ function determineAccount(csvFile, rulesDir, options, relativeCsvPath, metadata)
|
|
|
24295
24277
|
if (!account) {
|
|
24296
24278
|
const rulesMapping = loadRulesMapping(rulesDir);
|
|
24297
24279
|
const rulesFile = findRulesForCsv(csvFile, rulesMapping);
|
|
24298
|
-
const rulesHint = rulesFile ? `Add 'account1 assets:bank:...' to ${rulesFile} or
|
|
24280
|
+
const rulesHint = rulesFile ? `Add 'account1 assets:bank:...' to ${rulesFile} or use --account parameter` : `Create a rules file in ${rulesDir} with 'account1' directive or use --account parameter`;
|
|
24299
24281
|
return {
|
|
24300
24282
|
error: buildErrorResult4({
|
|
24301
24283
|
csvFile: relativeCsvPath,
|
|
@@ -24307,70 +24289,6 @@ function determineAccount(csvFile, rulesDir, options, relativeCsvPath, metadata)
|
|
|
24307
24289
|
}
|
|
24308
24290
|
return { account };
|
|
24309
24291
|
}
|
|
24310
|
-
function tryExtractClosingBalanceFromCSV(csvFile, rulesDir) {
|
|
24311
|
-
try {
|
|
24312
|
-
const rulesMapping = loadRulesMapping(rulesDir);
|
|
24313
|
-
const rulesFile = findRulesForCsv(csvFile, rulesMapping);
|
|
24314
|
-
if (!rulesFile) {
|
|
24315
|
-
return null;
|
|
24316
|
-
}
|
|
24317
|
-
const rulesContent = fs11.readFileSync(rulesFile, "utf-8");
|
|
24318
|
-
const rulesConfig = parseRulesFile(rulesContent);
|
|
24319
|
-
const csvRows = parseCsvFile(csvFile, rulesConfig);
|
|
24320
|
-
if (csvRows.length === 0) {
|
|
24321
|
-
return null;
|
|
24322
|
-
}
|
|
24323
|
-
const balanceFieldNames = [
|
|
24324
|
-
"balance",
|
|
24325
|
-
"Balance",
|
|
24326
|
-
"BALANCE",
|
|
24327
|
-
"closing_balance",
|
|
24328
|
-
"Closing Balance",
|
|
24329
|
-
"account_balance",
|
|
24330
|
-
"Account Balance",
|
|
24331
|
-
"saldo",
|
|
24332
|
-
"Saldo",
|
|
24333
|
-
"SALDO"
|
|
24334
|
-
];
|
|
24335
|
-
const lastRow = csvRows[csvRows.length - 1];
|
|
24336
|
-
let balanceField;
|
|
24337
|
-
let balanceValue;
|
|
24338
|
-
for (const fieldName of balanceFieldNames) {
|
|
24339
|
-
if (lastRow[fieldName] !== undefined && lastRow[fieldName].trim() !== "") {
|
|
24340
|
-
balanceField = fieldName;
|
|
24341
|
-
balanceValue = lastRow[fieldName];
|
|
24342
|
-
break;
|
|
24343
|
-
}
|
|
24344
|
-
}
|
|
24345
|
-
if (balanceValue && balanceField) {
|
|
24346
|
-
const numericValue = parseAmountValue(balanceValue);
|
|
24347
|
-
let currency = "";
|
|
24348
|
-
const balanceCurrencyMatch = balanceValue.match(/[A-Z]{3}/);
|
|
24349
|
-
if (balanceCurrencyMatch) {
|
|
24350
|
-
currency = balanceCurrencyMatch[0];
|
|
24351
|
-
}
|
|
24352
|
-
if (!currency) {
|
|
24353
|
-
const amountField = rulesConfig.amountFields.single || rulesConfig.amountFields.credit || rulesConfig.amountFields.debit;
|
|
24354
|
-
if (amountField) {
|
|
24355
|
-
const amountStr = lastRow[amountField] || "";
|
|
24356
|
-
const currencyMatch = amountStr.match(/[A-Z]{3}/);
|
|
24357
|
-
if (currencyMatch) {
|
|
24358
|
-
currency = currencyMatch[0];
|
|
24359
|
-
}
|
|
24360
|
-
}
|
|
24361
|
-
}
|
|
24362
|
-
const balanceStr = currency ? `${currency} ${numericValue.toFixed(2)}` : numericValue.toFixed(2);
|
|
24363
|
-
return {
|
|
24364
|
-
balance: balanceStr,
|
|
24365
|
-
confidence: "high",
|
|
24366
|
-
method: `Extracted from ${balanceField} field in last CSV row`
|
|
24367
|
-
};
|
|
24368
|
-
}
|
|
24369
|
-
return null;
|
|
24370
|
-
} catch {
|
|
24371
|
-
return null;
|
|
24372
|
-
}
|
|
24373
|
-
}
|
|
24374
24292
|
async function reconcileStatement(directory, agent, options, configLoader = loadImportConfig, hledgerExecutor = defaultHledgerExecutor, worktreeChecker = isInWorktree) {
|
|
24375
24293
|
const restrictionError = checkAccountantAgent(agent, "reconcile statement");
|
|
24376
24294
|
if (restrictionError) {
|
|
@@ -24393,11 +24311,11 @@ async function reconcileStatement(directory, agent, options, configLoader = load
|
|
|
24393
24311
|
return csvResult.error;
|
|
24394
24312
|
}
|
|
24395
24313
|
const { csvFile, relativePath: relativeCsvPath } = csvResult;
|
|
24396
|
-
const balanceResult = determineClosingBalance(csvFile, config2, options, relativeCsvPath
|
|
24314
|
+
const balanceResult = determineClosingBalance(csvFile, config2, options, relativeCsvPath);
|
|
24397
24315
|
if ("error" in balanceResult) {
|
|
24398
24316
|
return balanceResult.error;
|
|
24399
24317
|
}
|
|
24400
|
-
const { closingBalance, metadata
|
|
24318
|
+
const { closingBalance, metadata } = balanceResult;
|
|
24401
24319
|
const accountResult = determineAccount(csvFile, rulesDir, options, relativeCsvPath, metadata);
|
|
24402
24320
|
if ("error" in accountResult) {
|
|
24403
24321
|
return accountResult.error;
|
|
@@ -24439,19 +24357,14 @@ async function reconcileStatement(directory, agent, options, configLoader = load
|
|
|
24439
24357
|
});
|
|
24440
24358
|
}
|
|
24441
24359
|
if (doBalancesMatch) {
|
|
24442
|
-
|
|
24443
|
-
success: true,
|
|
24360
|
+
return buildSuccessResult4({
|
|
24444
24361
|
csvFile: relativeCsvPath,
|
|
24445
24362
|
account,
|
|
24446
24363
|
lastTransactionDate,
|
|
24447
24364
|
expectedBalance: closingBalance,
|
|
24448
24365
|
actualBalance,
|
|
24449
24366
|
metadata
|
|
24450
|
-
};
|
|
24451
|
-
if (fromCSVAnalysis) {
|
|
24452
|
-
result.note = `Closing balance auto-detected from CSV data (no metadata available). Account: ${account}`;
|
|
24453
|
-
}
|
|
24454
|
-
return JSON.stringify(result);
|
|
24367
|
+
});
|
|
24455
24368
|
}
|
|
24456
24369
|
let difference;
|
|
24457
24370
|
try {
|
|
@@ -24516,125 +24429,8 @@ It must be run inside an import worktree (use import-pipeline for the full workf
|
|
|
24516
24429
|
}
|
|
24517
24430
|
});
|
|
24518
24431
|
// src/tools/import-pipeline.ts
|
|
24519
|
-
import * as fs13 from "fs";
|
|
24520
|
-
import * as path12 from "path";
|
|
24521
|
-
|
|
24522
|
-
// src/utils/accountDeclarations.ts
|
|
24523
24432
|
import * as fs12 from "fs";
|
|
24524
|
-
|
|
24525
|
-
const accounts = new Set;
|
|
24526
|
-
if (!fs12.existsSync(rulesPath)) {
|
|
24527
|
-
return accounts;
|
|
24528
|
-
}
|
|
24529
|
-
const content = fs12.readFileSync(rulesPath, "utf-8");
|
|
24530
|
-
const lines = content.split(`
|
|
24531
|
-
`);
|
|
24532
|
-
for (const line of lines) {
|
|
24533
|
-
const trimmed = line.trim();
|
|
24534
|
-
if (trimmed.startsWith("#") || trimmed.startsWith(";") || trimmed === "") {
|
|
24535
|
-
continue;
|
|
24536
|
-
}
|
|
24537
|
-
const account1Match = trimmed.match(/^account1\s+(.+?)(?:\s+|$)/);
|
|
24538
|
-
if (account1Match) {
|
|
24539
|
-
accounts.add(account1Match[1].trim());
|
|
24540
|
-
continue;
|
|
24541
|
-
}
|
|
24542
|
-
const account2Match = trimmed.match(/account2\s+(.+?)(?:\s+|$)/);
|
|
24543
|
-
if (account2Match) {
|
|
24544
|
-
accounts.add(account2Match[1].trim());
|
|
24545
|
-
continue;
|
|
24546
|
-
}
|
|
24547
|
-
}
|
|
24548
|
-
return accounts;
|
|
24549
|
-
}
|
|
24550
|
-
function getAllAccountsFromRules(rulesPaths) {
|
|
24551
|
-
const allAccounts = new Set;
|
|
24552
|
-
for (const rulesPath of rulesPaths) {
|
|
24553
|
-
const accounts = extractAccountsFromRulesFile(rulesPath);
|
|
24554
|
-
for (const account of accounts) {
|
|
24555
|
-
allAccounts.add(account);
|
|
24556
|
-
}
|
|
24557
|
-
}
|
|
24558
|
-
return allAccounts;
|
|
24559
|
-
}
|
|
24560
|
-
function sortAccountDeclarations(accounts) {
|
|
24561
|
-
return Array.from(accounts).sort((a, b) => a.localeCompare(b));
|
|
24562
|
-
}
|
|
24563
|
-
function ensureAccountDeclarations(yearJournalPath, accounts) {
|
|
24564
|
-
if (!fs12.existsSync(yearJournalPath)) {
|
|
24565
|
-
throw new Error(`Year journal not found: ${yearJournalPath}`);
|
|
24566
|
-
}
|
|
24567
|
-
const content = fs12.readFileSync(yearJournalPath, "utf-8");
|
|
24568
|
-
const lines = content.split(`
|
|
24569
|
-
`);
|
|
24570
|
-
const existingAccounts = new Set;
|
|
24571
|
-
const commentLines = [];
|
|
24572
|
-
const accountLines = [];
|
|
24573
|
-
const otherLines = [];
|
|
24574
|
-
let inAccountSection = false;
|
|
24575
|
-
let accountSectionEnded = false;
|
|
24576
|
-
for (const line of lines) {
|
|
24577
|
-
const trimmed = line.trim();
|
|
24578
|
-
if (trimmed.startsWith(";") || trimmed.startsWith("#")) {
|
|
24579
|
-
if (!accountSectionEnded) {
|
|
24580
|
-
commentLines.push(line);
|
|
24581
|
-
} else {
|
|
24582
|
-
otherLines.push(line);
|
|
24583
|
-
}
|
|
24584
|
-
continue;
|
|
24585
|
-
}
|
|
24586
|
-
if (trimmed.startsWith("account ")) {
|
|
24587
|
-
inAccountSection = true;
|
|
24588
|
-
const accountMatch = trimmed.match(/^account\s+(.+?)(?:\s+|$)/);
|
|
24589
|
-
if (accountMatch) {
|
|
24590
|
-
const accountName = accountMatch[1].trim();
|
|
24591
|
-
existingAccounts.add(accountName);
|
|
24592
|
-
accountLines.push(line);
|
|
24593
|
-
}
|
|
24594
|
-
continue;
|
|
24595
|
-
}
|
|
24596
|
-
if (trimmed === "") {
|
|
24597
|
-
if (inAccountSection && !accountSectionEnded) {
|
|
24598
|
-
accountLines.push(line);
|
|
24599
|
-
} else {
|
|
24600
|
-
otherLines.push(line);
|
|
24601
|
-
}
|
|
24602
|
-
continue;
|
|
24603
|
-
}
|
|
24604
|
-
if (inAccountSection) {
|
|
24605
|
-
accountSectionEnded = true;
|
|
24606
|
-
}
|
|
24607
|
-
otherLines.push(line);
|
|
24608
|
-
}
|
|
24609
|
-
const missingAccounts = new Set;
|
|
24610
|
-
for (const account of accounts) {
|
|
24611
|
-
if (!existingAccounts.has(account)) {
|
|
24612
|
-
missingAccounts.add(account);
|
|
24613
|
-
}
|
|
24614
|
-
}
|
|
24615
|
-
if (missingAccounts.size === 0) {
|
|
24616
|
-
return { added: [], updated: false };
|
|
24617
|
-
}
|
|
24618
|
-
const allAccounts = new Set([...existingAccounts, ...missingAccounts]);
|
|
24619
|
-
const sortedAccounts = sortAccountDeclarations(allAccounts);
|
|
24620
|
-
const newAccountLines = sortedAccounts.map((account) => `account ${account}`);
|
|
24621
|
-
const newContent = [];
|
|
24622
|
-
newContent.push(...commentLines);
|
|
24623
|
-
if (newAccountLines.length > 0) {
|
|
24624
|
-
newContent.push("");
|
|
24625
|
-
newContent.push(...newAccountLines);
|
|
24626
|
-
newContent.push("");
|
|
24627
|
-
}
|
|
24628
|
-
newContent.push(...otherLines);
|
|
24629
|
-
fs12.writeFileSync(yearJournalPath, newContent.join(`
|
|
24630
|
-
`));
|
|
24631
|
-
return {
|
|
24632
|
-
added: Array.from(missingAccounts).sort(),
|
|
24633
|
-
updated: true
|
|
24634
|
-
};
|
|
24635
|
-
}
|
|
24636
|
-
|
|
24637
|
-
// src/tools/import-pipeline.ts
|
|
24433
|
+
import * as path12 from "path";
|
|
24638
24434
|
class NoTransactionsError extends Error {
|
|
24639
24435
|
constructor() {
|
|
24640
24436
|
super("No transactions to import");
|
|
@@ -24648,7 +24444,7 @@ function buildStepResult(success2, message, details) {
|
|
|
24648
24444
|
}
|
|
24649
24445
|
return result;
|
|
24650
24446
|
}
|
|
24651
|
-
function
|
|
24447
|
+
function buildSuccessResult5(result, summary) {
|
|
24652
24448
|
result.success = true;
|
|
24653
24449
|
result.summary = summary;
|
|
24654
24450
|
return JSON.stringify(result);
|
|
@@ -24674,7 +24470,7 @@ function buildCommitMessage(provider, currency, fromDate, untilDate, transaction
|
|
|
24674
24470
|
}
|
|
24675
24471
|
function cleanupIncomingFiles(worktree, context) {
|
|
24676
24472
|
const incomingDir = path12.join(worktree.mainRepoPath, "import/incoming");
|
|
24677
|
-
if (!
|
|
24473
|
+
if (!fs12.existsSync(incomingDir)) {
|
|
24678
24474
|
return;
|
|
24679
24475
|
}
|
|
24680
24476
|
const importStep = context.result.steps.import;
|
|
@@ -24691,9 +24487,9 @@ function cleanupIncomingFiles(worktree, context) {
|
|
|
24691
24487
|
continue;
|
|
24692
24488
|
const filename = path12.basename(fileResult.csv);
|
|
24693
24489
|
const filePath = path12.join(incomingDir, filename);
|
|
24694
|
-
if (
|
|
24490
|
+
if (fs12.existsSync(filePath)) {
|
|
24695
24491
|
try {
|
|
24696
|
-
|
|
24492
|
+
fs12.unlinkSync(filePath);
|
|
24697
24493
|
deletedCount++;
|
|
24698
24494
|
} catch (error45) {
|
|
24699
24495
|
console.error(`[ERROR] Failed to delete ${filename}: ${error45 instanceof Error ? error45.message : String(error45)}`);
|
|
@@ -24724,86 +24520,6 @@ async function executeClassifyStep(context, worktree) {
|
|
|
24724
24520
|
};
|
|
24725
24521
|
context.result.steps.classify = buildStepResult(success2, message, details);
|
|
24726
24522
|
}
|
|
24727
|
-
async function executeAccountDeclarationsStep(context, worktree) {
|
|
24728
|
-
const config2 = context.configLoader(worktree.path);
|
|
24729
|
-
const pendingDir = path12.join(worktree.path, config2.paths.pending);
|
|
24730
|
-
const rulesDir = path12.join(worktree.path, config2.paths.rules);
|
|
24731
|
-
const csvFiles = findCsvFiles(pendingDir, context.options.provider, context.options.currency);
|
|
24732
|
-
if (csvFiles.length === 0) {
|
|
24733
|
-
context.result.steps.accountDeclarations = buildStepResult(true, "No CSV files to process", {
|
|
24734
|
-
accountsAdded: [],
|
|
24735
|
-
journalUpdated: "",
|
|
24736
|
-
rulesScanned: []
|
|
24737
|
-
});
|
|
24738
|
-
return;
|
|
24739
|
-
}
|
|
24740
|
-
const rulesMapping = loadRulesMapping(rulesDir);
|
|
24741
|
-
const matchedRulesFiles = new Set;
|
|
24742
|
-
for (const csvFile of csvFiles) {
|
|
24743
|
-
const rulesFile = findRulesForCsv(csvFile, rulesMapping);
|
|
24744
|
-
if (rulesFile) {
|
|
24745
|
-
matchedRulesFiles.add(rulesFile);
|
|
24746
|
-
}
|
|
24747
|
-
}
|
|
24748
|
-
if (matchedRulesFiles.size === 0) {
|
|
24749
|
-
context.result.steps.accountDeclarations = buildStepResult(true, "No matching rules files found", {
|
|
24750
|
-
accountsAdded: [],
|
|
24751
|
-
journalUpdated: "",
|
|
24752
|
-
rulesScanned: []
|
|
24753
|
-
});
|
|
24754
|
-
return;
|
|
24755
|
-
}
|
|
24756
|
-
const allAccounts = getAllAccountsFromRules(Array.from(matchedRulesFiles));
|
|
24757
|
-
if (allAccounts.size === 0) {
|
|
24758
|
-
context.result.steps.accountDeclarations = buildStepResult(true, "No accounts found in rules files", {
|
|
24759
|
-
accountsAdded: [],
|
|
24760
|
-
journalUpdated: "",
|
|
24761
|
-
rulesScanned: Array.from(matchedRulesFiles).map((f) => path12.relative(worktree.path, f))
|
|
24762
|
-
});
|
|
24763
|
-
return;
|
|
24764
|
-
}
|
|
24765
|
-
let transactionYear;
|
|
24766
|
-
for (const rulesFile of matchedRulesFiles) {
|
|
24767
|
-
try {
|
|
24768
|
-
const result2 = await context.hledgerExecutor(["print", "-f", rulesFile]);
|
|
24769
|
-
if (result2.exitCode === 0) {
|
|
24770
|
-
const years = extractTransactionYears(result2.stdout);
|
|
24771
|
-
if (years.size > 0) {
|
|
24772
|
-
transactionYear = Array.from(years)[0];
|
|
24773
|
-
break;
|
|
24774
|
-
}
|
|
24775
|
-
}
|
|
24776
|
-
} catch {
|
|
24777
|
-
continue;
|
|
24778
|
-
}
|
|
24779
|
-
}
|
|
24780
|
-
if (!transactionYear) {
|
|
24781
|
-
context.result.steps.accountDeclarations = buildStepResult(false, "Could not determine transaction year from CSV files", {
|
|
24782
|
-
accountsAdded: [],
|
|
24783
|
-
journalUpdated: "",
|
|
24784
|
-
rulesScanned: Array.from(matchedRulesFiles).map((f) => path12.relative(worktree.path, f))
|
|
24785
|
-
});
|
|
24786
|
-
return;
|
|
24787
|
-
}
|
|
24788
|
-
let yearJournalPath;
|
|
24789
|
-
try {
|
|
24790
|
-
yearJournalPath = ensureYearJournalExists(worktree.path, transactionYear);
|
|
24791
|
-
} catch (error45) {
|
|
24792
|
-
context.result.steps.accountDeclarations = buildStepResult(false, `Failed to create year journal: ${error45 instanceof Error ? error45.message : String(error45)}`, {
|
|
24793
|
-
accountsAdded: [],
|
|
24794
|
-
journalUpdated: "",
|
|
24795
|
-
rulesScanned: Array.from(matchedRulesFiles).map((f) => path12.relative(worktree.path, f))
|
|
24796
|
-
});
|
|
24797
|
-
return;
|
|
24798
|
-
}
|
|
24799
|
-
const result = ensureAccountDeclarations(yearJournalPath, allAccounts);
|
|
24800
|
-
const message = result.added.length > 0 ? `Added ${result.added.length} account declaration(s) to ${path12.relative(worktree.path, yearJournalPath)}` : "All required accounts already declared";
|
|
24801
|
-
context.result.steps.accountDeclarations = buildStepResult(true, message, {
|
|
24802
|
-
accountsAdded: result.added,
|
|
24803
|
-
journalUpdated: path12.relative(worktree.path, yearJournalPath),
|
|
24804
|
-
rulesScanned: Array.from(matchedRulesFiles).map((f) => path12.relative(worktree.path, f))
|
|
24805
|
-
});
|
|
24806
|
-
}
|
|
24807
24523
|
async function executeDryRunStep(context, worktree) {
|
|
24808
24524
|
const inWorktree = () => true;
|
|
24809
24525
|
const dryRunResult = await importStatements(worktree.path, context.agent, {
|
|
@@ -24896,7 +24612,7 @@ function handleNoTransactions(result) {
|
|
|
24896
24612
|
result.steps.import = buildStepResult(true, "No transactions to import");
|
|
24897
24613
|
result.steps.reconcile = buildStepResult(true, "Reconciliation skipped (no transactions)");
|
|
24898
24614
|
result.steps.merge = buildStepResult(true, "Merge skipped (no changes)");
|
|
24899
|
-
return
|
|
24615
|
+
return buildSuccessResult5(result, "No transactions found to import");
|
|
24900
24616
|
}
|
|
24901
24617
|
async function importPipeline(directory, agent, options, configLoader = loadImportConfig, hledgerExecutor = defaultHledgerExecutor) {
|
|
24902
24618
|
const restrictionError = checkAccountantAgent(agent, "import pipeline");
|
|
@@ -24940,7 +24656,6 @@ async function importPipeline(directory, agent, options, configLoader = loadImpo
|
|
|
24940
24656
|
}
|
|
24941
24657
|
try {
|
|
24942
24658
|
await executeClassifyStep(context, worktree);
|
|
24943
|
-
await executeAccountDeclarationsStep(context, worktree);
|
|
24944
24659
|
await executeDryRunStep(context, worktree);
|
|
24945
24660
|
await executeImportStep(context, worktree);
|
|
24946
24661
|
await executeReconcileStep(context, worktree);
|
|
@@ -24978,7 +24693,7 @@ async function importPipeline(directory, agent, options, configLoader = loadImpo
|
|
|
24978
24693
|
};
|
|
24979
24694
|
}
|
|
24980
24695
|
const transactionCount = context.result.steps.import?.details?.summary?.totalTransactions || 0;
|
|
24981
|
-
return
|
|
24696
|
+
return buildSuccessResult5(result, `Successfully imported ${transactionCount} transaction(s)`);
|
|
24982
24697
|
} catch (error45) {
|
|
24983
24698
|
result.steps.cleanup = buildStepResult(true, "Worktree cleaned up after failure (CSV files preserved for retry)", {
|
|
24984
24699
|
cleanedAfterFailure: true,
|
|
@@ -25043,7 +24758,7 @@ This tool orchestrates the full import workflow in an isolated git worktree:
|
|
|
25043
24758
|
}
|
|
25044
24759
|
});
|
|
25045
24760
|
// src/tools/init-directories.ts
|
|
25046
|
-
import * as
|
|
24761
|
+
import * as fs13 from "fs";
|
|
25047
24762
|
import * as path13 from "path";
|
|
25048
24763
|
async function initDirectories(directory) {
|
|
25049
24764
|
try {
|
|
@@ -25051,8 +24766,8 @@ async function initDirectories(directory) {
|
|
|
25051
24766
|
const directoriesCreated = [];
|
|
25052
24767
|
const gitkeepFiles = [];
|
|
25053
24768
|
const importBase = path13.join(directory, "import");
|
|
25054
|
-
if (!
|
|
25055
|
-
|
|
24769
|
+
if (!fs13.existsSync(importBase)) {
|
|
24770
|
+
fs13.mkdirSync(importBase, { recursive: true });
|
|
25056
24771
|
directoriesCreated.push("import");
|
|
25057
24772
|
}
|
|
25058
24773
|
const pathsToCreate = [
|
|
@@ -25063,19 +24778,19 @@ async function initDirectories(directory) {
|
|
|
25063
24778
|
];
|
|
25064
24779
|
for (const { path: dirPath } of pathsToCreate) {
|
|
25065
24780
|
const fullPath = path13.join(directory, dirPath);
|
|
25066
|
-
if (!
|
|
25067
|
-
|
|
24781
|
+
if (!fs13.existsSync(fullPath)) {
|
|
24782
|
+
fs13.mkdirSync(fullPath, { recursive: true });
|
|
25068
24783
|
directoriesCreated.push(dirPath);
|
|
25069
24784
|
}
|
|
25070
24785
|
const gitkeepPath = path13.join(fullPath, ".gitkeep");
|
|
25071
|
-
if (!
|
|
25072
|
-
|
|
24786
|
+
if (!fs13.existsSync(gitkeepPath)) {
|
|
24787
|
+
fs13.writeFileSync(gitkeepPath, "");
|
|
25073
24788
|
gitkeepFiles.push(path13.join(dirPath, ".gitkeep"));
|
|
25074
24789
|
}
|
|
25075
24790
|
}
|
|
25076
24791
|
const gitignorePath = path13.join(importBase, ".gitignore");
|
|
25077
24792
|
let gitignoreCreated = false;
|
|
25078
|
-
if (!
|
|
24793
|
+
if (!fs13.existsSync(gitignorePath)) {
|
|
25079
24794
|
const gitignoreContent = `# Ignore CSV/PDF files in temporary directories
|
|
25080
24795
|
/incoming/*.csv
|
|
25081
24796
|
/incoming/*.pdf
|
|
@@ -25093,7 +24808,7 @@ async function initDirectories(directory) {
|
|
|
25093
24808
|
.DS_Store
|
|
25094
24809
|
Thumbs.db
|
|
25095
24810
|
`;
|
|
25096
|
-
|
|
24811
|
+
fs13.writeFileSync(gitignorePath, gitignoreContent);
|
|
25097
24812
|
gitignoreCreated = true;
|
|
25098
24813
|
}
|
|
25099
24814
|
const parts = [];
|