@fuzzle/opencode-accountant 0.4.0 → 0.4.1-next.1
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 +28 -6
- package/agent/accountant.md +64 -2
- package/dist/index.js +364 -61
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -122,12 +122,21 @@ providers:
|
|
|
122
122
|
currencyField: Currency
|
|
123
123
|
skipRows: 9
|
|
124
124
|
delimiter: ';'
|
|
125
|
-
renamePattern: '
|
|
125
|
+
renamePattern: 'ubs-{account-number}-transactions-{from-date}-to-{until-date}.csv'
|
|
126
126
|
metadata:
|
|
127
127
|
- field: account-number
|
|
128
128
|
row: 0
|
|
129
129
|
column: 1
|
|
130
130
|
normalize: spaces-to-dashes
|
|
131
|
+
- field: from-date
|
|
132
|
+
row: 2
|
|
133
|
+
column: 1
|
|
134
|
+
- field: until-date
|
|
135
|
+
row: 3
|
|
136
|
+
column: 1
|
|
137
|
+
- field: closing-balance
|
|
138
|
+
row: 5
|
|
139
|
+
column: 1
|
|
131
140
|
currencies:
|
|
132
141
|
CHF: chf
|
|
133
142
|
EUR: eur
|
|
@@ -226,7 +235,14 @@ The `import-pipeline` tool is the single entry point for importing bank statemen
|
|
|
226
235
|
|
|
227
236
|
#### Rules File Matching
|
|
228
237
|
|
|
229
|
-
The tool matches CSV files to their rules files
|
|
238
|
+
The tool matches CSV files to their rules files using multiple methods:
|
|
239
|
+
|
|
240
|
+
1. **Source directive matching** (primary): Parses the `source` directive in each `.rules` file and supports glob patterns
|
|
241
|
+
2. **Filename matching** (fallback): If path matching fails, matches based on the rules filename prefix
|
|
242
|
+
|
|
243
|
+
**Example using source directive:**
|
|
244
|
+
|
|
245
|
+
If `ubs-account.rules` contains:
|
|
230
246
|
|
|
231
247
|
```
|
|
232
248
|
source ../../import/pending/ubs/chf/transactions.csv
|
|
@@ -234,7 +250,13 @@ source ../../import/pending/ubs/chf/transactions.csv
|
|
|
234
250
|
|
|
235
251
|
The tool will use that rules file when processing `transactions.csv`.
|
|
236
252
|
|
|
237
|
-
**
|
|
253
|
+
**Example using filename matching:**
|
|
254
|
+
|
|
255
|
+
If you have a rules file named `ubs-1234-567890.rules` and a CSV file named `ubs-1234-567890-transactions-2026-01-05-to-2026-01-31.csv`, the tool will automatically match them based on the common prefix `ubs-1234-567890`.
|
|
256
|
+
|
|
257
|
+
This is particularly useful when CSV files move between directories (e.g., from `pending/` to `done/`) or when maintaining exact source paths is impractical.
|
|
258
|
+
|
|
259
|
+
**Note:** Name your rules files to match the prefix of your CSV files for automatic matching.
|
|
238
260
|
|
|
239
261
|
See the hledger documentation for details on rules file format and syntax.
|
|
240
262
|
|
|
@@ -265,13 +287,13 @@ Configure metadata extraction in `providers.yaml`:
|
|
|
265
287
|
|
|
266
288
|
```yaml
|
|
267
289
|
metadata:
|
|
268
|
-
- field:
|
|
290
|
+
- field: closing-balance
|
|
269
291
|
row: 5
|
|
270
292
|
column: 1
|
|
271
|
-
- field:
|
|
293
|
+
- field: from-date
|
|
272
294
|
row: 2
|
|
273
295
|
column: 1
|
|
274
|
-
- field:
|
|
296
|
+
- field: until-date
|
|
275
297
|
row: 3
|
|
276
298
|
column: 1
|
|
277
299
|
```
|
package/agent/accountant.md
CHANGED
|
@@ -97,15 +97,77 @@ The `import-pipeline` tool provides an **atomic, safe workflow** using git workt
|
|
|
97
97
|
- Deletes processed CSV files from main repo's import/incoming
|
|
98
98
|
- Cleans up the worktree
|
|
99
99
|
4. **Handle Failures**: If any step fails (e.g., unknown postings found):
|
|
100
|
-
- Worktree is
|
|
100
|
+
- Worktree is preserved by default at `/tmp/import-worktree-<uuid>` for debugging
|
|
101
|
+
- Main branch remains untouched
|
|
101
102
|
- Review error output for unknown postings with full CSV row data
|
|
102
103
|
- Update rules file with `if` directives to match the transaction
|
|
103
104
|
- Re-run `import-pipeline`
|
|
104
105
|
|
|
106
|
+
### Error Recovery and Worktree Preservation
|
|
107
|
+
|
|
108
|
+
**Default Behavior:**
|
|
109
|
+
|
|
110
|
+
- On success: Worktrees are automatically cleaned up
|
|
111
|
+
- On error: Worktrees are preserved in `/tmp/import-worktree-<uuid>` for debugging
|
|
112
|
+
- Worktrees in `/tmp` are automatically cleaned up on system reboot
|
|
113
|
+
|
|
114
|
+
**Manual Recovery from Failed Import:**
|
|
115
|
+
|
|
116
|
+
If an import fails and the worktree is preserved, you can:
|
|
117
|
+
|
|
118
|
+
1. **Inspect the worktree:**
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
cd /tmp/import-worktree-<uuid>
|
|
122
|
+
hledger check # Validate journal
|
|
123
|
+
hledger balance # Check balances
|
|
124
|
+
cat ledger/2026.journal # View imported transactions
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
2. **Continue the import manually:**
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
cd /tmp/import-worktree-<uuid>
|
|
131
|
+
# Fix any issues (edit rules, fix transactions, etc.)
|
|
132
|
+
git add .
|
|
133
|
+
git commit -m "Fix import issues"
|
|
134
|
+
git checkout main
|
|
135
|
+
git merge --no-ff import-<uuid>
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
3. **Clean up when done:**
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
git worktree remove /tmp/import-worktree-<uuid>
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
4. **Or use the cleanup tool:**
|
|
145
|
+
```bash
|
|
146
|
+
cleanup-worktrees # Removes worktrees >24h old
|
|
147
|
+
cleanup-worktrees --all true # Removes all import worktrees
|
|
148
|
+
cleanup-worktrees --dryRun true # Preview without removing
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
**Logs:**
|
|
152
|
+
|
|
153
|
+
- Every import run generates a detailed log: `.memory/import-<timestamp>.md`
|
|
154
|
+
- Log includes all commands, output, timing, and errors
|
|
155
|
+
- Log path is included in import-pipeline output
|
|
156
|
+
- Review the log to understand what failed and why
|
|
157
|
+
|
|
158
|
+
**Force Cleanup on Error:**
|
|
159
|
+
If you prefer the old behavior (always cleanup, even on error):
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
import-pipeline --keepWorktreeOnError false
|
|
163
|
+
```
|
|
164
|
+
|
|
105
165
|
### Rules Files
|
|
106
166
|
|
|
107
167
|
- The location of the rules files is configured in `config/import/providers.yaml`
|
|
108
|
-
- Match CSV to rules file via the `source` directive in each `.rules` file
|
|
168
|
+
- Match CSV to rules file via the `source` directive in each `.rules` file, with automatic fallback to filename-based matching
|
|
169
|
+
- **Filename matching example:** If the rules file is named `ubs-1234-567890.rules`, it will automatically match CSV files like `ubs-1234-567890-transactions-2026-01.csv` based on the common prefix. This works even when CSV files move between directories.
|
|
170
|
+
- When account detection fails, recommend users either fix their `source` directive or rename their rules file to match the CSV filename prefix
|
|
109
171
|
- Use field names from the `fields` directive for matching
|
|
110
172
|
- Unknown account pattern: `income:unknown` (positive amounts) / `expenses:unknown` (negative amounts)
|
|
111
173
|
|
package/dist/index.js
CHANGED
|
@@ -17509,7 +17509,8 @@ function validatePaths(paths) {
|
|
|
17509
17509
|
pending: pathsObj.pending,
|
|
17510
17510
|
done: pathsObj.done,
|
|
17511
17511
|
unrecognized: pathsObj.unrecognized,
|
|
17512
|
-
rules: pathsObj.rules
|
|
17512
|
+
rules: pathsObj.rules,
|
|
17513
|
+
logs: pathsObj.logs
|
|
17513
17514
|
};
|
|
17514
17515
|
}
|
|
17515
17516
|
function validateDetectionRule(providerName, index, rule) {
|
|
@@ -17639,6 +17640,9 @@ function loadImportConfig(directory) {
|
|
|
17639
17640
|
for (const [name, config2] of Object.entries(providersObj)) {
|
|
17640
17641
|
providers[name] = validateProviderConfig(name, config2);
|
|
17641
17642
|
}
|
|
17643
|
+
if (!paths.logs) {
|
|
17644
|
+
paths.logs = ".memory";
|
|
17645
|
+
}
|
|
17642
17646
|
return { paths, providers };
|
|
17643
17647
|
}
|
|
17644
17648
|
|
|
@@ -17907,15 +17911,35 @@ function isInWorktree(directory) {
|
|
|
17907
17911
|
return false;
|
|
17908
17912
|
}
|
|
17909
17913
|
}
|
|
17910
|
-
async function withWorktree(directory, operation) {
|
|
17914
|
+
async function withWorktree(directory, operation, options) {
|
|
17911
17915
|
let createdWorktree = null;
|
|
17916
|
+
let operationSucceeded = false;
|
|
17917
|
+
const logger = options?.logger;
|
|
17918
|
+
const keepOnError = options?.keepOnError ?? true;
|
|
17912
17919
|
try {
|
|
17920
|
+
logger?.logStep("Create Worktree", "start");
|
|
17913
17921
|
createdWorktree = createImportWorktree(directory);
|
|
17922
|
+
logger?.logStep("Create Worktree", "success", `Path: ${createdWorktree.path}`);
|
|
17923
|
+
logger?.info(`Branch: ${createdWorktree.branch}`);
|
|
17924
|
+
logger?.info(`UUID: ${createdWorktree.uuid}`);
|
|
17914
17925
|
const result = await operation(createdWorktree);
|
|
17926
|
+
operationSucceeded = true;
|
|
17915
17927
|
return result;
|
|
17916
17928
|
} finally {
|
|
17917
17929
|
if (createdWorktree) {
|
|
17918
|
-
|
|
17930
|
+
if (operationSucceeded) {
|
|
17931
|
+
logger?.logStep("Cleanup Worktree", "start");
|
|
17932
|
+
removeWorktree(createdWorktree, true);
|
|
17933
|
+
logger?.logStep("Cleanup Worktree", "success", "Worktree removed");
|
|
17934
|
+
} else if (!keepOnError) {
|
|
17935
|
+
logger?.warn("Operation failed, but keepOnError=false, removing worktree");
|
|
17936
|
+
removeWorktree(createdWorktree, true);
|
|
17937
|
+
} else {
|
|
17938
|
+
logger?.warn("Operation failed, worktree preserved for debugging");
|
|
17939
|
+
logger?.info(`Worktree path: ${createdWorktree.path}`);
|
|
17940
|
+
logger?.info(`To clean up manually: git worktree remove ${createdWorktree.path}`);
|
|
17941
|
+
logger?.info(`To list all worktrees: git worktree list`);
|
|
17942
|
+
}
|
|
17919
17943
|
}
|
|
17920
17944
|
}
|
|
17921
17945
|
}
|
|
@@ -23458,6 +23482,18 @@ function findRulesForCsv(csvPath, mapping) {
|
|
|
23458
23482
|
return rulesFile;
|
|
23459
23483
|
}
|
|
23460
23484
|
}
|
|
23485
|
+
const csvBasename = path9.basename(csvPath);
|
|
23486
|
+
const matches = [];
|
|
23487
|
+
for (const rulesFile of Object.values(mapping)) {
|
|
23488
|
+
const rulesBasename = path9.basename(rulesFile, ".rules");
|
|
23489
|
+
if (csvBasename.startsWith(rulesBasename)) {
|
|
23490
|
+
matches.push({ rulesFile, prefixLength: rulesBasename.length });
|
|
23491
|
+
}
|
|
23492
|
+
}
|
|
23493
|
+
if (matches.length > 0) {
|
|
23494
|
+
matches.sort((a, b) => b.prefixLength - a.prefixLength);
|
|
23495
|
+
return matches[0].rulesFile;
|
|
23496
|
+
}
|
|
23461
23497
|
return null;
|
|
23462
23498
|
}
|
|
23463
23499
|
|
|
@@ -24016,7 +24052,7 @@ async function processCsvFile(csvFile, rulesMapping, directory, hledgerExecutor)
|
|
|
24016
24052
|
transactionYear
|
|
24017
24053
|
};
|
|
24018
24054
|
}
|
|
24019
|
-
async function importStatements(directory, agent, options, configLoader = loadImportConfig, hledgerExecutor = defaultHledgerExecutor, worktreeChecker = isInWorktree) {
|
|
24055
|
+
async function importStatements(directory, agent, options, configLoader = loadImportConfig, hledgerExecutor = defaultHledgerExecutor, worktreeChecker = isInWorktree, _logger) {
|
|
24020
24056
|
const restrictionError = checkAccountantAgent(agent, "import statements");
|
|
24021
24057
|
if (restrictionError) {
|
|
24022
24058
|
return restrictionError;
|
|
@@ -24240,10 +24276,11 @@ function determineClosingBalance(csvFile, config2, options, relativeCsvPath, rul
|
|
|
24240
24276
|
metadata = undefined;
|
|
24241
24277
|
}
|
|
24242
24278
|
let closingBalance = options.closingBalance;
|
|
24243
|
-
if (!closingBalance && metadata?.
|
|
24244
|
-
const
|
|
24245
|
-
|
|
24246
|
-
|
|
24279
|
+
if (!closingBalance && metadata?.["closing-balance"]) {
|
|
24280
|
+
const closingBalanceValue = metadata["closing-balance"];
|
|
24281
|
+
const currency = metadata.currency;
|
|
24282
|
+
closingBalance = closingBalanceValue;
|
|
24283
|
+
if (currency && closingBalance && !closingBalance.includes(currency)) {
|
|
24247
24284
|
closingBalance = `${currency} ${closingBalance}`;
|
|
24248
24285
|
}
|
|
24249
24286
|
}
|
|
@@ -24324,7 +24361,7 @@ function tryExtractClosingBalanceFromCSV(csvFile, rulesDir) {
|
|
|
24324
24361
|
"balance",
|
|
24325
24362
|
"Balance",
|
|
24326
24363
|
"BALANCE",
|
|
24327
|
-
"
|
|
24364
|
+
"closing-balance",
|
|
24328
24365
|
"Closing Balance",
|
|
24329
24366
|
"account_balance",
|
|
24330
24367
|
"Account Balance",
|
|
@@ -24516,8 +24553,8 @@ It must be run inside an import worktree (use import-pipeline for the full workf
|
|
|
24516
24553
|
}
|
|
24517
24554
|
});
|
|
24518
24555
|
// src/tools/import-pipeline.ts
|
|
24519
|
-
import * as
|
|
24520
|
-
import * as
|
|
24556
|
+
import * as fs14 from "fs";
|
|
24557
|
+
import * as path13 from "path";
|
|
24521
24558
|
|
|
24522
24559
|
// src/utils/accountDeclarations.ts
|
|
24523
24560
|
import * as fs12 from "fs";
|
|
@@ -24634,6 +24671,158 @@ function ensureAccountDeclarations(yearJournalPath, accounts) {
|
|
|
24634
24671
|
};
|
|
24635
24672
|
}
|
|
24636
24673
|
|
|
24674
|
+
// src/utils/logger.ts
|
|
24675
|
+
import fs13 from "fs/promises";
|
|
24676
|
+
import path12 from "path";
|
|
24677
|
+
|
|
24678
|
+
class MarkdownLogger {
|
|
24679
|
+
buffer = [];
|
|
24680
|
+
logPath;
|
|
24681
|
+
context = {};
|
|
24682
|
+
autoFlush;
|
|
24683
|
+
sectionDepth = 0;
|
|
24684
|
+
constructor(config2) {
|
|
24685
|
+
this.autoFlush = config2.autoFlush ?? true;
|
|
24686
|
+
this.context = config2.context || {};
|
|
24687
|
+
const filename = config2.filename || `import-${this.getTimestamp()}.md`;
|
|
24688
|
+
this.logPath = path12.join(config2.logDir, filename);
|
|
24689
|
+
this.buffer.push(`# Import Pipeline Log`);
|
|
24690
|
+
this.buffer.push(`**Started**: ${new Date().toLocaleString()}`);
|
|
24691
|
+
this.buffer.push("");
|
|
24692
|
+
}
|
|
24693
|
+
startSection(title, level = 2) {
|
|
24694
|
+
this.buffer.push("");
|
|
24695
|
+
this.buffer.push(`${"#".repeat(level + 1)} ${title}`);
|
|
24696
|
+
this.buffer.push(`**Started**: ${this.getTime()}`);
|
|
24697
|
+
this.buffer.push("");
|
|
24698
|
+
this.sectionDepth++;
|
|
24699
|
+
}
|
|
24700
|
+
endSection() {
|
|
24701
|
+
if (this.sectionDepth > 0) {
|
|
24702
|
+
this.buffer.push("");
|
|
24703
|
+
this.buffer.push("---");
|
|
24704
|
+
this.buffer.push("");
|
|
24705
|
+
this.sectionDepth--;
|
|
24706
|
+
}
|
|
24707
|
+
}
|
|
24708
|
+
info(message) {
|
|
24709
|
+
this.buffer.push(message);
|
|
24710
|
+
if (this.autoFlush)
|
|
24711
|
+
this.flushAsync();
|
|
24712
|
+
}
|
|
24713
|
+
warn(message) {
|
|
24714
|
+
this.buffer.push(`\u26A0\uFE0F **WARNING**: ${message}`);
|
|
24715
|
+
if (this.autoFlush)
|
|
24716
|
+
this.flushAsync();
|
|
24717
|
+
}
|
|
24718
|
+
error(message, error45) {
|
|
24719
|
+
this.buffer.push(`\u274C **ERROR**: ${message}`);
|
|
24720
|
+
if (error45) {
|
|
24721
|
+
const errorStr = error45 instanceof Error ? error45.message : String(error45);
|
|
24722
|
+
this.buffer.push("");
|
|
24723
|
+
this.buffer.push("```");
|
|
24724
|
+
this.buffer.push(errorStr);
|
|
24725
|
+
if (error45 instanceof Error && error45.stack) {
|
|
24726
|
+
this.buffer.push("");
|
|
24727
|
+
this.buffer.push(error45.stack);
|
|
24728
|
+
}
|
|
24729
|
+
this.buffer.push("```");
|
|
24730
|
+
this.buffer.push("");
|
|
24731
|
+
}
|
|
24732
|
+
if (this.autoFlush)
|
|
24733
|
+
this.flushAsync();
|
|
24734
|
+
}
|
|
24735
|
+
debug(message) {
|
|
24736
|
+
this.buffer.push(`\uD83D\uDD0D ${message}`);
|
|
24737
|
+
if (this.autoFlush)
|
|
24738
|
+
this.flushAsync();
|
|
24739
|
+
}
|
|
24740
|
+
logStep(stepName, status, details) {
|
|
24741
|
+
const icon = status === "success" ? "\u2705" : status === "error" ? "\u274C" : "\u25B6\uFE0F";
|
|
24742
|
+
const statusText = status.charAt(0).toUpperCase() + status.slice(1);
|
|
24743
|
+
this.buffer.push(`**${stepName}**: ${icon} ${statusText}`);
|
|
24744
|
+
if (details) {
|
|
24745
|
+
this.buffer.push(` ${details}`);
|
|
24746
|
+
}
|
|
24747
|
+
this.buffer.push("");
|
|
24748
|
+
if (this.autoFlush)
|
|
24749
|
+
this.flushAsync();
|
|
24750
|
+
}
|
|
24751
|
+
logCommand(command, output) {
|
|
24752
|
+
this.buffer.push("```bash");
|
|
24753
|
+
this.buffer.push(`$ ${command}`);
|
|
24754
|
+
if (output) {
|
|
24755
|
+
this.buffer.push("");
|
|
24756
|
+
const lines = output.trim().split(`
|
|
24757
|
+
`);
|
|
24758
|
+
if (lines.length > 50) {
|
|
24759
|
+
this.buffer.push(...lines.slice(0, 50));
|
|
24760
|
+
this.buffer.push(`... (${lines.length - 50} more lines omitted)`);
|
|
24761
|
+
} else {
|
|
24762
|
+
this.buffer.push(output.trim());
|
|
24763
|
+
}
|
|
24764
|
+
}
|
|
24765
|
+
this.buffer.push("```");
|
|
24766
|
+
this.buffer.push("");
|
|
24767
|
+
if (this.autoFlush)
|
|
24768
|
+
this.flushAsync();
|
|
24769
|
+
}
|
|
24770
|
+
logResult(data) {
|
|
24771
|
+
this.buffer.push("```json");
|
|
24772
|
+
this.buffer.push(JSON.stringify(data, null, 2));
|
|
24773
|
+
this.buffer.push("```");
|
|
24774
|
+
this.buffer.push("");
|
|
24775
|
+
if (this.autoFlush)
|
|
24776
|
+
this.flushAsync();
|
|
24777
|
+
}
|
|
24778
|
+
setContext(key, value) {
|
|
24779
|
+
this.context[key] = value;
|
|
24780
|
+
}
|
|
24781
|
+
async flush() {
|
|
24782
|
+
if (this.buffer.length === 0)
|
|
24783
|
+
return;
|
|
24784
|
+
try {
|
|
24785
|
+
await fs13.mkdir(path12.dirname(this.logPath), { recursive: true });
|
|
24786
|
+
await fs13.writeFile(this.logPath, this.buffer.join(`
|
|
24787
|
+
`), "utf-8");
|
|
24788
|
+
} catch {}
|
|
24789
|
+
}
|
|
24790
|
+
getLogPath() {
|
|
24791
|
+
return this.logPath;
|
|
24792
|
+
}
|
|
24793
|
+
flushAsync() {
|
|
24794
|
+
this.flush().catch(() => {});
|
|
24795
|
+
}
|
|
24796
|
+
getTimestamp() {
|
|
24797
|
+
return new Date().toISOString().replace(/:/g, "-").split(".")[0];
|
|
24798
|
+
}
|
|
24799
|
+
getTime() {
|
|
24800
|
+
return new Date().toLocaleTimeString();
|
|
24801
|
+
}
|
|
24802
|
+
}
|
|
24803
|
+
function createLogger(config2) {
|
|
24804
|
+
return new MarkdownLogger(config2);
|
|
24805
|
+
}
|
|
24806
|
+
function createImportLogger(directory, worktreeId, provider) {
|
|
24807
|
+
const context = {};
|
|
24808
|
+
if (worktreeId)
|
|
24809
|
+
context.worktreeId = worktreeId;
|
|
24810
|
+
if (provider)
|
|
24811
|
+
context.provider = provider;
|
|
24812
|
+
const logger = createLogger({
|
|
24813
|
+
logDir: path12.join(directory, ".memory"),
|
|
24814
|
+
autoFlush: true,
|
|
24815
|
+
context
|
|
24816
|
+
});
|
|
24817
|
+
if (worktreeId)
|
|
24818
|
+
logger.info(`**Worktree ID**: ${worktreeId}`);
|
|
24819
|
+
if (provider)
|
|
24820
|
+
logger.info(`**Provider**: ${provider}`);
|
|
24821
|
+
logger.info(`**Repository**: ${directory}`);
|
|
24822
|
+
logger.info("");
|
|
24823
|
+
return logger;
|
|
24824
|
+
}
|
|
24825
|
+
|
|
24637
24826
|
// src/tools/import-pipeline.ts
|
|
24638
24827
|
class NoTransactionsError extends Error {
|
|
24639
24828
|
constructor() {
|
|
@@ -24673,8 +24862,8 @@ function buildCommitMessage(provider, currency, fromDate, untilDate, transaction
|
|
|
24673
24862
|
return `${parts.join(" ")}${dateRange}${txStr}`;
|
|
24674
24863
|
}
|
|
24675
24864
|
function cleanupIncomingFiles(worktree, context) {
|
|
24676
|
-
const incomingDir =
|
|
24677
|
-
if (!
|
|
24865
|
+
const incomingDir = path13.join(worktree.mainRepoPath, "import/incoming");
|
|
24866
|
+
if (!fs14.existsSync(incomingDir)) {
|
|
24678
24867
|
return;
|
|
24679
24868
|
}
|
|
24680
24869
|
const importStep = context.result.steps.import;
|
|
@@ -24689,11 +24878,11 @@ function cleanupIncomingFiles(worktree, context) {
|
|
|
24689
24878
|
for (const fileResult of importResult.files) {
|
|
24690
24879
|
if (!fileResult.csv)
|
|
24691
24880
|
continue;
|
|
24692
|
-
const filename =
|
|
24693
|
-
const filePath =
|
|
24694
|
-
if (
|
|
24881
|
+
const filename = path13.basename(fileResult.csv);
|
|
24882
|
+
const filePath = path13.join(incomingDir, filename);
|
|
24883
|
+
if (fs14.existsSync(filePath)) {
|
|
24695
24884
|
try {
|
|
24696
|
-
|
|
24885
|
+
fs14.unlinkSync(filePath);
|
|
24697
24886
|
deletedCount++;
|
|
24698
24887
|
} catch (error45) {
|
|
24699
24888
|
console.error(`[ERROR] Failed to delete ${filename}: ${error45 instanceof Error ? error45.message : String(error45)}`);
|
|
@@ -24704,9 +24893,13 @@ function cleanupIncomingFiles(worktree, context) {
|
|
|
24704
24893
|
console.log(`[INFO] Cleaned up ${deletedCount} file(s) from import/incoming/`);
|
|
24705
24894
|
}
|
|
24706
24895
|
}
|
|
24707
|
-
async function executeClassifyStep(context, worktree) {
|
|
24896
|
+
async function executeClassifyStep(context, worktree, logger) {
|
|
24897
|
+
logger?.startSection("Step 2: Classify Transactions");
|
|
24898
|
+
logger?.logStep("Classify", "start");
|
|
24708
24899
|
if (context.options.skipClassify) {
|
|
24900
|
+
logger?.info("Classification skipped (skipClassify: true)");
|
|
24709
24901
|
context.result.steps.classify = buildStepResult(true, "Classification skipped (skipClassify: true)");
|
|
24902
|
+
logger?.endSection();
|
|
24710
24903
|
return;
|
|
24711
24904
|
}
|
|
24712
24905
|
const inWorktree = () => true;
|
|
@@ -24716,18 +24909,23 @@ async function executeClassifyStep(context, worktree) {
|
|
|
24716
24909
|
let message = success2 ? "Classification complete" : "Classification had issues";
|
|
24717
24910
|
if (classifyParsed.unrecognized?.length > 0) {
|
|
24718
24911
|
message = `Classification complete with ${classifyParsed.unrecognized.length} unrecognized file(s)`;
|
|
24912
|
+
logger?.warn(`${classifyParsed.unrecognized.length} unrecognized file(s)`);
|
|
24719
24913
|
}
|
|
24914
|
+
logger?.logStep("Classify", success2 ? "success" : "error", message);
|
|
24720
24915
|
const details = {
|
|
24721
24916
|
success: success2,
|
|
24722
24917
|
unrecognized: classifyParsed.unrecognized,
|
|
24723
24918
|
classified: classifyParsed
|
|
24724
24919
|
};
|
|
24725
24920
|
context.result.steps.classify = buildStepResult(success2, message, details);
|
|
24921
|
+
logger?.endSection();
|
|
24726
24922
|
}
|
|
24727
|
-
async function executeAccountDeclarationsStep(context, worktree) {
|
|
24923
|
+
async function executeAccountDeclarationsStep(context, worktree, logger) {
|
|
24924
|
+
logger?.startSection("Step 3: Check Account Declarations");
|
|
24925
|
+
logger?.logStep("Check Accounts", "start");
|
|
24728
24926
|
const config2 = context.configLoader(worktree.path);
|
|
24729
|
-
const pendingDir =
|
|
24730
|
-
const rulesDir =
|
|
24927
|
+
const pendingDir = path13.join(worktree.path, config2.paths.pending);
|
|
24928
|
+
const rulesDir = path13.join(worktree.path, config2.paths.rules);
|
|
24731
24929
|
const csvFiles = findCsvFiles(pendingDir, context.options.provider, context.options.currency);
|
|
24732
24930
|
if (csvFiles.length === 0) {
|
|
24733
24931
|
context.result.steps.accountDeclarations = buildStepResult(true, "No CSV files to process", {
|
|
@@ -24758,7 +24956,7 @@ async function executeAccountDeclarationsStep(context, worktree) {
|
|
|
24758
24956
|
context.result.steps.accountDeclarations = buildStepResult(true, "No accounts found in rules files", {
|
|
24759
24957
|
accountsAdded: [],
|
|
24760
24958
|
journalUpdated: "",
|
|
24761
|
-
rulesScanned: Array.from(matchedRulesFiles).map((f) =>
|
|
24959
|
+
rulesScanned: Array.from(matchedRulesFiles).map((f) => path13.relative(worktree.path, f))
|
|
24762
24960
|
});
|
|
24763
24961
|
return;
|
|
24764
24962
|
}
|
|
@@ -24781,7 +24979,7 @@ async function executeAccountDeclarationsStep(context, worktree) {
|
|
|
24781
24979
|
context.result.steps.accountDeclarations = buildStepResult(false, "Could not determine transaction year from CSV files", {
|
|
24782
24980
|
accountsAdded: [],
|
|
24783
24981
|
journalUpdated: "",
|
|
24784
|
-
rulesScanned: Array.from(matchedRulesFiles).map((f) =>
|
|
24982
|
+
rulesScanned: Array.from(matchedRulesFiles).map((f) => path13.relative(worktree.path, f))
|
|
24785
24983
|
});
|
|
24786
24984
|
return;
|
|
24787
24985
|
}
|
|
@@ -24792,19 +24990,28 @@ async function executeAccountDeclarationsStep(context, worktree) {
|
|
|
24792
24990
|
context.result.steps.accountDeclarations = buildStepResult(false, `Failed to create year journal: ${error45 instanceof Error ? error45.message : String(error45)}`, {
|
|
24793
24991
|
accountsAdded: [],
|
|
24794
24992
|
journalUpdated: "",
|
|
24795
|
-
rulesScanned: Array.from(matchedRulesFiles).map((f) =>
|
|
24993
|
+
rulesScanned: Array.from(matchedRulesFiles).map((f) => path13.relative(worktree.path, f))
|
|
24796
24994
|
});
|
|
24797
24995
|
return;
|
|
24798
24996
|
}
|
|
24799
24997
|
const result = ensureAccountDeclarations(yearJournalPath, allAccounts);
|
|
24800
|
-
const message = result.added.length > 0 ? `Added ${result.added.length} account declaration(s) to ${
|
|
24998
|
+
const message = result.added.length > 0 ? `Added ${result.added.length} account declaration(s) to ${path13.relative(worktree.path, yearJournalPath)}` : "All required accounts already declared";
|
|
24999
|
+
logger?.logStep("Check Accounts", "success", message);
|
|
25000
|
+
if (result.added.length > 0) {
|
|
25001
|
+
for (const account of result.added) {
|
|
25002
|
+
logger?.info(` - ${account}`);
|
|
25003
|
+
}
|
|
25004
|
+
}
|
|
24801
25005
|
context.result.steps.accountDeclarations = buildStepResult(true, message, {
|
|
24802
25006
|
accountsAdded: result.added,
|
|
24803
|
-
journalUpdated:
|
|
24804
|
-
rulesScanned: Array.from(matchedRulesFiles).map((f) =>
|
|
25007
|
+
journalUpdated: path13.relative(worktree.path, yearJournalPath),
|
|
25008
|
+
rulesScanned: Array.from(matchedRulesFiles).map((f) => path13.relative(worktree.path, f))
|
|
24805
25009
|
});
|
|
25010
|
+
logger?.endSection();
|
|
24806
25011
|
}
|
|
24807
|
-
async function executeDryRunStep(context, worktree) {
|
|
25012
|
+
async function executeDryRunStep(context, worktree, logger) {
|
|
25013
|
+
logger?.startSection("Step 4: Dry Run Import");
|
|
25014
|
+
logger?.logStep("Dry Run", "start");
|
|
24808
25015
|
const inWorktree = () => true;
|
|
24809
25016
|
const dryRunResult = await importStatements(worktree.path, context.agent, {
|
|
24810
25017
|
provider: context.options.provider,
|
|
@@ -24813,20 +25020,30 @@ async function executeDryRunStep(context, worktree) {
|
|
|
24813
25020
|
}, context.configLoader, context.hledgerExecutor, inWorktree);
|
|
24814
25021
|
const dryRunParsed = JSON.parse(dryRunResult);
|
|
24815
25022
|
const message = dryRunParsed.success ? `Dry run passed: ${dryRunParsed.summary?.totalTransactions || 0} transactions ready` : `Dry run failed: ${dryRunParsed.summary?.unknown || 0} unknown account(s)`;
|
|
25023
|
+
logger?.logStep("Dry Run", dryRunParsed.success ? "success" : "error", message);
|
|
25024
|
+
if (dryRunParsed.summary?.totalTransactions) {
|
|
25025
|
+
logger?.info(`Found ${dryRunParsed.summary.totalTransactions} transactions`);
|
|
25026
|
+
}
|
|
24816
25027
|
context.result.steps.dryRun = buildStepResult(dryRunParsed.success, message, {
|
|
24817
25028
|
success: dryRunParsed.success,
|
|
24818
25029
|
summary: dryRunParsed.summary
|
|
24819
25030
|
});
|
|
24820
25031
|
if (!dryRunParsed.success) {
|
|
25032
|
+
logger?.error("Dry run found unknown accounts or errors");
|
|
25033
|
+
logger?.endSection();
|
|
24821
25034
|
context.result.error = "Dry run found unknown accounts or errors";
|
|
24822
25035
|
context.result.hint = "Add rules to categorize unknown transactions, then retry";
|
|
24823
25036
|
throw new Error("Dry run failed");
|
|
24824
25037
|
}
|
|
24825
25038
|
if (dryRunParsed.summary?.totalTransactions === 0) {
|
|
25039
|
+
logger?.endSection();
|
|
24826
25040
|
throw new NoTransactionsError;
|
|
24827
25041
|
}
|
|
25042
|
+
logger?.endSection();
|
|
24828
25043
|
}
|
|
24829
|
-
async function executeImportStep(context, worktree) {
|
|
25044
|
+
async function executeImportStep(context, worktree, logger) {
|
|
25045
|
+
logger?.startSection("Step 5: Import Transactions");
|
|
25046
|
+
logger?.logStep("Import", "start");
|
|
24830
25047
|
const inWorktree = () => true;
|
|
24831
25048
|
const importResult = await importStatements(worktree.path, context.agent, {
|
|
24832
25049
|
provider: context.options.provider,
|
|
@@ -24835,17 +25052,23 @@ async function executeImportStep(context, worktree) {
|
|
|
24835
25052
|
}, context.configLoader, context.hledgerExecutor, inWorktree);
|
|
24836
25053
|
const importParsed = JSON.parse(importResult);
|
|
24837
25054
|
const message = importParsed.success ? `Imported ${importParsed.summary?.totalTransactions || 0} transactions` : `Import failed: ${importParsed.error || "Unknown error"}`;
|
|
25055
|
+
logger?.logStep("Import", importParsed.success ? "success" : "error", message);
|
|
24838
25056
|
context.result.steps.import = buildStepResult(importParsed.success, message, {
|
|
24839
25057
|
success: importParsed.success,
|
|
24840
25058
|
summary: importParsed.summary,
|
|
24841
25059
|
error: importParsed.error
|
|
24842
25060
|
});
|
|
24843
25061
|
if (!importParsed.success) {
|
|
25062
|
+
logger?.error("Import failed", new Error(importParsed.error || "Unknown error"));
|
|
25063
|
+
logger?.endSection();
|
|
24844
25064
|
context.result.error = `Import failed: ${importParsed.error || "Unknown error"}`;
|
|
24845
25065
|
throw new Error("Import failed");
|
|
24846
25066
|
}
|
|
25067
|
+
logger?.endSection();
|
|
24847
25068
|
}
|
|
24848
|
-
async function executeReconcileStep(context, worktree) {
|
|
25069
|
+
async function executeReconcileStep(context, worktree, logger) {
|
|
25070
|
+
logger?.startSection("Step 6: Reconcile Balance");
|
|
25071
|
+
logger?.logStep("Reconcile", "start");
|
|
24849
25072
|
const inWorktree = () => true;
|
|
24850
25073
|
const reconcileResult = await reconcileStatement(worktree.path, context.agent, {
|
|
24851
25074
|
provider: context.options.provider,
|
|
@@ -24855,6 +25078,11 @@ async function executeReconcileStep(context, worktree) {
|
|
|
24855
25078
|
}, context.configLoader, context.hledgerExecutor, inWorktree);
|
|
24856
25079
|
const reconcileParsed = JSON.parse(reconcileResult);
|
|
24857
25080
|
const message = reconcileParsed.success ? `Balance reconciled: ${reconcileParsed.actualBalance}` : `Balance mismatch: expected ${reconcileParsed.expectedBalance}, got ${reconcileParsed.actualBalance}`;
|
|
25081
|
+
logger?.logStep("Reconcile", reconcileParsed.success ? "success" : "error", message);
|
|
25082
|
+
if (reconcileParsed.success) {
|
|
25083
|
+
logger?.info(`Actual: ${reconcileParsed.actualBalance}`);
|
|
25084
|
+
logger?.info(`Expected: ${reconcileParsed.expectedBalance}`);
|
|
25085
|
+
}
|
|
24858
25086
|
context.result.steps.reconcile = buildStepResult(reconcileParsed.success, message, {
|
|
24859
25087
|
success: reconcileParsed.success,
|
|
24860
25088
|
actualBalance: reconcileParsed.actualBalance,
|
|
@@ -24863,29 +25091,40 @@ async function executeReconcileStep(context, worktree) {
|
|
|
24863
25091
|
error: reconcileParsed.error
|
|
24864
25092
|
});
|
|
24865
25093
|
if (!reconcileParsed.success) {
|
|
25094
|
+
logger?.error("Reconciliation failed", new Error(reconcileParsed.error || "Balance mismatch"));
|
|
25095
|
+
logger?.endSection();
|
|
24866
25096
|
context.result.error = `Reconciliation failed: ${reconcileParsed.error || "Balance mismatch"}`;
|
|
24867
25097
|
context.result.hint = "Check for missing transactions or incorrect rules";
|
|
24868
25098
|
throw new Error("Reconciliation failed");
|
|
24869
25099
|
}
|
|
25100
|
+
logger?.endSection();
|
|
24870
25101
|
}
|
|
24871
|
-
async function executeMergeStep(context, worktree) {
|
|
25102
|
+
async function executeMergeStep(context, worktree, logger) {
|
|
25103
|
+
logger?.startSection("Step 7: Merge to Main");
|
|
25104
|
+
logger?.logStep("Merge", "start");
|
|
24872
25105
|
const importDetails = context.result.steps.import?.details;
|
|
24873
25106
|
const reconcileDetails = context.result.steps.reconcile?.details;
|
|
24874
25107
|
if (!importDetails || !reconcileDetails) {
|
|
24875
25108
|
throw new Error("Import or reconcile step not completed before merge");
|
|
24876
25109
|
}
|
|
24877
25110
|
const commitInfo = {
|
|
24878
|
-
fromDate: reconcileDetails.metadata?.
|
|
24879
|
-
untilDate: reconcileDetails.metadata?.
|
|
25111
|
+
fromDate: reconcileDetails.metadata?.["from-date"],
|
|
25112
|
+
untilDate: reconcileDetails.metadata?.["until-date"]
|
|
24880
25113
|
};
|
|
24881
25114
|
const transactionCount = importDetails.summary?.totalTransactions || 0;
|
|
24882
25115
|
const commitMessage = buildCommitMessage(context.options.provider, context.options.currency, commitInfo.fromDate, commitInfo.untilDate, transactionCount);
|
|
24883
25116
|
try {
|
|
25117
|
+
logger?.info(`Commit message: "${commitMessage}"`);
|
|
24884
25118
|
mergeWorktree(worktree, commitMessage);
|
|
25119
|
+
logger?.logStep("Merge", "success", "Merged to main branch");
|
|
24885
25120
|
const mergeDetails = { commitMessage };
|
|
24886
25121
|
context.result.steps.merge = buildStepResult(true, `Merged to main: "${commitMessage}"`, mergeDetails);
|
|
24887
25122
|
cleanupIncomingFiles(worktree, context);
|
|
25123
|
+
logger?.endSection();
|
|
24888
25124
|
} catch (error45) {
|
|
25125
|
+
logger?.logStep("Merge", "error");
|
|
25126
|
+
logger?.error("Merge to main branch failed", error45);
|
|
25127
|
+
logger?.endSection();
|
|
24889
25128
|
const message = `Merge failed: ${error45 instanceof Error ? error45.message : String(error45)}`;
|
|
24890
25129
|
context.result.steps.merge = buildStepResult(false, message);
|
|
24891
25130
|
context.result.error = "Merge to main branch failed";
|
|
@@ -24903,6 +25142,13 @@ async function importPipeline(directory, agent, options, configLoader = loadImpo
|
|
|
24903
25142
|
if (restrictionError) {
|
|
24904
25143
|
return restrictionError;
|
|
24905
25144
|
}
|
|
25145
|
+
const logger = createImportLogger(directory, undefined, options.provider);
|
|
25146
|
+
logger.startSection("Import Pipeline", 1);
|
|
25147
|
+
logger.info(`Provider filter: ${options.provider || "all"}`);
|
|
25148
|
+
logger.info(`Currency filter: ${options.currency || "all"}`);
|
|
25149
|
+
logger.info(`Skip classify: ${options.skipClassify || false}`);
|
|
25150
|
+
logger.info(`Keep worktree on error: ${options.keepWorktreeOnError ?? true}`);
|
|
25151
|
+
logger.info("");
|
|
24906
25152
|
const result = {
|
|
24907
25153
|
success: false,
|
|
24908
25154
|
steps: {}
|
|
@@ -24917,33 +25163,47 @@ async function importPipeline(directory, agent, options, configLoader = loadImpo
|
|
|
24917
25163
|
};
|
|
24918
25164
|
try {
|
|
24919
25165
|
return await withWorktree(directory, async (worktree) => {
|
|
25166
|
+
logger.setContext("worktreeId", worktree.uuid);
|
|
25167
|
+
logger.setContext("worktreePath", worktree.path);
|
|
24920
25168
|
result.worktreeId = worktree.uuid;
|
|
24921
25169
|
result.steps.worktree = buildStepResult(true, `Created worktree at ${worktree.path}`, {
|
|
24922
25170
|
path: worktree.path,
|
|
24923
25171
|
branch: worktree.branch
|
|
24924
25172
|
});
|
|
25173
|
+
logger.startSection("Step 1: Sync Files");
|
|
25174
|
+
logger.logStep("Sync Files", "start");
|
|
24925
25175
|
try {
|
|
24926
25176
|
const config2 = configLoader(directory);
|
|
24927
25177
|
const syncResult = syncCSVFilesToWorktree(directory, worktree.path, config2.paths.import);
|
|
24928
25178
|
if (syncResult.synced.length === 0 && syncResult.errors.length === 0) {
|
|
25179
|
+
logger.logStep("Sync Files", "success", "No CSV files to sync");
|
|
24929
25180
|
result.steps.sync = buildStepResult(true, "No CSV files to sync", {
|
|
24930
25181
|
synced: []
|
|
24931
25182
|
});
|
|
24932
25183
|
} else if (syncResult.errors.length > 0) {
|
|
25184
|
+
logger.warn(`Synced ${syncResult.synced.length} file(s) with ${syncResult.errors.length} error(s)`);
|
|
24933
25185
|
result.steps.sync = buildStepResult(true, `Synced ${syncResult.synced.length} file(s) with ${syncResult.errors.length} error(s)`, { synced: syncResult.synced, errors: syncResult.errors });
|
|
24934
25186
|
} else {
|
|
25187
|
+
logger.logStep("Sync Files", "success", `Synced ${syncResult.synced.length} CSV file(s)`);
|
|
25188
|
+
for (const file2 of syncResult.synced) {
|
|
25189
|
+
logger.info(` - ${file2}`);
|
|
25190
|
+
}
|
|
24935
25191
|
result.steps.sync = buildStepResult(true, `Synced ${syncResult.synced.length} CSV file(s) to worktree`, { synced: syncResult.synced });
|
|
24936
25192
|
}
|
|
25193
|
+
logger.endSection();
|
|
24937
25194
|
} catch (error45) {
|
|
25195
|
+
logger.logStep("Sync Files", "error");
|
|
25196
|
+
logger.error("Failed to sync CSV files", error45);
|
|
25197
|
+
logger.endSection();
|
|
24938
25198
|
const errorMsg = error45 instanceof Error ? error45.message : String(error45);
|
|
24939
25199
|
result.steps.sync = buildStepResult(false, `Failed to sync CSV files: ${errorMsg}`, { synced: [], errors: [{ file: "unknown", error: errorMsg }] });
|
|
24940
25200
|
}
|
|
24941
25201
|
try {
|
|
24942
|
-
await executeClassifyStep(context, worktree);
|
|
24943
|
-
await executeAccountDeclarationsStep(context, worktree);
|
|
24944
|
-
await executeDryRunStep(context, worktree);
|
|
24945
|
-
await executeImportStep(context, worktree);
|
|
24946
|
-
await executeReconcileStep(context, worktree);
|
|
25202
|
+
await executeClassifyStep(context, worktree, logger);
|
|
25203
|
+
await executeAccountDeclarationsStep(context, worktree, logger);
|
|
25204
|
+
await executeDryRunStep(context, worktree, logger);
|
|
25205
|
+
await executeImportStep(context, worktree, logger);
|
|
25206
|
+
await executeReconcileStep(context, worktree, logger);
|
|
24947
25207
|
try {
|
|
24948
25208
|
const config2 = configLoader(directory);
|
|
24949
25209
|
const cleanupResult = cleanupProcessedCSVFiles(directory, config2.paths.import);
|
|
@@ -24968,7 +25228,7 @@ async function importPipeline(directory, agent, options, configLoader = loadImpo
|
|
|
24968
25228
|
}
|
|
24969
25229
|
});
|
|
24970
25230
|
}
|
|
24971
|
-
await executeMergeStep(context, worktree);
|
|
25231
|
+
await executeMergeStep(context, worktree, logger);
|
|
24972
25232
|
const existingCleanup = result.steps.cleanup;
|
|
24973
25233
|
if (existingCleanup) {
|
|
24974
25234
|
existingCleanup.message += ", worktree cleaned up";
|
|
@@ -24978,10 +25238,30 @@ async function importPipeline(directory, agent, options, configLoader = loadImpo
|
|
|
24978
25238
|
};
|
|
24979
25239
|
}
|
|
24980
25240
|
const transactionCount = context.result.steps.import?.details?.summary?.totalTransactions || 0;
|
|
25241
|
+
logger.startSection("Summary");
|
|
25242
|
+
logger.info(`\u2705 Import completed successfully`);
|
|
25243
|
+
logger.info(`Total transactions imported: ${transactionCount}`);
|
|
25244
|
+
if (context.result.steps.reconcile?.details?.actualBalance) {
|
|
25245
|
+
logger.info(`Balance reconciliation: \u2705 Matched (${context.result.steps.reconcile.details.actualBalance})`);
|
|
25246
|
+
}
|
|
25247
|
+
logger.info(`Log file: ${logger.getLogPath()}`);
|
|
25248
|
+
logger.endSection();
|
|
24981
25249
|
return buildSuccessResult4(result, `Successfully imported ${transactionCount} transaction(s)`);
|
|
24982
25250
|
} catch (error45) {
|
|
24983
|
-
result.steps.
|
|
24984
|
-
|
|
25251
|
+
const worktreePath = context.result.steps.worktree?.details?.path;
|
|
25252
|
+
const keepWorktree = options.keepWorktreeOnError ?? true;
|
|
25253
|
+
logger.error("Pipeline step failed", error45);
|
|
25254
|
+
if (keepWorktree && worktreePath) {
|
|
25255
|
+
logger.warn(`Worktree preserved at: ${worktreePath}`);
|
|
25256
|
+
logger.info(`To continue manually: cd ${worktreePath}`);
|
|
25257
|
+
logger.info(`To clean up: git worktree remove ${worktreePath}`);
|
|
25258
|
+
}
|
|
25259
|
+
logger.info(`Log file: ${logger.getLogPath()}`);
|
|
25260
|
+
result.steps.cleanup = buildStepResult(true, keepWorktree ? `Worktree preserved for debugging (CSV files preserved for retry)` : "Worktree cleaned up after failure (CSV files preserved for retry)", {
|
|
25261
|
+
cleanedAfterFailure: !keepWorktree,
|
|
25262
|
+
worktreePreserved: keepWorktree,
|
|
25263
|
+
worktreePath,
|
|
25264
|
+
preserveReason: keepWorktree ? "error occurred" : undefined,
|
|
24985
25265
|
csvCleanup: { deleted: [] }
|
|
24986
25266
|
});
|
|
24987
25267
|
if (error45 instanceof NoTransactionsError) {
|
|
@@ -24992,11 +25272,18 @@ async function importPipeline(directory, agent, options, configLoader = loadImpo
|
|
|
24992
25272
|
}
|
|
24993
25273
|
return buildErrorResult5(result, result.error, result.hint);
|
|
24994
25274
|
}
|
|
25275
|
+
}, {
|
|
25276
|
+
keepOnError: options.keepWorktreeOnError ?? true,
|
|
25277
|
+
logger
|
|
24995
25278
|
});
|
|
24996
25279
|
} catch (error45) {
|
|
25280
|
+
logger.error("Pipeline failed", error45);
|
|
24997
25281
|
result.steps.worktree = buildStepResult(false, `Failed to create worktree: ${error45 instanceof Error ? error45.message : String(error45)}`);
|
|
24998
25282
|
result.error = "Failed to create worktree";
|
|
24999
25283
|
return buildErrorResult5(result, result.error);
|
|
25284
|
+
} finally {
|
|
25285
|
+
logger.endSection();
|
|
25286
|
+
await logger.flush();
|
|
25000
25287
|
}
|
|
25001
25288
|
}
|
|
25002
25289
|
var import_pipeline_default = tool({
|
|
@@ -25011,25 +25298,40 @@ This tool orchestrates the full import workflow in an isolated git worktree:
|
|
|
25011
25298
|
4. **Import**: Imports transactions to the journal
|
|
25012
25299
|
5. **Reconcile**: Validates closing balance matches CSV metadata
|
|
25013
25300
|
6. **Merge**: Merges worktree to main with --no-ff
|
|
25014
|
-
7. **Cleanup**: Removes worktree
|
|
25301
|
+
7. **Cleanup**: Removes worktree (or preserves on error)
|
|
25015
25302
|
|
|
25016
25303
|
**Safety Features:**
|
|
25017
25304
|
- All changes happen in isolated worktree
|
|
25018
|
-
- If any step fails, worktree is
|
|
25305
|
+
- If any step fails, worktree is preserved by default for debugging
|
|
25019
25306
|
- Balance reconciliation ensures data integrity
|
|
25020
25307
|
- Atomic commit with merge --no-ff preserves history
|
|
25021
25308
|
|
|
25309
|
+
**Worktree Cleanup:**
|
|
25310
|
+
- On success: Worktree is always cleaned up
|
|
25311
|
+
- On error (default): Worktree is kept at /tmp/import-worktree-<uuid> for debugging
|
|
25312
|
+
- On error (--keepWorktreeOnError false): Worktree is removed (old behavior)
|
|
25313
|
+
- Manual cleanup: git worktree remove /tmp/import-worktree-<uuid>
|
|
25314
|
+
- Auto cleanup: System reboot (worktrees are in /tmp)
|
|
25315
|
+
|
|
25316
|
+
**Logging:**
|
|
25317
|
+
- All operations are logged to .memory/import-<timestamp>.md
|
|
25318
|
+
- Log includes full command output, timing, and error details
|
|
25319
|
+
- Log path is included in tool output for easy access
|
|
25320
|
+
- NO console output (avoids polluting OpenCode TUI)
|
|
25321
|
+
|
|
25022
25322
|
**Usage:**
|
|
25023
25323
|
- Basic: import-pipeline (processes all pending CSVs)
|
|
25024
25324
|
- Filtered: import-pipeline --provider ubs --currency chf
|
|
25025
25325
|
- With manual balance: import-pipeline --closingBalance "CHF 1234.56"
|
|
25026
|
-
- Skip classify: import-pipeline --skipClassify true
|
|
25326
|
+
- Skip classify: import-pipeline --skipClassify true
|
|
25327
|
+
- Always cleanup: import-pipeline --keepWorktreeOnError false`,
|
|
25027
25328
|
args: {
|
|
25028
25329
|
provider: tool.schema.string().optional().describe('Filter by provider (e.g., "ubs", "revolut")'),
|
|
25029
25330
|
currency: tool.schema.string().optional().describe('Filter by currency (e.g., "chf", "eur")'),
|
|
25030
25331
|
closingBalance: tool.schema.string().optional().describe("Manual closing balance override (if not in CSV metadata)"),
|
|
25031
25332
|
account: tool.schema.string().optional().describe("Manual account override (auto-detected from rules file if not provided)"),
|
|
25032
|
-
skipClassify: tool.schema.boolean().optional().describe("Skip the classify step (default: false)")
|
|
25333
|
+
skipClassify: tool.schema.boolean().optional().describe("Skip the classify step (default: false)"),
|
|
25334
|
+
keepWorktreeOnError: tool.schema.boolean().optional().describe("Keep worktree on error for debugging (default: true)")
|
|
25033
25335
|
},
|
|
25034
25336
|
async execute(params, context) {
|
|
25035
25337
|
const { directory, agent } = context;
|
|
@@ -25038,21 +25340,22 @@ This tool orchestrates the full import workflow in an isolated git worktree:
|
|
|
25038
25340
|
currency: params.currency,
|
|
25039
25341
|
closingBalance: params.closingBalance,
|
|
25040
25342
|
account: params.account,
|
|
25041
|
-
skipClassify: params.skipClassify
|
|
25343
|
+
skipClassify: params.skipClassify,
|
|
25344
|
+
keepWorktreeOnError: params.keepWorktreeOnError
|
|
25042
25345
|
});
|
|
25043
25346
|
}
|
|
25044
25347
|
});
|
|
25045
25348
|
// src/tools/init-directories.ts
|
|
25046
|
-
import * as
|
|
25047
|
-
import * as
|
|
25349
|
+
import * as fs15 from "fs";
|
|
25350
|
+
import * as path14 from "path";
|
|
25048
25351
|
async function initDirectories(directory) {
|
|
25049
25352
|
try {
|
|
25050
25353
|
const config2 = loadImportConfig(directory);
|
|
25051
25354
|
const directoriesCreated = [];
|
|
25052
25355
|
const gitkeepFiles = [];
|
|
25053
|
-
const importBase =
|
|
25054
|
-
if (!
|
|
25055
|
-
|
|
25356
|
+
const importBase = path14.join(directory, "import");
|
|
25357
|
+
if (!fs15.existsSync(importBase)) {
|
|
25358
|
+
fs15.mkdirSync(importBase, { recursive: true });
|
|
25056
25359
|
directoriesCreated.push("import");
|
|
25057
25360
|
}
|
|
25058
25361
|
const pathsToCreate = [
|
|
@@ -25062,20 +25365,20 @@ async function initDirectories(directory) {
|
|
|
25062
25365
|
{ key: "unrecognized", path: config2.paths.unrecognized }
|
|
25063
25366
|
];
|
|
25064
25367
|
for (const { path: dirPath } of pathsToCreate) {
|
|
25065
|
-
const fullPath =
|
|
25066
|
-
if (!
|
|
25067
|
-
|
|
25368
|
+
const fullPath = path14.join(directory, dirPath);
|
|
25369
|
+
if (!fs15.existsSync(fullPath)) {
|
|
25370
|
+
fs15.mkdirSync(fullPath, { recursive: true });
|
|
25068
25371
|
directoriesCreated.push(dirPath);
|
|
25069
25372
|
}
|
|
25070
|
-
const gitkeepPath =
|
|
25071
|
-
if (!
|
|
25072
|
-
|
|
25073
|
-
gitkeepFiles.push(
|
|
25373
|
+
const gitkeepPath = path14.join(fullPath, ".gitkeep");
|
|
25374
|
+
if (!fs15.existsSync(gitkeepPath)) {
|
|
25375
|
+
fs15.writeFileSync(gitkeepPath, "");
|
|
25376
|
+
gitkeepFiles.push(path14.join(dirPath, ".gitkeep"));
|
|
25074
25377
|
}
|
|
25075
25378
|
}
|
|
25076
|
-
const gitignorePath =
|
|
25379
|
+
const gitignorePath = path14.join(importBase, ".gitignore");
|
|
25077
25380
|
let gitignoreCreated = false;
|
|
25078
|
-
if (!
|
|
25381
|
+
if (!fs15.existsSync(gitignorePath)) {
|
|
25079
25382
|
const gitignoreContent = `# Ignore CSV/PDF files in temporary directories
|
|
25080
25383
|
/incoming/*.csv
|
|
25081
25384
|
/incoming/*.pdf
|
|
@@ -25093,7 +25396,7 @@ async function initDirectories(directory) {
|
|
|
25093
25396
|
.DS_Store
|
|
25094
25397
|
Thumbs.db
|
|
25095
25398
|
`;
|
|
25096
|
-
|
|
25399
|
+
fs15.writeFileSync(gitignorePath, gitignoreContent);
|
|
25097
25400
|
gitignoreCreated = true;
|
|
25098
25401
|
}
|
|
25099
25402
|
const parts = [];
|