@fuzzle/opencode-accountant 0.7.2-next.1 → 0.7.2

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.
@@ -118,7 +118,8 @@ The `import-pipeline` tool operates directly on the working directory:
118
118
  3. **Automatic Processing**: The tool:
119
119
  - Classifies CSV files by provider/currency (creates import contexts in `.memory/`)
120
120
  - Extracts required accounts from rules files and updates year journal
121
- - Validates and imports transactions to the appropriate year journal
121
+ - Validates all transactions have matching rules (dry run)
122
+ - Imports transactions to the appropriate year journal
122
123
  - Reconciles closing balance (auto-detected from CSV metadata or data, or manual override)
123
124
  - CSV files move: `incoming/` → `pending/` → `done/`
124
125
  4. **After Pipeline**: All changes remain uncommitted in your working directory for inspection
@@ -192,7 +193,8 @@ The following are MCP tools available to you. Always call these tools directly -
192
193
  1. Classifies CSV files and creates import contexts (unless `skipClassify: true`)
193
194
  2. For each context (sequentially, fail-fast):
194
195
  - Extracts accounts from matched rules and updates year journal with declarations
195
- - Validates and imports transactions to year journal, moves CSV from `pending/` to `done/`
196
+ - Validates all transactions have matching rules (dry run)
197
+ - Imports transactions to year journal, moves CSV from `pending/` to `done/`
196
198
  - Reconciles closing balance (auto-detected from CSV metadata/data or manual override)
197
199
  3. All changes remain uncommitted in the working directory
198
200
 
package/dist/index.js CHANGED
@@ -23667,6 +23667,28 @@ async function importStatements(directory, agent, options, configLoader = loadIm
23667
23667
  }
23668
23668
  const hasUnknowns = totalUnknown > 0;
23669
23669
  const hasErrors = filesWithErrors > 0 || filesWithoutRules > 0;
23670
+ if (options.checkOnly !== false) {
23671
+ const result = {
23672
+ success: !hasUnknowns && !hasErrors,
23673
+ files: fileResults,
23674
+ summary: {
23675
+ filesProcessed: fileResults.length,
23676
+ filesWithErrors,
23677
+ filesWithoutRules,
23678
+ totalTransactions,
23679
+ matched: totalMatched,
23680
+ unknown: totalUnknown
23681
+ }
23682
+ };
23683
+ if (hasUnknowns) {
23684
+ result.message = `Found ${totalUnknown} transaction(s) with unknown accounts. Add rules to categorize them.`;
23685
+ } else if (hasErrors) {
23686
+ result.message = `Some files had errors. Check the file results for details.`;
23687
+ } else {
23688
+ result.message = "All transactions matched. Ready to import with checkOnly: false";
23689
+ }
23690
+ return JSON.stringify(result);
23691
+ }
23670
23692
  if (hasUnknowns || hasErrors) {
23671
23693
  return buildErrorResultWithDetails("Cannot import: some transactions have unknown accounts or files have errors", fileResults, {
23672
23694
  filesProcessed: fileResults.length,
@@ -23675,7 +23697,7 @@ async function importStatements(directory, agent, options, configLoader = loadIm
23675
23697
  totalTransactions,
23676
23698
  matched: totalMatched,
23677
23699
  unknown: totalUnknown
23678
- }, "Add missing rules to categorize unknown transactions, then retry");
23700
+ }, "Run with checkOnly: true to see details, then add missing rules");
23679
23701
  }
23680
23702
  const importResult = await executeImports(fileResults, directory, pendingDir, doneDir, hledgerExecutor);
23681
23703
  if (!importResult.success) {
@@ -23715,19 +23737,32 @@ var import_statements_default = tool({
23715
23737
 
23716
23738
  This tool processes CSV files in the pending import directory and uses hledger's CSV import capabilities with matching rules files.
23717
23739
 
23718
- **Behavior:**
23719
- - First validates all transactions have known accounts (runs hledger print)
23720
- - If any 'income:unknown' or 'expenses:unknown' accounts found, aborts and reports them
23740
+ **Check Mode (checkOnly: true, default):**
23741
+ - Runs hledger print to preview transactions
23742
+ - Identifies transactions with 'income:unknown' or 'expenses:unknown' accounts
23743
+ - These indicate missing rules that need to be added
23744
+
23745
+ **Import Mode (checkOnly: false):**
23746
+ - First validates all transactions have known accounts
23747
+ - If any unknowns exist, aborts and reports them
23721
23748
  - If all clean, imports transactions and moves CSVs to done directory
23722
23749
 
23750
+ **Workflow:**
23751
+ 1. Run with checkOnly: true (or no args)
23752
+ 2. If unknowns found, add rules to the appropriate .rules file
23753
+ 3. Repeat until no unknowns
23754
+ 4. Run with checkOnly: false to import
23755
+
23723
23756
  Note: This tool is typically called via import-pipeline for the full workflow.`,
23724
23757
  args: {
23725
- contextId: tool.schema.string().describe("Context ID from classify step. Used to locate the specific CSV file to process.")
23758
+ contextId: tool.schema.string().describe("Context ID from classify step. Used to locate the specific CSV file to process."),
23759
+ checkOnly: tool.schema.boolean().optional().describe("If true (default), only check for unknown accounts without importing. Set to false to perform actual import.")
23726
23760
  },
23727
23761
  async execute(params, context) {
23728
23762
  const { directory, agent } = context;
23729
23763
  return importStatements(directory, agent, {
23730
- contextId: params.contextId
23764
+ contextId: params.contextId,
23765
+ checkOnly: params.checkOnly
23731
23766
  });
23732
23767
  }
23733
23768
  });
@@ -24985,6 +25020,61 @@ async function buildSuggestionContext(context, contextId, logger) {
24985
25020
  logger
24986
25021
  };
24987
25022
  }
25023
+ async function executeDryRunStep(context, contextId, logger) {
25024
+ logger?.startSection("Step 3: Dry Run Import");
25025
+ logger?.logStep("Dry Run", "start");
25026
+ const dryRunResult = await importStatements(context.directory, context.agent, {
25027
+ contextId,
25028
+ checkOnly: true
25029
+ }, context.configLoader, context.hledgerExecutor);
25030
+ const dryRunParsed = JSON.parse(dryRunResult);
25031
+ const message = dryRunParsed.success ? `Dry run passed: ${dryRunParsed.summary?.totalTransactions || 0} transactions ready` : `Dry run failed: ${dryRunParsed.summary?.unknown || 0} unknown account(s)`;
25032
+ logger?.logStep("Dry Run", dryRunParsed.success ? "success" : "error", message);
25033
+ if (dryRunParsed.summary?.totalTransactions) {
25034
+ logger?.info(`Found ${dryRunParsed.summary.totalTransactions} transactions`);
25035
+ }
25036
+ let postingsWithSuggestions = [];
25037
+ if (!dryRunParsed.success) {
25038
+ const allUnknownPostings = [];
25039
+ for (const file2 of dryRunParsed.files ?? []) {
25040
+ if (file2.unknownPostings && file2.unknownPostings.length > 0) {
25041
+ allUnknownPostings.push(...file2.unknownPostings);
25042
+ }
25043
+ }
25044
+ if (allUnknownPostings.length > 0) {
25045
+ try {
25046
+ const { suggestAccountsForPostingsBatch: suggestAccountsForPostingsBatch2 } = await Promise.resolve().then(() => (init_accountSuggester(), exports_accountSuggester));
25047
+ const suggestionContext = await buildSuggestionContext(context, contextId, logger);
25048
+ postingsWithSuggestions = await suggestAccountsForPostingsBatch2(allUnknownPostings, suggestionContext);
25049
+ } catch (error45) {
25050
+ logger?.error(`[ERROR] Failed to generate account suggestions: ${error45 instanceof Error ? error45.message : String(error45)}`);
25051
+ postingsWithSuggestions = allUnknownPostings;
25052
+ }
25053
+ }
25054
+ }
25055
+ const detailsLog = postingsWithSuggestions.length > 0 ? formatUnknownPostingsLog(postingsWithSuggestions) : undefined;
25056
+ context.result.steps.dryRun = buildStepResult(dryRunParsed.success, message, {
25057
+ success: dryRunParsed.success,
25058
+ summary: dryRunParsed.summary,
25059
+ unknownPostings: postingsWithSuggestions.length > 0 ? postingsWithSuggestions : undefined,
25060
+ detailsLog
25061
+ });
25062
+ if (!dryRunParsed.success) {
25063
+ if (detailsLog) {
25064
+ logger?.error("Dry run found unknown accounts or errors");
25065
+ logger?.info(detailsLog);
25066
+ }
25067
+ logger?.endSection();
25068
+ context.result.error = "Dry run found unknown accounts or errors";
25069
+ context.result.hint = "Add rules to categorize unknown transactions, then retry. See details above for suggestions.";
25070
+ throw new Error("Dry run failed");
25071
+ }
25072
+ if (dryRunParsed.summary?.totalTransactions === 0) {
25073
+ logger?.endSection();
25074
+ throw new NoTransactionsError;
25075
+ }
25076
+ logger?.endSection();
25077
+ }
24988
25078
  function formatUnknownPostingsLog(postings) {
24989
25079
  if (postings.length === 0)
24990
25080
  return "";
@@ -25021,40 +25111,18 @@ function formatUnknownPostingsLog(postings) {
25021
25111
  }
25022
25112
  async function executeImportStep(context, contextId, logger) {
25023
25113
  const importContext = loadContext(context.directory, contextId);
25024
- logger?.startSection(`Step 3: Import Transactions (${importContext.accountNumber || contextId})`);
25114
+ logger?.startSection(`Step 4: Import Transactions (${importContext.accountNumber || contextId})`);
25025
25115
  logger?.logStep("Import", "start");
25026
25116
  const importResult = await importStatements(context.directory, context.agent, {
25027
- contextId
25117
+ contextId,
25118
+ checkOnly: false
25028
25119
  }, context.configLoader, context.hledgerExecutor);
25029
25120
  const importParsed = JSON.parse(importResult);
25030
25121
  const message = importParsed.success ? `Imported ${importParsed.summary?.totalTransactions || 0} transactions` : `Import failed: ${importParsed.error || "Unknown error"}`;
25031
25122
  logger?.logStep("Import", importParsed.success ? "success" : "error", message);
25032
- let postingsWithSuggestions;
25033
- let detailsLog;
25034
- if (!importParsed.success) {
25035
- const allUnknownPostings = [];
25036
- for (const file2 of importParsed.files ?? []) {
25037
- if (file2.unknownPostings && file2.unknownPostings.length > 0) {
25038
- allUnknownPostings.push(...file2.unknownPostings);
25039
- }
25040
- }
25041
- if (allUnknownPostings.length > 0) {
25042
- try {
25043
- const { suggestAccountsForPostingsBatch: suggestAccountsForPostingsBatch2 } = await Promise.resolve().then(() => (init_accountSuggester(), exports_accountSuggester));
25044
- const suggestionContext = await buildSuggestionContext(context, contextId, logger);
25045
- postingsWithSuggestions = await suggestAccountsForPostingsBatch2(allUnknownPostings, suggestionContext);
25046
- } catch (error45) {
25047
- logger?.error(`[ERROR] Failed to generate account suggestions: ${error45 instanceof Error ? error45.message : String(error45)}`);
25048
- postingsWithSuggestions = allUnknownPostings;
25049
- }
25050
- detailsLog = postingsWithSuggestions && postingsWithSuggestions.length > 0 ? formatUnknownPostingsLog(postingsWithSuggestions) : undefined;
25051
- }
25052
- }
25053
25123
  context.result.steps.import = buildStepResult(importParsed.success, message, {
25054
25124
  success: importParsed.success,
25055
25125
  summary: importParsed.summary,
25056
- unknownPostings: postingsWithSuggestions,
25057
- detailsLog,
25058
25126
  error: importParsed.error
25059
25127
  });
25060
25128
  if (importParsed.success) {
@@ -25065,26 +25133,16 @@ async function executeImportStep(context, contextId, logger) {
25065
25133
  });
25066
25134
  }
25067
25135
  if (!importParsed.success) {
25068
- if (detailsLog) {
25069
- logger?.error("Import found unknown accounts or errors");
25070
- logger?.info(detailsLog);
25071
- } else {
25072
- logger?.error("Import failed", new Error(importParsed.error || "Unknown error"));
25073
- }
25136
+ logger?.error("Import failed", new Error(importParsed.error || "Unknown error"));
25074
25137
  logger?.endSection();
25075
- context.result.error = detailsLog ? "Import found unknown accounts or errors" : `Import failed: ${importParsed.error || "Unknown error"}`;
25076
- context.result.hint = detailsLog ? "Add rules to categorize unknown transactions, then retry. See details above for suggestions." : undefined;
25138
+ context.result.error = `Import failed: ${importParsed.error || "Unknown error"}`;
25077
25139
  throw new Error("Import failed");
25078
25140
  }
25079
- if (importParsed.summary?.totalTransactions === 0) {
25080
- logger?.endSection();
25081
- throw new NoTransactionsError;
25082
- }
25083
25141
  logger?.endSection();
25084
25142
  }
25085
25143
  async function executeReconcileStep(context, contextId, logger) {
25086
25144
  const importContext = loadContext(context.directory, contextId);
25087
- logger?.startSection(`Step 4: Reconcile Balance (${importContext.accountNumber || contextId})`);
25145
+ logger?.startSection(`Step 5: Reconcile Balance (${importContext.accountNumber || contextId})`);
25088
25146
  logger?.logStep("Reconcile", "start");
25089
25147
  const reconcileResult = await reconcileStatement(context.directory, context.agent, {
25090
25148
  contextId,
@@ -25163,6 +25221,7 @@ async function importPipeline(directory, agent, options, configLoader = loadImpo
25163
25221
  const importContext = loadContext(context.directory, contextId);
25164
25222
  logger.info(`Processing: ${importContext.filename} (${importContext.accountNumber || "unknown account"})`);
25165
25223
  await executeAccountDeclarationsStep(context, contextId, logger);
25224
+ await executeDryRunStep(context, contextId, logger);
25166
25225
  await executeImportStep(context, contextId, logger);
25167
25226
  await executeReconcileStep(context, contextId, logger);
25168
25227
  totalTransactions += context.result.steps.import?.details?.summary?.totalTransactions || 0;
@@ -25200,8 +25259,9 @@ This tool orchestrates the full import workflow:
25200
25259
  **Pipeline Steps:**
25201
25260
  1. **Classify**: Moves CSVs from import/incoming to import/pending (optional, skip with skipClassify)
25202
25261
  2. **Account Declarations**: Ensures all required accounts are declared in year journal
25203
- 3. **Import**: Validates all transactions have known accounts, imports to journal (moves CSVs to import/done)
25204
- 4. **Reconcile**: Validates closing balance matches CSV metadata
25262
+ 3. **Dry Run**: Validates all transactions have known accounts
25263
+ 4. **Import**: Imports transactions to the journal (moves CSVs to import/done)
25264
+ 5. **Reconcile**: Validates closing balance matches CSV metadata
25205
25265
 
25206
25266
  **Important:**
25207
25267
  - All changes remain uncommitted in your working directory
@@ -12,8 +12,9 @@ The pipeline automates these sequential steps:
12
12
  1a. **Preprocess BTC CSV** _(Revolut only)_ - Add computed columns (fees in BTC, total, clean price) for hledger rules
13
13
  1b. **Generate BTC Purchases** _(Revolut only)_ - Cross-reference fiat + BTC CSVs for equity conversion entries
14
14
  2. **Account Declarations** - Ensure all accounts exist in year journals
15
- 3. **Import** - Validate transactions, add to journals, move files to done
16
- 4. **Reconcile** - Verify balances match expectations
15
+ 3. **Dry Run** - Validate transactions, check for unknown accounts
16
+ 4. **Import** - Add transactions to journals, move files to done
17
+ 5. **Reconcile** - Verify balances match expectations
17
18
 
18
19
  Steps 1a and 1b are Revolut-specific and are automatically skipped when no Revolut BTC CSV is present.
19
20
 
@@ -48,6 +49,10 @@ When all steps complete successfully:
48
49
  "success": true,
49
50
  "message": "Account declarations complete"
50
51
  },
52
+ "dryRun": {
53
+ "success": true,
54
+ "message": "Dry run complete - all transactions validated"
55
+ },
51
56
  "import": {
52
57
  "success": true,
53
58
  "message": "Imported 42 transactions"
@@ -81,9 +86,9 @@ When the incoming directory is empty:
81
86
  }
82
87
  ```
83
88
 
84
- ### Failure - Unknown Accounts in Import
89
+ ### Failure - Unknown Accounts in Dry Run
85
90
 
86
- When import detects unknown accounts:
91
+ When dry run detects unknown accounts:
87
92
 
88
93
  ```json
89
94
  {
@@ -98,13 +103,13 @@ When import detects unknown accounts:
98
103
  "success": true,
99
104
  "message": "Account declarations complete"
100
105
  },
101
- "import": {
106
+ "dryRun": {
102
107
  "success": false,
103
- "message": "Import failed: 3 transactions with unknown accounts"
108
+ "message": "Dry run failed: 3 transactions with unknown accounts"
104
109
  }
105
110
  },
106
- "error": "Import found unknown accounts or errors",
107
- "hint": "Add rules to categorize unknown transactions, then retry. See details above for suggestions."
111
+ "error": "Dry run validation failed: 3 transactions with unknown accounts",
112
+ "hint": "Update rules file to categorize unknown transactions"
108
113
  }
109
114
  ```
110
115
 
@@ -119,6 +124,7 @@ When reconciliation fails:
119
124
  "steps": {
120
125
  "classify": { "success": true },
121
126
  "accountDeclarations": { "success": true },
127
+ "dryRun": { "success": true },
122
128
  "import": { "success": true, "message": "Imported 42 transactions" },
123
129
  "reconcile": {
124
130
  "success": false,
@@ -200,17 +206,34 @@ See [classify-statements](classify-statements.md) for details.
200
206
  - Multiple years in single CSV
201
207
  - Cannot create year journal file
202
208
 
203
- ### Step 3: Import
209
+ ### Step 3: Dry Run
210
+
211
+ **Purpose**: Validate all transactions can be categorized before importing
212
+
213
+ **What happens** (per context):
214
+
215
+ 1. Loads context to find CSV file
216
+ 2. Runs `hledger print` with rules file
217
+ 3. Checks for `income:unknown` or `expenses:unknown` accounts
218
+ 4. Reports unknown postings if found
219
+
220
+ **Failure scenarios**:
221
+
222
+ - Unknown accounts detected → **Pipeline stops here**
223
+ - CSV parsing errors
224
+ - Rules file syntax errors
204
225
 
205
- **Purpose**: Validate and add transactions to journal, mark files as processed
226
+ **User action required**: Update rules file to categorize unknown transactions, then re-run pipeline.
227
+
228
+ ### Step 4: Import
229
+
230
+ **Purpose**: Add transactions to journal and mark files as processed
206
231
 
207
232
  **What happens** (per context):
208
233
 
209
234
  1. Loads context to find CSV file
210
- 2. Validates all transactions have known accounts (checks for `income:unknown`/`expenses:unknown`)
211
- 3. If unknown accounts found, generates account suggestions and **stops pipeline**
212
- 4. Imports transactions using `hledger import`
213
- 5. Moves CSV from `pending/` to `done/`
235
+ 2. Imports transactions using `hledger import`
236
+ 3. Moves CSV from `pending/` to `done/`
214
237
  4. Updates context with:
215
238
  - `rulesFile` path
216
239
  - `yearJournal` path
@@ -219,14 +242,13 @@ See [classify-statements](classify-statements.md) for details.
219
242
 
220
243
  **Failure scenarios**:
221
244
 
222
- - Unknown accounts detected → **Pipeline stops here** with account suggestions
223
245
  - Duplicate transactions detected
224
246
  - Journal file write error
225
247
  - File move operation fails
226
248
 
227
249
  See [import-statements](import-statements.md) for details.
228
250
 
229
- ### Step 4: Reconcile
251
+ ### Step 5: Reconcile
230
252
 
231
253
  **Purpose**: Verify imported transactions result in correct closing balance
232
254
 
@@ -298,7 +320,7 @@ See [reconcile-statement](reconcile-statement.md) for details.
298
320
  │ │
299
321
  ▼ │
300
322
  ┌────────────────────────────────────────┐ │
301
- │ STEPS 2-4 (for uuid-1): │ │
323
+ │ STEPS 2-5 (for uuid-1): │ │
302
324
  │ • Account Declarations │ │
303
325
  │ • Dry Run │ │
304
326
  │ • Import │ │
@@ -309,9 +331,10 @@ See [reconcile-statement](reconcile-statement.md) for details.
309
331
  │ │
310
332
  │ ▼
311
333
  │ ┌────────────────────────────────────────┐
312
- │ │ STEPS 2-4 (for uuid-2): │
334
+ │ │ STEPS 2-5 (for uuid-2): │
313
335
  │ │ • Account Declarations │
314
- │ • Import
336
+ │ │ • Dry Run
337
+ │ │ • Import │
315
338
  │ │ • Reconcile │
316
339
  │ │ • UPDATE: .memory/uuid-2.json │
317
340
  │ └────────────────────────────────────────┘
@@ -330,7 +353,7 @@ The pipeline processes contexts **one at a time**:
330
353
  ```
331
354
  for each contextId:
332
355
  1. Load context
333
- 2. Run steps 2-4 for this context
356
+ 2. Run steps 2-5 for this context
334
357
  3. If ANY step fails → STOP PIPELINE
335
358
  4. Update context with results
336
359
  5. Move to next context
@@ -394,8 +417,8 @@ classify-statements
394
417
  # Output: { "contexts": ["uuid-1", "uuid-2"] }
395
418
 
396
419
  # Import each manually
397
- import-statements --contextId "uuid-1"
398
- import-statements --contextId "uuid-2"
420
+ import-statements --contextId "uuid-1" --checkOnly false
421
+ import-statements --contextId "uuid-2" --checkOnly false
399
422
 
400
423
  # Reconcile with different balances
401
424
  reconcile-statement --contextId "uuid-1" --closingBalance "EUR 1000.00"
@@ -415,12 +438,12 @@ import-pipeline --skipClassify
415
438
  ### Scenario 5: Handling Unknown Accounts
416
439
 
417
440
  ```bash
418
- # First run: import fails with unknown accounts
441
+ # First run: dry run fails
419
442
  import-pipeline
420
- # Output: "Import found unknown accounts or errors"
443
+ # Output: "Dry run failed: 3 transactions with unknown accounts"
421
444
 
422
- # Check which transactions are unknown (details in pipeline output)
423
- import-statements --contextId {from-output}
445
+ # Check which transactions are unknown
446
+ import-statements --contextId {from-output} --checkOnly true
424
447
  # Shows unknown postings with suggestions
425
448
 
426
449
  # Update rules file
@@ -439,7 +462,7 @@ import-pipeline --skipClassify
439
462
  | ----------------------- | ------------------- | ----------------------------- | --------------------------------------------------- |
440
463
  | File collision | Classify | Target file already exists | Move existing file to done or delete |
441
464
  | Unrecognized CSV | Classify | Unknown provider | Add provider config or move to pending manually |
442
- | Unknown accounts | Import | Transactions without rules | Update rules file to categorize |
465
+ | Unknown accounts | Dry Run | Transactions without rules | Update rules file to categorize |
443
466
  | Balance mismatch | Reconcile | Missing transactions | Check skip rules, verify all transactions imported |
444
467
  | Context not found | Import/Reconcile | Invalid context ID | Re-run classify-statements |
445
468
  | Multi-year CSV | Account Declaration | CSV spans multiple years | Split CSV by year or import to single year manually |
@@ -458,9 +481,9 @@ The output shows which step failed:
458
481
  "steps": {
459
482
  "classify": { "success": true },
460
483
  "accountDeclarations": { "success": true },
461
- "import": { "success": false } // ← Failed here
484
+ "dryRun": { "success": false } // ← Failed here
462
485
  },
463
- "error": "Import found unknown accounts or errors..."
486
+ "error": "Dry run validation failed..."
464
487
  }
465
488
  ```
466
489
 
@@ -471,7 +494,7 @@ The output shows which step failed:
471
494
  # contexts: ["abc123-..."]
472
495
 
473
496
  # Run the failed step individually
474
- import-statements --contextId "abc123-..."
497
+ import-statements --contextId "abc123-..." --checkOnly true
475
498
  ```
476
499
 
477
500
  **Step 3: Fix issue and resume**
@@ -563,7 +586,7 @@ Context 3: classify → import → reconcile ✓
563
586
  The pipeline invokes these tools internally:
564
587
 
565
588
  1. `classify-statements` → Returns context IDs
566
- 2. `import-statements` (per context) → Validates and imports, updates contexts
589
+ 2. `import-statements` (per context) → Updates contexts
567
590
  3. `reconcile-statement` (per context) → Updates contexts
568
591
 
569
592
  ### Manual Tool Usage
@@ -575,8 +598,11 @@ You can run tools independently for more control:
575
598
  classify-statements
576
599
  # → Get contextIds
577
600
 
578
- import-statements --contextId {uuid}
579
- # → Validate and import
601
+ import-statements --contextId {uuid} --checkOnly true
602
+ # → Validate first
603
+
604
+ import-statements --contextId {uuid} --checkOnly false
605
+ # → Import
580
606
 
581
607
  reconcile-statement --contextId {uuid}
582
608
  # → Reconcile
@@ -605,10 +631,11 @@ For typical import (1-2 CSVs, 50-100 transactions each):
605
631
 
606
632
  - **Classify**: <1 second
607
633
  - **Account Declarations**: <1 second
608
- - **Import**: 1-2 seconds (hledger validation + import)
634
+ - **Dry Run**: 1-2 seconds (hledger processing)
635
+ - **Import**: 1-2 seconds (hledger processing)
609
636
  - **Reconcile**: 1-2 seconds (hledger queries)
610
637
 
611
- **Total**: ~3-6 seconds per context
638
+ **Total**: ~5-10 seconds per context
612
639
 
613
640
  ### Scalability
614
641
 
@@ -625,5 +652,5 @@ If you regularly import 50+ CSVs, consider:
625
652
 
626
653
  - [Import Context Architecture](../architecture/import-context.md) - Deep dive into context system
627
654
  - [classify-statements Tool](classify-statements.md) - Step 1: Classification
628
- - [import-statements Tool](import-statements.md) - Step 3: Import
629
- - [reconcile-statement Tool](reconcile-statement.md) - Step 4: Reconciliation
655
+ - [import-statements Tool](import-statements.md) - Step 4: Import
656
+ - [reconcile-statement Tool](reconcile-statement.md) - Step 5: Reconciliation
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fuzzle/opencode-accountant",
3
- "version": "0.7.2-next.1",
3
+ "version": "0.7.2",
4
4
  "description": "An OpenCode accounting agent, specialized in double-entry-bookkepping with hledger",
5
5
  "author": {
6
6
  "name": "ali bengali",