@fuzzle/opencode-accountant 0.0.17 → 0.1.0-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 CHANGED
@@ -193,26 +193,33 @@ your-project/
193
193
 
194
194
  #### Workflow
195
195
 
196
+ The `import-pipeline` tool provides an atomic, safe import workflow using git worktrees:
197
+
196
198
  1. Drop CSV files into `{paths.import}` (default: `import/incoming/`)
197
- 2. Run `classify-statements` tool
198
- 3. Files are moved to `{paths.pending}/<provider>/<currency>/` (default: `import/pending/`)
199
- 4. Unrecognized files are moved to `{paths.unrecognized}/` (default: `import/unrecognized/`)
200
- 5. Run `import-statements` with `checkOnly: true` to validate transactions
201
- 6. If unknown postings found: Add rules to the `.rules` file, repeat step 5
202
- 7. Once all transactions match: Run `import-statements` with `checkOnly: false`
203
- 8. Transactions are imported to journal, CSV files moved to `{paths.done}/<provider>/<currency>/` (default: `import/done/`)
199
+ 2. Run `import-pipeline` tool with optional provider/currency filters
200
+ 3. The tool automatically:
201
+ - Creates an isolated git worktree
202
+ - Classifies CSV files by provider/currency
203
+ - Validates all transactions have matching rules
204
+ - Imports transactions to the appropriate year journal
205
+ - Reconciles closing balance (if available in CSV metadata)
206
+ - Merges changes back to main branch with `--no-ff`
207
+ - Cleans up the worktree
208
+ 4. If any step fails, the worktree is discarded and main branch remains untouched
204
209
 
205
210
  ### Statement Import
206
211
 
207
- 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
+ The `import-pipeline` tool is the single entry point for importing bank statements. It orchestrates classification, validation, import, and reconciliation in an atomic operation.
208
213
 
209
214
  #### Tool Arguments
210
215
 
211
- | Argument | Type | Default | Description |
212
- | ----------- | ------- | ------- | ------------------------------------------- |
213
- | `provider` | string | - | Filter by provider (e.g., `revolut`, `ubs`) |
214
- | `currency` | string | - | Filter by currency (e.g., `chf`, `eur`) |
215
- | `checkOnly` | boolean | `true` | If true, only validate without importing |
216
+ | Argument | Type | Default | Description |
217
+ | ---------------- | ------- | ------- | ------------------------------------------------------ |
218
+ | `provider` | string | - | Filter by provider (e.g., `revolut`, `ubs`) |
219
+ | `currency` | string | - | Filter by currency (e.g., `chf`, `eur`) |
220
+ | `skipClassify` | boolean | `false` | Skip classification step (if files already classified) |
221
+ | `closingBalance` | string | - | Manual closing balance for reconciliation |
222
+ | `account` | string | - | Manual account override (auto-detected from rules) |
216
223
 
217
224
  #### Rules File Matching
218
225
 
@@ -230,9 +237,26 @@ See the hledger documentation for details on rules file format and syntax.
230
237
 
231
238
  #### Unknown Postings
232
239
 
233
- 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.
240
+ 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.
241
+
242
+ #### Closing Balance Reconciliation
243
+
244
+ 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`:
245
+
246
+ ```yaml
247
+ metadata:
248
+ - field: closing_balance
249
+ row: 5
250
+ column: 1
251
+ - field: from_date
252
+ row: 2
253
+ column: 1
254
+ - field: until_date
255
+ row: 3
256
+ column: 1
257
+ ```
234
258
 
235
- For detailed output format examples, see [`docs/tools/import-statements.md`](docs/tools/import-statements.md).
259
+ For providers without closing balance in metadata (e.g., Revolut), provide it manually via the `closingBalance` argument.
236
260
 
237
261
  ## Development
238
262
 
@@ -8,7 +8,7 @@ tools:
8
8
  bash: true
9
9
  edit: true
10
10
  write: true
11
- # MCP tools available: classify-statements, import-statements, update-prices
11
+ # MCP tools available: import-pipeline, update-prices
12
12
  permission:
13
13
  bash: allow
14
14
  edit: allow
@@ -56,11 +56,10 @@ When working with accounting tasks:
56
56
 
57
57
  You have access to specialized MCP tools that MUST be used for their designated tasks. Do NOT attempt to replicate their functionality with bash commands, direct hledger CLI calls, or manual file edits.
58
58
 
59
- | Tool | Use For | NEVER Do Instead |
60
- | --------------------- | ---------------------------------- | ------------------------------------------ |
61
- | `classify-statements` | Organizing incoming CSV files | Manual file moves or bash `mv` commands |
62
- | `import-statements` | Importing transactions to journals | `hledger import`, manual journal edits |
63
- | `update-prices` | Fetching exchange rates | `curl` to price APIs, manual price entries |
59
+ | Tool | Use For | NEVER Do Instead |
60
+ | ----------------- | ---------------------------------------------------- | --------------------------------------------------------- |
61
+ | `import-pipeline` | Full import workflow (classify → import → reconcile) | Manual file moves, `hledger import`, manual journal edits |
62
+ | `update-prices` | Fetching exchange rates | `curl` to price APIs, manual price entries |
64
63
 
65
64
  These tools handle validation, deduplication, error checking, and file organization automatically. Bypassing them risks data corruption, duplicate transactions, and inconsistent state.
66
65
 
@@ -74,26 +73,31 @@ Bash is allowed ONLY for:
74
73
 
75
74
  Bash is FORBIDDEN for:
76
75
 
77
- - `hledger import` - use `import-statements` tool instead
78
- - Moving/copying CSV files - use `classify-statements` tool instead
76
+ - `hledger import` - use `import-pipeline` tool instead
77
+ - Moving/copying CSV files - use `import-pipeline` tool instead
79
78
  - Editing journal files directly - use `edit` tool only for rules files
80
79
  - Fetching prices - use `update-prices` tool instead
81
80
 
82
81
  ## Statement Import Workflow
83
82
 
84
- **IMPORTANT:** You MUST use the MCP tools below for statement imports. Do NOT edit journals manually, run `hledger import` directly, or move files with bash commands. The workflow:
83
+ **IMPORTANT:** You MUST use `import-pipeline` for statement imports. Do NOT edit journals manually, run `hledger import` directly, or move files with bash commands.
84
+
85
+ The `import-pipeline` tool provides an **atomic, safe workflow** using git worktrees:
85
86
 
86
87
  1. **Prepare**: Drop CSV files into `{paths.import}` (configured in `config/import/providers.yaml`, default: `import/incoming`)
87
- 2. **Classify**: Run `classify-statements` tool to organize files by provider/currency
88
- - Files moved to `{paths.pending}/<provider>/<currency>/`
89
- 3. **Validate (check mode)**: Run `import-statements(checkOnly: true)` to validate transactions
90
- 4. **Handle unknowns**: If unknown postings found:
91
- - Tool returns full CSV row data for each unknown posting
92
- - Analyze the CSV row data to understand the transaction
93
- - Create or update rules file with `if` directives to match the transaction
94
- - Repeat step 3 until all postings are matched
95
- 5. **Import**: Once all transactions have matching rules, run `import-statements(checkOnly: false)`
96
- 6. **Complete**: Transactions imported to journal, CSVs moved to `{paths.done}/<provider>/<currency>/`
88
+ 2. **Run Pipeline**: Execute `import-pipeline` (optionally filter by `provider` and `currency`)
89
+ 3. **Automatic Processing**: The tool creates an isolated git worktree and:
90
+ - Classifies CSV files by provider/currency
91
+ - Validates all transactions have matching rules
92
+ - Imports transactions to the appropriate year journal
93
+ - Reconciles closing balance (if available in CSV metadata)
94
+ - Merges changes back to main branch with `--no-ff`
95
+ - Cleans up the worktree
96
+ 4. **Handle Failures**: If any step fails (e.g., unknown postings found):
97
+ - Worktree is discarded, main branch remains untouched
98
+ - Review error output for unknown postings with full CSV row data
99
+ - Update rules file with `if` directives to match the transaction
100
+ - Re-run `import-pipeline`
97
101
 
98
102
  ### Rules Files
99
103
 
@@ -106,51 +110,51 @@ Bash is FORBIDDEN for:
106
110
 
107
111
  The following are MCP tools available to you. Always call these tools directly - do not attempt to replicate their behavior with shell commands.
108
112
 
109
- ### classify-statements
110
-
111
- **Purpose:** Organizes CSV files by auto-detecting provider and currency.
112
-
113
- **Usage:** `classify-statements()` (no arguments)
114
-
115
- **Behavior:**
116
-
117
- - Scans `{paths.import}` for CSV files
118
- - Detects provider using header matching + filename patterns
119
- - Moves classified files to `{paths.pending}/<provider>/<currency>/`
120
- - Moves unrecognized files to `{paths.unrecognized}/`
121
- - Aborts if any file collision detected (no partial moves)
113
+ ### import-pipeline
122
114
 
123
- **Output:** Returns classified/unrecognized file lists with target paths
124
-
125
- **Common issues:**
115
+ **Purpose:** Atomic import workflow that classifies, validates, imports, and reconciles bank statements.
126
116
 
127
- - Unrecognized files → Add provider config to `config/import/providers.yaml`
128
- - Collisions → Move/rename existing pending files before re-running
117
+ **Usage:**
129
118
 
130
- ---
119
+ - Basic: `import-pipeline()`
120
+ - Filtered: `import-pipeline(provider: "ubs", currency: "chf")`
121
+ - With manual closing balance: `import-pipeline(provider: "revolut", closingBalance: "CHF 1234.56")`
122
+ - Skip classification: `import-pipeline(skipClassify: true)` (if files already classified)
131
123
 
132
- ### import-statements
124
+ **Arguments:**
133
125
 
134
- **Purpose:** Imports classified CSV transactions into hledger journals.
126
+ | Argument | Type | Default | Description |
127
+ | ---------------- | ------- | ------- | -------------------------------------------------- |
128
+ | `provider` | string | - | Filter by provider (e.g., `revolut`, `ubs`) |
129
+ | `currency` | string | - | Filter by currency (e.g., `chf`, `eur`) |
130
+ | `skipClassify` | boolean | `false` | Skip classification step |
131
+ | `closingBalance` | string | - | Manual closing balance for reconciliation |
132
+ | `account` | string | - | Manual account override (auto-detected from rules) |
135
133
 
136
- **Usage:**
134
+ **Behavior:**
137
135
 
138
- - Check mode (default): `import-statements(checkOnly: true)` or `import-statements()`
139
- - Import mode: `import-statements(checkOnly: false)`
136
+ 1. Creates isolated git worktree
137
+ 2. Classifies CSV files (unless `skipClassify: true`)
138
+ 3. Validates all transactions have matching rules (dry run)
139
+ 4. Imports transactions to year journal
140
+ 5. Reconciles closing balance against CSV metadata or manual value
141
+ 6. Merges to main with `--no-ff` commit
142
+ 7. Cleans up worktree
140
143
 
141
- **Behavior:**
144
+ **Output:** Returns step-by-step results with success/failure for each phase
142
145
 
143
- - Processes CSV files in `{paths.pending}/<provider>/<currency>/`
144
- - Matches each CSV to rules file via `source` directive
145
- - Check mode: Validates transactions, reports unknown postings with full CSV row data
146
- - Import mode: Only proceeds if ALL transactions have known accounts, moves CSVs to `{paths.done}/`
146
+ **On Failure:**
147
147
 
148
- **Output:** Returns per-file results with transaction counts and unknown postings (if any)
148
+ - Worktree is discarded automatically
149
+ - Main branch remains untouched
150
+ - Error details include unknown postings with full CSV row data
151
+ - Fix rules and re-run the pipeline
149
152
 
150
- **Required for import:**
153
+ **Common issues:**
151
154
 
152
- - All transactions must have matching rules (no `income:unknown` or `expenses:unknown`)
153
- - Each CSV must have a corresponding `.rules` file in `{paths.rules}`
155
+ - Unknown postings Add `if` directives to rules file
156
+ - Unrecognized files Add provider config to `config/import/providers.yaml`
157
+ - Balance mismatch → Check for missing transactions or incorrect rules
154
158
 
155
159
  ---
156
160
 
package/dist/index.js CHANGED
@@ -17457,6 +17457,24 @@ function detectProvider(filename, content, config2) {
17457
17457
  return null;
17458
17458
  }
17459
17459
 
17460
+ // src/utils/worktreeManager.ts
17461
+ import { spawnSync } from "child_process";
17462
+ function execGit(args, cwd) {
17463
+ const result = spawnSync("git", args, { cwd, encoding: "utf-8" });
17464
+ if (result.status !== 0) {
17465
+ throw new Error(result.stderr || result.stdout || `git ${args[0]} failed`);
17466
+ }
17467
+ return (result.stdout || "").trim();
17468
+ }
17469
+ function isInWorktree(directory) {
17470
+ try {
17471
+ const gitDir = execGit(["rev-parse", "--git-dir"], directory);
17472
+ return gitDir.includes(".git/worktrees/");
17473
+ } catch {
17474
+ return false;
17475
+ }
17476
+ }
17477
+
17460
17478
  // src/tools/classify-statements.ts
17461
17479
  function findCSVFiles(importsDir) {
17462
17480
  if (!fs4.existsSync(importsDir)) {
@@ -17472,7 +17490,7 @@ function ensureDirectory(dirPath) {
17472
17490
  fs4.mkdirSync(dirPath, { recursive: true });
17473
17491
  }
17474
17492
  }
17475
- async function classifyStatementsCore(directory, agent, configLoader = loadImportConfig) {
17493
+ async function classifyStatementsCore(directory, agent, configLoader = loadImportConfig, worktreeChecker = isInWorktree) {
17476
17494
  const restrictionError = checkAccountantAgent(agent, "classify statements", {
17477
17495
  classified: [],
17478
17496
  unrecognized: []
@@ -17480,6 +17498,15 @@ async function classifyStatementsCore(directory, agent, configLoader = loadImpor
17480
17498
  if (restrictionError) {
17481
17499
  return restrictionError;
17482
17500
  }
17501
+ if (!worktreeChecker(directory)) {
17502
+ return JSON.stringify({
17503
+ success: false,
17504
+ error: "classify-statements must be run inside an import worktree",
17505
+ hint: "Use import-pipeline tool to orchestrate the full workflow",
17506
+ classified: [],
17507
+ unrecognized: []
17508
+ });
17509
+ }
17483
17510
  let config2;
17484
17511
  try {
17485
17512
  config2 = configLoader(directory);
@@ -17997,11 +18024,18 @@ function findPendingCsvFiles(pendingDir, provider, currency) {
17997
18024
  scanDirectory(searchPath);
17998
18025
  return csvFiles.sort();
17999
18026
  }
18000
- async function importStatementsCore(directory, agent, options, configLoader = loadImportConfig, hledgerExecutor = defaultHledgerExecutor) {
18027
+ async function importStatementsCore(directory, agent, options, configLoader = loadImportConfig, hledgerExecutor = defaultHledgerExecutor, worktreeChecker = isInWorktree) {
18001
18028
  const restrictionError = checkAccountantAgent(agent, "import statements");
18002
18029
  if (restrictionError) {
18003
18030
  return restrictionError;
18004
18031
  }
18032
+ if (!worktreeChecker(directory)) {
18033
+ return JSON.stringify({
18034
+ success: false,
18035
+ error: "import-statements must be run inside an import worktree",
18036
+ hint: "Use import-pipeline tool to orchestrate the full workflow"
18037
+ });
18038
+ }
18005
18039
  let config2;
18006
18040
  try {
18007
18041
  config2 = configLoader(directory);
@@ -18233,7 +18267,7 @@ async function importStatementsCore(directory, agent, options, configLoader = lo
18233
18267
  unknown: totalUnknown
18234
18268
  },
18235
18269
  error: `Ledger validation failed after import: ${validationResult.errors.join("; ")}`,
18236
- hint: "The import created invalid transactions. Check your rules file configuration (e.g., balance vs balance2 for balance assertions). CSV files have NOT been moved to done."
18270
+ hint: "The import created invalid transactions. Check your rules file configuration. CSV files have NOT been moved to done."
18237
18271
  });
18238
18272
  }
18239
18273
  for (const csvFile of importedFiles) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fuzzle/opencode-accountant",
3
- "version": "0.0.17",
3
+ "version": "0.1.0-next.1",
4
4
  "description": "An OpenCode accounting agent, specialized in double-entry-bookkepping with hledger",
5
5
  "author": {
6
6
  "name": "ali bengali",
@@ -31,13 +31,15 @@
31
31
  "@opencode-ai/plugin": "latest",
32
32
  "convert-csv-to-json": "^3.20.0",
33
33
  "js-yaml": "^4.1.0",
34
- "papaparse": "^5.5.3"
34
+ "papaparse": "^5.5.3",
35
+ "uuid": "^13.0.0"
35
36
  },
36
37
  "devDependencies": {
37
38
  "@eslint/js": "^9.39.1",
38
39
  "@types/js-yaml": "^4.0.9",
39
40
  "@types/node": "^20.11.5",
40
41
  "@types/papaparse": "^5.5.2",
42
+ "@types/uuid": "^11.0.0",
41
43
  "@typescript-eslint/eslint-plugin": "8.47.0",
42
44
  "@typescript-eslint/parser": "8.47.0",
43
45
  "bun-types": "latest",