@fuzzle/opencode-accountant 0.0.9-next.1 → 0.0.10
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 +45 -7
- package/agent/accountant.md +9 -25
- package/dist/index.js +367 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -97,6 +97,7 @@ paths:
|
|
|
97
97
|
pending: doc/agent/todo/import
|
|
98
98
|
done: doc/agent/done/import
|
|
99
99
|
unrecognized: statements/import/unrecognized
|
|
100
|
+
rules: ledger/rules
|
|
100
101
|
|
|
101
102
|
providers:
|
|
102
103
|
revolut:
|
|
@@ -121,9 +122,9 @@ providers:
|
|
|
121
122
|
currencyField: Currency
|
|
122
123
|
skipRows: 9
|
|
123
124
|
delimiter: ';'
|
|
124
|
-
renamePattern: 'transactions-ubs-{
|
|
125
|
+
renamePattern: 'transactions-ubs-{account-number}.csv'
|
|
125
126
|
metadata:
|
|
126
|
-
- field:
|
|
127
|
+
- field: account-number
|
|
127
128
|
row: 0
|
|
128
129
|
column: 1
|
|
129
130
|
normalize: spaces-to-dashes
|
|
@@ -143,6 +144,7 @@ providers:
|
|
|
143
144
|
| `pending` | Base path for classified files awaiting import |
|
|
144
145
|
| `done` | Base path for archived files after import |
|
|
145
146
|
| `unrecognized` | Directory for files that couldn't be classified |
|
|
147
|
+
| `rules` | Directory containing hledger `.rules` files |
|
|
146
148
|
|
|
147
149
|
**Provider Detection Rules:**
|
|
148
150
|
|
|
@@ -175,15 +177,18 @@ your-project/
|
|
|
175
177
|
├── config/
|
|
176
178
|
│ └── import/
|
|
177
179
|
│ └── providers.yaml
|
|
180
|
+
├── ledger/
|
|
181
|
+
│ └── rules/ # hledger rules files
|
|
182
|
+
│ └── <provider>-{account-number}.rules # {account-number} from metadata extraction
|
|
178
183
|
├── statements/
|
|
179
|
-
│ └── import/
|
|
180
|
-
│ └── unrecognized/
|
|
184
|
+
│ └── import/ # Drop CSV files here
|
|
185
|
+
│ └── unrecognized/ # Unclassified files moved here
|
|
181
186
|
└── doc/
|
|
182
187
|
└── agent/
|
|
183
188
|
├── todo/
|
|
184
189
|
│ └── import/
|
|
185
|
-
│ └── <provider>/
|
|
186
|
-
│ └── <currency>/
|
|
190
|
+
│ └── <provider>/ # e.g. revolut
|
|
191
|
+
│ └── <currency>/ # e.g. chf, eur, usd, btc
|
|
187
192
|
└── done/
|
|
188
193
|
└── import/
|
|
189
194
|
└── <provider>/
|
|
@@ -196,7 +201,40 @@ your-project/
|
|
|
196
201
|
2. Run `classify-statements` tool
|
|
197
202
|
3. Files are moved to `doc/agent/todo/import/<provider>/<currency>/`
|
|
198
203
|
4. Unrecognized files are moved to `statements/import/unrecognized/`
|
|
199
|
-
5.
|
|
204
|
+
5. Run `import-statements` with `checkOnly: true` to validate transactions
|
|
205
|
+
6. If unknown postings found: Add rules to the `.rules` file, repeat step 5
|
|
206
|
+
7. Once all transactions match: Run `import-statements` with `checkOnly: false`
|
|
207
|
+
8. Transactions are imported to journal, CSV files moved to `doc/agent/done/import/`
|
|
208
|
+
|
|
209
|
+
### Statement Import
|
|
210
|
+
|
|
211
|
+
The `import-statements` tool imports classified CSV statements into hledger using rules files. It validates transactions before import and identifies any that cannot be categorized.
|
|
212
|
+
|
|
213
|
+
#### Tool Arguments
|
|
214
|
+
|
|
215
|
+
| Argument | Type | Default | Description |
|
|
216
|
+
| ----------- | ------- | ------- | ------------------------------------------- |
|
|
217
|
+
| `provider` | string | - | Filter by provider (e.g., `revolut`, `ubs`) |
|
|
218
|
+
| `currency` | string | - | Filter by currency (e.g., `chf`, `eur`) |
|
|
219
|
+
| `checkOnly` | boolean | `true` | If true, only validate without importing |
|
|
220
|
+
|
|
221
|
+
#### Rules File Matching
|
|
222
|
+
|
|
223
|
+
The tool matches CSV files to their rules files by parsing the `source` directive in each `.rules` file. For example, if `ubs-account.rules` contains:
|
|
224
|
+
|
|
225
|
+
```
|
|
226
|
+
source ../../doc/agent/todo/import/ubs/chf/transactions.csv
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
The tool will use that rules file when processing `transactions.csv`.
|
|
230
|
+
|
|
231
|
+
See the hledger documentation for details on rules file format and syntax.
|
|
232
|
+
|
|
233
|
+
#### Unknown Postings
|
|
234
|
+
|
|
235
|
+
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 tool reports these so you can add appropriate rules.
|
|
236
|
+
|
|
237
|
+
For detailed output format examples, see [`docs/tools/import-statements.md`](docs/tools/import-statements.md).
|
|
200
238
|
|
|
201
239
|
## Development
|
|
202
240
|
|
package/agent/accountant.md
CHANGED
|
@@ -3,13 +3,13 @@ description: Specialized agent for hledger accounting tasks and transaction mana
|
|
|
3
3
|
prompt: You are an accounting specialist with expertise in hledger and double-entry bookkeeping. Your role is to help maintain accurate financial records following the user's conventions.
|
|
4
4
|
mode: subagent
|
|
5
5
|
temperature: 0.1
|
|
6
|
-
steps:
|
|
6
|
+
steps: 8
|
|
7
7
|
tools:
|
|
8
|
-
bash:
|
|
8
|
+
bash: true
|
|
9
9
|
edit: true
|
|
10
10
|
write: true
|
|
11
11
|
permission:
|
|
12
|
-
bash:
|
|
12
|
+
bash: allow
|
|
13
13
|
edit: allow
|
|
14
14
|
glob: allow
|
|
15
15
|
grep: allow
|
|
@@ -24,25 +24,19 @@ permission:
|
|
|
24
24
|
|
|
25
25
|
## Repository Structure
|
|
26
26
|
|
|
27
|
-
- `.hledger.journal` - Global hledger journal file
|
|
27
|
+
- `.hledger.journal` - Global hledger journal file
|
|
28
28
|
- `ledger/` - All ledger related files are stored here
|
|
29
29
|
- `ledger/currencies/` - Currency exchange rate files
|
|
30
30
|
- `ledger/YYYY.journal` - Annual hledger journal files
|
|
31
|
+
- `ledger/rules` - hledger rules files
|
|
31
32
|
- `statements/` - Bank and broker account statements
|
|
32
33
|
- `statements/import` - Upload folder for new statements to process
|
|
33
|
-
- `statements/provider/YYYY` -
|
|
34
|
-
- `doc/agent/todo/` -
|
|
34
|
+
- `statements/{provider}/YYYY` - Processed statements organized by source and year
|
|
35
|
+
- `doc/agent/todo/` - Agent's task work directory
|
|
35
36
|
- `doc/agent/done/` - Tasks completed by the agent
|
|
36
37
|
- `config/conventions/` - Accounting conventions
|
|
37
38
|
- `config/rules/` - import rules files
|
|
38
39
|
|
|
39
|
-
## System Environment
|
|
40
|
-
|
|
41
|
-
**Required for accounting tasks:**
|
|
42
|
-
|
|
43
|
-
- `pricehist` - Price data fetching
|
|
44
|
-
- `hledger`, `hledger-fmt` - Accounting operations
|
|
45
|
-
|
|
46
40
|
## Conventions & Workflow
|
|
47
41
|
|
|
48
42
|
All account conventions, conversion patterns (currency, crypto, equity postings), transaction status management, import
|
|
@@ -59,15 +53,5 @@ When working with accounting tasks:
|
|
|
59
53
|
1. **File organization** - Keep transactions in appropriate year journals
|
|
60
54
|
1. **Duplicate checking** - Take extra care to avoid duplicate transactions
|
|
61
55
|
1. **Unintended edits** - If a balance is off, check the journal for unintended edits
|
|
62
|
-
1. **Statement tracking** - Move processed statements to `statements/provider/YYYY`
|
|
63
|
-
|
|
64
|
-
## Common Tasks
|
|
65
|
-
|
|
66
|
-
- Adding new transactions from statements
|
|
67
|
-
- Processing crypto purchases and transfers
|
|
68
|
-
- Currency conversions between CHF/EUR/USD
|
|
69
|
-
- Validating journal integrity
|
|
70
|
-
- Generating balance reports
|
|
71
|
-
- Correcting malformed transactions
|
|
72
|
-
|
|
73
|
-
Focus on accuracy, precision, and strict adherence to the repository's accounting conventions.
|
|
56
|
+
1. **Statement tracking** - Move processed statements to `statements/{provider}/YYYY`
|
|
57
|
+
1. **Consistency** - Maintain consistent formatting and naming conventions across all files
|
package/dist/index.js
CHANGED
|
@@ -1341,7 +1341,7 @@ var require_papaparse = __commonJS((exports, module) => {
|
|
|
1341
1341
|
});
|
|
1342
1342
|
|
|
1343
1343
|
// src/index.ts
|
|
1344
|
-
import { dirname as
|
|
1344
|
+
import { dirname as dirname4, join as join7 } from "path";
|
|
1345
1345
|
import { fileURLToPath } from "url";
|
|
1346
1346
|
|
|
1347
1347
|
// src/utils/agentLoader.ts
|
|
@@ -16584,7 +16584,13 @@ import * as fs4 from "fs";
|
|
|
16584
16584
|
import * as fs3 from "fs";
|
|
16585
16585
|
import * as path3 from "path";
|
|
16586
16586
|
var CONFIG_FILE2 = "config/import/providers.yaml";
|
|
16587
|
-
var REQUIRED_PATH_FIELDS = [
|
|
16587
|
+
var REQUIRED_PATH_FIELDS = [
|
|
16588
|
+
"import",
|
|
16589
|
+
"pending",
|
|
16590
|
+
"done",
|
|
16591
|
+
"unrecognized",
|
|
16592
|
+
"rules"
|
|
16593
|
+
];
|
|
16588
16594
|
var REQUIRED_DETECTION_FIELDS = ["header", "currencyField"];
|
|
16589
16595
|
function validatePaths(paths) {
|
|
16590
16596
|
if (typeof paths !== "object" || paths === null) {
|
|
@@ -16600,7 +16606,8 @@ function validatePaths(paths) {
|
|
|
16600
16606
|
import: pathsObj.import,
|
|
16601
16607
|
pending: pathsObj.pending,
|
|
16602
16608
|
done: pathsObj.done,
|
|
16603
|
-
unrecognized: pathsObj.unrecognized
|
|
16609
|
+
unrecognized: pathsObj.unrecognized,
|
|
16610
|
+
rules: pathsObj.rules
|
|
16604
16611
|
};
|
|
16605
16612
|
}
|
|
16606
16613
|
function validateDetectionRule(providerName, index, rule) {
|
|
@@ -16968,15 +16975,369 @@ var classify_statements_default = tool({
|
|
|
16968
16975
|
return classifyStatementsCore(directory, agent);
|
|
16969
16976
|
}
|
|
16970
16977
|
});
|
|
16978
|
+
// src/tools/import-statements.ts
|
|
16979
|
+
import * as fs6 from "fs";
|
|
16980
|
+
import * as path6 from "path";
|
|
16981
|
+
|
|
16982
|
+
// src/utils/rulesMatcher.ts
|
|
16983
|
+
import * as fs5 from "fs";
|
|
16984
|
+
import * as path5 from "path";
|
|
16985
|
+
function parseSourceDirective(content) {
|
|
16986
|
+
const match = content.match(/^source\s+([^\n#]+)/m);
|
|
16987
|
+
if (!match) {
|
|
16988
|
+
return null;
|
|
16989
|
+
}
|
|
16990
|
+
return match[1].trim();
|
|
16991
|
+
}
|
|
16992
|
+
function resolveSourcePath(sourcePath, rulesFilePath) {
|
|
16993
|
+
if (path5.isAbsolute(sourcePath)) {
|
|
16994
|
+
return sourcePath;
|
|
16995
|
+
}
|
|
16996
|
+
const rulesDir = path5.dirname(rulesFilePath);
|
|
16997
|
+
return path5.resolve(rulesDir, sourcePath);
|
|
16998
|
+
}
|
|
16999
|
+
function loadRulesMapping(rulesDir) {
|
|
17000
|
+
const mapping = {};
|
|
17001
|
+
if (!fs5.existsSync(rulesDir)) {
|
|
17002
|
+
return mapping;
|
|
17003
|
+
}
|
|
17004
|
+
const files = fs5.readdirSync(rulesDir);
|
|
17005
|
+
for (const file2 of files) {
|
|
17006
|
+
if (!file2.endsWith(".rules")) {
|
|
17007
|
+
continue;
|
|
17008
|
+
}
|
|
17009
|
+
const rulesFilePath = path5.join(rulesDir, file2);
|
|
17010
|
+
const stat = fs5.statSync(rulesFilePath);
|
|
17011
|
+
if (!stat.isFile()) {
|
|
17012
|
+
continue;
|
|
17013
|
+
}
|
|
17014
|
+
const content = fs5.readFileSync(rulesFilePath, "utf-8");
|
|
17015
|
+
const sourcePath = parseSourceDirective(content);
|
|
17016
|
+
if (!sourcePath) {
|
|
17017
|
+
continue;
|
|
17018
|
+
}
|
|
17019
|
+
const absoluteCsvPath = resolveSourcePath(sourcePath, rulesFilePath);
|
|
17020
|
+
mapping[absoluteCsvPath] = rulesFilePath;
|
|
17021
|
+
}
|
|
17022
|
+
return mapping;
|
|
17023
|
+
}
|
|
17024
|
+
function findRulesForCsv(csvPath, mapping) {
|
|
17025
|
+
if (mapping[csvPath]) {
|
|
17026
|
+
return mapping[csvPath];
|
|
17027
|
+
}
|
|
17028
|
+
const normalizedCsvPath = path5.normalize(csvPath);
|
|
17029
|
+
for (const [mappedCsv, rulesFile] of Object.entries(mapping)) {
|
|
17030
|
+
if (path5.normalize(mappedCsv) === normalizedCsvPath) {
|
|
17031
|
+
return rulesFile;
|
|
17032
|
+
}
|
|
17033
|
+
}
|
|
17034
|
+
return null;
|
|
17035
|
+
}
|
|
17036
|
+
|
|
17037
|
+
// src/utils/hledgerExecutor.ts
|
|
17038
|
+
var {$: $2 } = globalThis.Bun;
|
|
17039
|
+
async function defaultHledgerExecutor(cmdArgs) {
|
|
17040
|
+
try {
|
|
17041
|
+
const result = await $2`hledger ${cmdArgs}`.quiet().nothrow();
|
|
17042
|
+
return {
|
|
17043
|
+
stdout: result.stdout.toString(),
|
|
17044
|
+
stderr: result.stderr.toString(),
|
|
17045
|
+
exitCode: result.exitCode
|
|
17046
|
+
};
|
|
17047
|
+
} catch (error45) {
|
|
17048
|
+
return {
|
|
17049
|
+
stdout: "",
|
|
17050
|
+
stderr: error45 instanceof Error ? error45.message : String(error45),
|
|
17051
|
+
exitCode: 1
|
|
17052
|
+
};
|
|
17053
|
+
}
|
|
17054
|
+
}
|
|
17055
|
+
function parseUnknownPostings(hledgerOutput) {
|
|
17056
|
+
const unknownPostings = [];
|
|
17057
|
+
const lines = hledgerOutput.split(`
|
|
17058
|
+
`);
|
|
17059
|
+
let currentDate = "";
|
|
17060
|
+
let currentDescription = "";
|
|
17061
|
+
for (const line of lines) {
|
|
17062
|
+
const headerMatch = line.match(/^(\d{4}-\d{2}-\d{2})\s+(.+)$/);
|
|
17063
|
+
if (headerMatch) {
|
|
17064
|
+
currentDate = headerMatch[1];
|
|
17065
|
+
currentDescription = headerMatch[2].trim();
|
|
17066
|
+
continue;
|
|
17067
|
+
}
|
|
17068
|
+
const postingMatch = line.match(/^\s+(income:unknown|expenses:unknown)\s+([^\s]+(?:\s+[^\s=]+)?)\s*(?:=\s*(.+))?$/);
|
|
17069
|
+
if (postingMatch && currentDate) {
|
|
17070
|
+
unknownPostings.push({
|
|
17071
|
+
date: currentDate,
|
|
17072
|
+
description: currentDescription,
|
|
17073
|
+
amount: postingMatch[2].trim(),
|
|
17074
|
+
account: postingMatch[1],
|
|
17075
|
+
balance: postingMatch[3]?.trim()
|
|
17076
|
+
});
|
|
17077
|
+
}
|
|
17078
|
+
}
|
|
17079
|
+
return unknownPostings;
|
|
17080
|
+
}
|
|
17081
|
+
function countTransactions(hledgerOutput) {
|
|
17082
|
+
const lines = hledgerOutput.split(`
|
|
17083
|
+
`);
|
|
17084
|
+
let count = 0;
|
|
17085
|
+
for (const line of lines) {
|
|
17086
|
+
if (/^\d{4}-\d{2}-\d{2}\s+/.test(line)) {
|
|
17087
|
+
count++;
|
|
17088
|
+
}
|
|
17089
|
+
}
|
|
17090
|
+
return count;
|
|
17091
|
+
}
|
|
17092
|
+
|
|
17093
|
+
// src/tools/import-statements.ts
|
|
17094
|
+
function findPendingCsvFiles(pendingDir, provider, currency) {
|
|
17095
|
+
const csvFiles = [];
|
|
17096
|
+
if (!fs6.existsSync(pendingDir)) {
|
|
17097
|
+
return csvFiles;
|
|
17098
|
+
}
|
|
17099
|
+
let searchPath = pendingDir;
|
|
17100
|
+
if (provider) {
|
|
17101
|
+
searchPath = path6.join(searchPath, provider);
|
|
17102
|
+
if (currency) {
|
|
17103
|
+
searchPath = path6.join(searchPath, currency);
|
|
17104
|
+
}
|
|
17105
|
+
}
|
|
17106
|
+
if (!fs6.existsSync(searchPath)) {
|
|
17107
|
+
return csvFiles;
|
|
17108
|
+
}
|
|
17109
|
+
function scanDirectory(directory) {
|
|
17110
|
+
const entries = fs6.readdirSync(directory, { withFileTypes: true });
|
|
17111
|
+
for (const entry of entries) {
|
|
17112
|
+
const fullPath = path6.join(directory, entry.name);
|
|
17113
|
+
if (entry.isDirectory()) {
|
|
17114
|
+
scanDirectory(fullPath);
|
|
17115
|
+
} else if (entry.isFile() && entry.name.endsWith(".csv")) {
|
|
17116
|
+
csvFiles.push(fullPath);
|
|
17117
|
+
}
|
|
17118
|
+
}
|
|
17119
|
+
}
|
|
17120
|
+
scanDirectory(searchPath);
|
|
17121
|
+
return csvFiles.sort();
|
|
17122
|
+
}
|
|
17123
|
+
async function importStatementsCore(directory, agent, options, configLoader = loadImportConfig, hledgerExecutor = defaultHledgerExecutor) {
|
|
17124
|
+
if (agent !== "accountant") {
|
|
17125
|
+
return JSON.stringify({
|
|
17126
|
+
success: false,
|
|
17127
|
+
error: `This tool is restricted to the accountant agent only. Called by: ${agent || "main assistant"}`,
|
|
17128
|
+
hint: "Use: Task(subagent_type='accountant', prompt='import statements')"
|
|
17129
|
+
});
|
|
17130
|
+
}
|
|
17131
|
+
let config2;
|
|
17132
|
+
try {
|
|
17133
|
+
config2 = configLoader(directory);
|
|
17134
|
+
} catch (error45) {
|
|
17135
|
+
return JSON.stringify({
|
|
17136
|
+
success: false,
|
|
17137
|
+
error: `Failed to load configuration: ${error45 instanceof Error ? error45.message : String(error45)}`,
|
|
17138
|
+
hint: 'Ensure config/import/providers.yaml exists with required paths including "rules"'
|
|
17139
|
+
});
|
|
17140
|
+
}
|
|
17141
|
+
const pendingDir = path6.join(directory, config2.paths.pending);
|
|
17142
|
+
const rulesDir = path6.join(directory, config2.paths.rules);
|
|
17143
|
+
const doneDir = path6.join(directory, config2.paths.done);
|
|
17144
|
+
const rulesMapping = loadRulesMapping(rulesDir);
|
|
17145
|
+
const csvFiles = findPendingCsvFiles(pendingDir, options.provider, options.currency);
|
|
17146
|
+
if (csvFiles.length === 0) {
|
|
17147
|
+
return JSON.stringify({
|
|
17148
|
+
success: true,
|
|
17149
|
+
files: [],
|
|
17150
|
+
summary: {
|
|
17151
|
+
filesProcessed: 0,
|
|
17152
|
+
filesWithErrors: 0,
|
|
17153
|
+
filesWithoutRules: 0,
|
|
17154
|
+
totalTransactions: 0,
|
|
17155
|
+
matched: 0,
|
|
17156
|
+
unknown: 0
|
|
17157
|
+
},
|
|
17158
|
+
message: "No CSV files found to process"
|
|
17159
|
+
});
|
|
17160
|
+
}
|
|
17161
|
+
const fileResults = [];
|
|
17162
|
+
let totalTransactions = 0;
|
|
17163
|
+
let totalMatched = 0;
|
|
17164
|
+
let totalUnknown = 0;
|
|
17165
|
+
let filesWithErrors = 0;
|
|
17166
|
+
let filesWithoutRules = 0;
|
|
17167
|
+
for (const csvFile of csvFiles) {
|
|
17168
|
+
const rulesFile = findRulesForCsv(csvFile, rulesMapping);
|
|
17169
|
+
if (!rulesFile) {
|
|
17170
|
+
filesWithoutRules++;
|
|
17171
|
+
fileResults.push({
|
|
17172
|
+
csv: path6.relative(directory, csvFile),
|
|
17173
|
+
rulesFile: null,
|
|
17174
|
+
totalTransactions: 0,
|
|
17175
|
+
matchedTransactions: 0,
|
|
17176
|
+
unknownPostings: [],
|
|
17177
|
+
error: "No matching rules file found"
|
|
17178
|
+
});
|
|
17179
|
+
continue;
|
|
17180
|
+
}
|
|
17181
|
+
const result = await hledgerExecutor(["print", "-f", csvFile, "--rules-file", rulesFile]);
|
|
17182
|
+
if (result.exitCode !== 0) {
|
|
17183
|
+
filesWithErrors++;
|
|
17184
|
+
fileResults.push({
|
|
17185
|
+
csv: path6.relative(directory, csvFile),
|
|
17186
|
+
rulesFile: path6.relative(directory, rulesFile),
|
|
17187
|
+
totalTransactions: 0,
|
|
17188
|
+
matchedTransactions: 0,
|
|
17189
|
+
unknownPostings: [],
|
|
17190
|
+
error: `hledger error: ${result.stderr.trim() || "Unknown error"}`
|
|
17191
|
+
});
|
|
17192
|
+
continue;
|
|
17193
|
+
}
|
|
17194
|
+
const unknownPostings = parseUnknownPostings(result.stdout);
|
|
17195
|
+
const transactionCount = countTransactions(result.stdout);
|
|
17196
|
+
const matchedCount = transactionCount - unknownPostings.length;
|
|
17197
|
+
totalTransactions += transactionCount;
|
|
17198
|
+
totalMatched += matchedCount;
|
|
17199
|
+
totalUnknown += unknownPostings.length;
|
|
17200
|
+
fileResults.push({
|
|
17201
|
+
csv: path6.relative(directory, csvFile),
|
|
17202
|
+
rulesFile: path6.relative(directory, rulesFile),
|
|
17203
|
+
totalTransactions: transactionCount,
|
|
17204
|
+
matchedTransactions: matchedCount,
|
|
17205
|
+
unknownPostings
|
|
17206
|
+
});
|
|
17207
|
+
}
|
|
17208
|
+
const hasUnknowns = totalUnknown > 0;
|
|
17209
|
+
const hasErrors = filesWithErrors > 0 || filesWithoutRules > 0;
|
|
17210
|
+
if (options.checkOnly !== false) {
|
|
17211
|
+
const result = {
|
|
17212
|
+
success: !hasUnknowns && !hasErrors,
|
|
17213
|
+
files: fileResults,
|
|
17214
|
+
summary: {
|
|
17215
|
+
filesProcessed: csvFiles.length,
|
|
17216
|
+
filesWithErrors,
|
|
17217
|
+
filesWithoutRules,
|
|
17218
|
+
totalTransactions,
|
|
17219
|
+
matched: totalMatched,
|
|
17220
|
+
unknown: totalUnknown
|
|
17221
|
+
}
|
|
17222
|
+
};
|
|
17223
|
+
if (hasUnknowns) {
|
|
17224
|
+
result.message = `Found ${totalUnknown} transaction(s) with unknown accounts. Add rules to categorize them.`;
|
|
17225
|
+
} else if (hasErrors) {
|
|
17226
|
+
result.message = `Some files had errors. Check the file results for details.`;
|
|
17227
|
+
} else {
|
|
17228
|
+
result.message = "All transactions matched. Ready to import with checkOnly: false";
|
|
17229
|
+
}
|
|
17230
|
+
return JSON.stringify(result);
|
|
17231
|
+
}
|
|
17232
|
+
if (hasUnknowns || hasErrors) {
|
|
17233
|
+
return JSON.stringify({
|
|
17234
|
+
success: false,
|
|
17235
|
+
files: fileResults,
|
|
17236
|
+
summary: {
|
|
17237
|
+
filesProcessed: csvFiles.length,
|
|
17238
|
+
filesWithErrors,
|
|
17239
|
+
filesWithoutRules,
|
|
17240
|
+
totalTransactions,
|
|
17241
|
+
matched: totalMatched,
|
|
17242
|
+
unknown: totalUnknown
|
|
17243
|
+
},
|
|
17244
|
+
error: "Cannot import: some transactions have unknown accounts or files have errors",
|
|
17245
|
+
hint: "Run with checkOnly: true to see details, then add missing rules"
|
|
17246
|
+
});
|
|
17247
|
+
}
|
|
17248
|
+
const importedFiles = [];
|
|
17249
|
+
for (const csvFile of csvFiles) {
|
|
17250
|
+
const rulesFile = findRulesForCsv(csvFile, rulesMapping);
|
|
17251
|
+
if (!rulesFile)
|
|
17252
|
+
continue;
|
|
17253
|
+
const result = await hledgerExecutor(["import", csvFile, "--rules-file", rulesFile]);
|
|
17254
|
+
if (result.exitCode !== 0) {
|
|
17255
|
+
return JSON.stringify({
|
|
17256
|
+
success: false,
|
|
17257
|
+
files: fileResults,
|
|
17258
|
+
summary: {
|
|
17259
|
+
filesProcessed: csvFiles.length,
|
|
17260
|
+
filesWithErrors: 1,
|
|
17261
|
+
filesWithoutRules,
|
|
17262
|
+
totalTransactions,
|
|
17263
|
+
matched: totalMatched,
|
|
17264
|
+
unknown: totalUnknown
|
|
17265
|
+
},
|
|
17266
|
+
error: `Import failed for ${path6.relative(directory, csvFile)}: ${result.stderr.trim()}`
|
|
17267
|
+
});
|
|
17268
|
+
}
|
|
17269
|
+
importedFiles.push(csvFile);
|
|
17270
|
+
}
|
|
17271
|
+
for (const csvFile of importedFiles) {
|
|
17272
|
+
const relativePath = path6.relative(pendingDir, csvFile);
|
|
17273
|
+
const destPath = path6.join(doneDir, relativePath);
|
|
17274
|
+
const destDir = path6.dirname(destPath);
|
|
17275
|
+
if (!fs6.existsSync(destDir)) {
|
|
17276
|
+
fs6.mkdirSync(destDir, { recursive: true });
|
|
17277
|
+
}
|
|
17278
|
+
fs6.renameSync(csvFile, destPath);
|
|
17279
|
+
}
|
|
17280
|
+
return JSON.stringify({
|
|
17281
|
+
success: true,
|
|
17282
|
+
files: fileResults.map((f) => ({
|
|
17283
|
+
...f,
|
|
17284
|
+
imported: true
|
|
17285
|
+
})),
|
|
17286
|
+
summary: {
|
|
17287
|
+
filesProcessed: csvFiles.length,
|
|
17288
|
+
filesWithErrors: 0,
|
|
17289
|
+
filesWithoutRules: 0,
|
|
17290
|
+
totalTransactions,
|
|
17291
|
+
matched: totalMatched,
|
|
17292
|
+
unknown: 0
|
|
17293
|
+
},
|
|
17294
|
+
message: `Successfully imported ${totalTransactions} transaction(s) from ${importedFiles.length} file(s)`
|
|
17295
|
+
});
|
|
17296
|
+
}
|
|
17297
|
+
var import_statements_default = tool({
|
|
17298
|
+
description: `Import classified bank statement CSVs into hledger using rules files.
|
|
17299
|
+
|
|
17300
|
+
This tool processes CSV files in the pending import directory and uses hledger's CSV import capabilities with matching rules files.
|
|
17301
|
+
|
|
17302
|
+
**Check Mode (checkOnly: true, default):**
|
|
17303
|
+
- Runs hledger print to preview transactions
|
|
17304
|
+
- Identifies transactions with 'income:unknown' or 'expenses:unknown' accounts
|
|
17305
|
+
- These indicate missing rules that need to be added
|
|
17306
|
+
|
|
17307
|
+
**Import Mode (checkOnly: false):**
|
|
17308
|
+
- First validates all transactions have known accounts
|
|
17309
|
+
- If any unknowns exist, aborts and reports them
|
|
17310
|
+
- If all clean, imports transactions and moves CSVs to done directory
|
|
17311
|
+
|
|
17312
|
+
**Workflow:**
|
|
17313
|
+
1. Run with checkOnly: true (or no args)
|
|
17314
|
+
2. If unknowns found, add rules to the appropriate .rules file
|
|
17315
|
+
3. Repeat until no unknowns
|
|
17316
|
+
4. Run with checkOnly: false to import`,
|
|
17317
|
+
args: {
|
|
17318
|
+
provider: tool.schema.string().optional().describe('Filter by provider (e.g., "revolut", "ubs"). If omitted, process all providers.'),
|
|
17319
|
+
currency: tool.schema.string().optional().describe('Filter by currency (e.g., "chf", "eur"). If omitted, process all currencies for the provider.'),
|
|
17320
|
+
checkOnly: tool.schema.boolean().optional().describe("If true (default), only check for unknown accounts without importing. Set to false to perform actual import.")
|
|
17321
|
+
},
|
|
17322
|
+
async execute(params, context) {
|
|
17323
|
+
const { directory, agent } = context;
|
|
17324
|
+
return importStatementsCore(directory, agent, {
|
|
17325
|
+
provider: params.provider,
|
|
17326
|
+
currency: params.currency,
|
|
17327
|
+
checkOnly: params.checkOnly
|
|
17328
|
+
});
|
|
17329
|
+
}
|
|
17330
|
+
});
|
|
16971
17331
|
// src/index.ts
|
|
16972
|
-
var __dirname2 =
|
|
16973
|
-
var AGENT_FILE =
|
|
17332
|
+
var __dirname2 = dirname4(fileURLToPath(import.meta.url));
|
|
17333
|
+
var AGENT_FILE = join7(__dirname2, "..", "agent", "accountant.md");
|
|
16974
17334
|
var AccountantPlugin = async () => {
|
|
16975
17335
|
const agent = loadAgent(AGENT_FILE);
|
|
16976
17336
|
return {
|
|
16977
17337
|
tool: {
|
|
16978
17338
|
"update-prices": update_prices_default,
|
|
16979
|
-
"classify-statements": classify_statements_default
|
|
17339
|
+
"classify-statements": classify_statements_default,
|
|
17340
|
+
"import-statements": import_statements_default
|
|
16980
17341
|
},
|
|
16981
17342
|
config: async (config2) => {
|
|
16982
17343
|
if (agent) {
|