@fuzzle/opencode-accountant 0.4.1-next.1 → 0.4.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.
@@ -97,71 +97,11 @@ 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 preserved by default at `/tmp/import-worktree-<uuid>` for debugging
101
- - Main branch remains untouched
100
+ - Worktree is discarded, main branch remains untouched
102
101
  - Review error output for unknown postings with full CSV row data
103
102
  - Update rules file with `if` directives to match the transaction
104
103
  - Re-run `import-pipeline`
105
104
 
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
-
165
105
  ### Rules Files
166
106
 
167
107
  - The location of the rules files is configured in `config/import/providers.yaml`
package/dist/index.js CHANGED
@@ -17509,8 +17509,7 @@ function validatePaths(paths) {
17509
17509
  pending: pathsObj.pending,
17510
17510
  done: pathsObj.done,
17511
17511
  unrecognized: pathsObj.unrecognized,
17512
- rules: pathsObj.rules,
17513
- logs: pathsObj.logs
17512
+ rules: pathsObj.rules
17514
17513
  };
17515
17514
  }
17516
17515
  function validateDetectionRule(providerName, index, rule) {
@@ -17640,9 +17639,6 @@ function loadImportConfig(directory) {
17640
17639
  for (const [name, config2] of Object.entries(providersObj)) {
17641
17640
  providers[name] = validateProviderConfig(name, config2);
17642
17641
  }
17643
- if (!paths.logs) {
17644
- paths.logs = ".memory";
17645
- }
17646
17642
  return { paths, providers };
17647
17643
  }
17648
17644
 
@@ -17911,35 +17907,15 @@ function isInWorktree(directory) {
17911
17907
  return false;
17912
17908
  }
17913
17909
  }
17914
- async function withWorktree(directory, operation, options) {
17910
+ async function withWorktree(directory, operation) {
17915
17911
  let createdWorktree = null;
17916
- let operationSucceeded = false;
17917
- const logger = options?.logger;
17918
- const keepOnError = options?.keepOnError ?? true;
17919
17912
  try {
17920
- logger?.logStep("Create Worktree", "start");
17921
17913
  createdWorktree = createImportWorktree(directory);
17922
- logger?.logStep("Create Worktree", "success", `Path: ${createdWorktree.path}`);
17923
- logger?.info(`Branch: ${createdWorktree.branch}`);
17924
- logger?.info(`UUID: ${createdWorktree.uuid}`);
17925
17914
  const result = await operation(createdWorktree);
17926
- operationSucceeded = true;
17927
17915
  return result;
17928
17916
  } finally {
17929
17917
  if (createdWorktree) {
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
- }
17918
+ removeWorktree(createdWorktree, true);
17943
17919
  }
17944
17920
  }
17945
17921
  }
@@ -24052,7 +24028,7 @@ async function processCsvFile(csvFile, rulesMapping, directory, hledgerExecutor)
24052
24028
  transactionYear
24053
24029
  };
24054
24030
  }
24055
- async function importStatements(directory, agent, options, configLoader = loadImportConfig, hledgerExecutor = defaultHledgerExecutor, worktreeChecker = isInWorktree, _logger) {
24031
+ async function importStatements(directory, agent, options, configLoader = loadImportConfig, hledgerExecutor = defaultHledgerExecutor, worktreeChecker = isInWorktree) {
24056
24032
  const restrictionError = checkAccountantAgent(agent, "import statements");
24057
24033
  if (restrictionError) {
24058
24034
  return restrictionError;
@@ -24553,8 +24529,8 @@ It must be run inside an import worktree (use import-pipeline for the full workf
24553
24529
  }
24554
24530
  });
24555
24531
  // src/tools/import-pipeline.ts
24556
- import * as fs14 from "fs";
24557
- import * as path13 from "path";
24532
+ import * as fs13 from "fs";
24533
+ import * as path12 from "path";
24558
24534
 
24559
24535
  // src/utils/accountDeclarations.ts
24560
24536
  import * as fs12 from "fs";
@@ -24671,158 +24647,6 @@ function ensureAccountDeclarations(yearJournalPath, accounts) {
24671
24647
  };
24672
24648
  }
24673
24649
 
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
-
24826
24650
  // src/tools/import-pipeline.ts
24827
24651
  class NoTransactionsError extends Error {
24828
24652
  constructor() {
@@ -24862,8 +24686,8 @@ function buildCommitMessage(provider, currency, fromDate, untilDate, transaction
24862
24686
  return `${parts.join(" ")}${dateRange}${txStr}`;
24863
24687
  }
24864
24688
  function cleanupIncomingFiles(worktree, context) {
24865
- const incomingDir = path13.join(worktree.mainRepoPath, "import/incoming");
24866
- if (!fs14.existsSync(incomingDir)) {
24689
+ const incomingDir = path12.join(worktree.mainRepoPath, "import/incoming");
24690
+ if (!fs13.existsSync(incomingDir)) {
24867
24691
  return;
24868
24692
  }
24869
24693
  const importStep = context.result.steps.import;
@@ -24878,11 +24702,11 @@ function cleanupIncomingFiles(worktree, context) {
24878
24702
  for (const fileResult of importResult.files) {
24879
24703
  if (!fileResult.csv)
24880
24704
  continue;
24881
- const filename = path13.basename(fileResult.csv);
24882
- const filePath = path13.join(incomingDir, filename);
24883
- if (fs14.existsSync(filePath)) {
24705
+ const filename = path12.basename(fileResult.csv);
24706
+ const filePath = path12.join(incomingDir, filename);
24707
+ if (fs13.existsSync(filePath)) {
24884
24708
  try {
24885
- fs14.unlinkSync(filePath);
24709
+ fs13.unlinkSync(filePath);
24886
24710
  deletedCount++;
24887
24711
  } catch (error45) {
24888
24712
  console.error(`[ERROR] Failed to delete ${filename}: ${error45 instanceof Error ? error45.message : String(error45)}`);
@@ -24893,13 +24717,9 @@ function cleanupIncomingFiles(worktree, context) {
24893
24717
  console.log(`[INFO] Cleaned up ${deletedCount} file(s) from import/incoming/`);
24894
24718
  }
24895
24719
  }
24896
- async function executeClassifyStep(context, worktree, logger) {
24897
- logger?.startSection("Step 2: Classify Transactions");
24898
- logger?.logStep("Classify", "start");
24720
+ async function executeClassifyStep(context, worktree) {
24899
24721
  if (context.options.skipClassify) {
24900
- logger?.info("Classification skipped (skipClassify: true)");
24901
24722
  context.result.steps.classify = buildStepResult(true, "Classification skipped (skipClassify: true)");
24902
- logger?.endSection();
24903
24723
  return;
24904
24724
  }
24905
24725
  const inWorktree = () => true;
@@ -24909,23 +24729,18 @@ async function executeClassifyStep(context, worktree, logger) {
24909
24729
  let message = success2 ? "Classification complete" : "Classification had issues";
24910
24730
  if (classifyParsed.unrecognized?.length > 0) {
24911
24731
  message = `Classification complete with ${classifyParsed.unrecognized.length} unrecognized file(s)`;
24912
- logger?.warn(`${classifyParsed.unrecognized.length} unrecognized file(s)`);
24913
24732
  }
24914
- logger?.logStep("Classify", success2 ? "success" : "error", message);
24915
24733
  const details = {
24916
24734
  success: success2,
24917
24735
  unrecognized: classifyParsed.unrecognized,
24918
24736
  classified: classifyParsed
24919
24737
  };
24920
24738
  context.result.steps.classify = buildStepResult(success2, message, details);
24921
- logger?.endSection();
24922
24739
  }
24923
- async function executeAccountDeclarationsStep(context, worktree, logger) {
24924
- logger?.startSection("Step 3: Check Account Declarations");
24925
- logger?.logStep("Check Accounts", "start");
24740
+ async function executeAccountDeclarationsStep(context, worktree) {
24926
24741
  const config2 = context.configLoader(worktree.path);
24927
- const pendingDir = path13.join(worktree.path, config2.paths.pending);
24928
- const rulesDir = path13.join(worktree.path, config2.paths.rules);
24742
+ const pendingDir = path12.join(worktree.path, config2.paths.pending);
24743
+ const rulesDir = path12.join(worktree.path, config2.paths.rules);
24929
24744
  const csvFiles = findCsvFiles(pendingDir, context.options.provider, context.options.currency);
24930
24745
  if (csvFiles.length === 0) {
24931
24746
  context.result.steps.accountDeclarations = buildStepResult(true, "No CSV files to process", {
@@ -24956,7 +24771,7 @@ async function executeAccountDeclarationsStep(context, worktree, logger) {
24956
24771
  context.result.steps.accountDeclarations = buildStepResult(true, "No accounts found in rules files", {
24957
24772
  accountsAdded: [],
24958
24773
  journalUpdated: "",
24959
- rulesScanned: Array.from(matchedRulesFiles).map((f) => path13.relative(worktree.path, f))
24774
+ rulesScanned: Array.from(matchedRulesFiles).map((f) => path12.relative(worktree.path, f))
24960
24775
  });
24961
24776
  return;
24962
24777
  }
@@ -24979,7 +24794,7 @@ async function executeAccountDeclarationsStep(context, worktree, logger) {
24979
24794
  context.result.steps.accountDeclarations = buildStepResult(false, "Could not determine transaction year from CSV files", {
24980
24795
  accountsAdded: [],
24981
24796
  journalUpdated: "",
24982
- rulesScanned: Array.from(matchedRulesFiles).map((f) => path13.relative(worktree.path, f))
24797
+ rulesScanned: Array.from(matchedRulesFiles).map((f) => path12.relative(worktree.path, f))
24983
24798
  });
24984
24799
  return;
24985
24800
  }
@@ -24990,28 +24805,19 @@ async function executeAccountDeclarationsStep(context, worktree, logger) {
24990
24805
  context.result.steps.accountDeclarations = buildStepResult(false, `Failed to create year journal: ${error45 instanceof Error ? error45.message : String(error45)}`, {
24991
24806
  accountsAdded: [],
24992
24807
  journalUpdated: "",
24993
- rulesScanned: Array.from(matchedRulesFiles).map((f) => path13.relative(worktree.path, f))
24808
+ rulesScanned: Array.from(matchedRulesFiles).map((f) => path12.relative(worktree.path, f))
24994
24809
  });
24995
24810
  return;
24996
24811
  }
24997
24812
  const result = ensureAccountDeclarations(yearJournalPath, allAccounts);
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
- }
24813
+ const message = result.added.length > 0 ? `Added ${result.added.length} account declaration(s) to ${path12.relative(worktree.path, yearJournalPath)}` : "All required accounts already declared";
25005
24814
  context.result.steps.accountDeclarations = buildStepResult(true, message, {
25006
24815
  accountsAdded: result.added,
25007
- journalUpdated: path13.relative(worktree.path, yearJournalPath),
25008
- rulesScanned: Array.from(matchedRulesFiles).map((f) => path13.relative(worktree.path, f))
24816
+ journalUpdated: path12.relative(worktree.path, yearJournalPath),
24817
+ rulesScanned: Array.from(matchedRulesFiles).map((f) => path12.relative(worktree.path, f))
25009
24818
  });
25010
- logger?.endSection();
25011
24819
  }
25012
- async function executeDryRunStep(context, worktree, logger) {
25013
- logger?.startSection("Step 4: Dry Run Import");
25014
- logger?.logStep("Dry Run", "start");
24820
+ async function executeDryRunStep(context, worktree) {
25015
24821
  const inWorktree = () => true;
25016
24822
  const dryRunResult = await importStatements(worktree.path, context.agent, {
25017
24823
  provider: context.options.provider,
@@ -25020,30 +24826,20 @@ async function executeDryRunStep(context, worktree, logger) {
25020
24826
  }, context.configLoader, context.hledgerExecutor, inWorktree);
25021
24827
  const dryRunParsed = JSON.parse(dryRunResult);
25022
24828
  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
- }
25027
24829
  context.result.steps.dryRun = buildStepResult(dryRunParsed.success, message, {
25028
24830
  success: dryRunParsed.success,
25029
24831
  summary: dryRunParsed.summary
25030
24832
  });
25031
24833
  if (!dryRunParsed.success) {
25032
- logger?.error("Dry run found unknown accounts or errors");
25033
- logger?.endSection();
25034
24834
  context.result.error = "Dry run found unknown accounts or errors";
25035
24835
  context.result.hint = "Add rules to categorize unknown transactions, then retry";
25036
24836
  throw new Error("Dry run failed");
25037
24837
  }
25038
24838
  if (dryRunParsed.summary?.totalTransactions === 0) {
25039
- logger?.endSection();
25040
24839
  throw new NoTransactionsError;
25041
24840
  }
25042
- logger?.endSection();
25043
24841
  }
25044
- async function executeImportStep(context, worktree, logger) {
25045
- logger?.startSection("Step 5: Import Transactions");
25046
- logger?.logStep("Import", "start");
24842
+ async function executeImportStep(context, worktree) {
25047
24843
  const inWorktree = () => true;
25048
24844
  const importResult = await importStatements(worktree.path, context.agent, {
25049
24845
  provider: context.options.provider,
@@ -25052,23 +24848,17 @@ async function executeImportStep(context, worktree, logger) {
25052
24848
  }, context.configLoader, context.hledgerExecutor, inWorktree);
25053
24849
  const importParsed = JSON.parse(importResult);
25054
24850
  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);
25056
24851
  context.result.steps.import = buildStepResult(importParsed.success, message, {
25057
24852
  success: importParsed.success,
25058
24853
  summary: importParsed.summary,
25059
24854
  error: importParsed.error
25060
24855
  });
25061
24856
  if (!importParsed.success) {
25062
- logger?.error("Import failed", new Error(importParsed.error || "Unknown error"));
25063
- logger?.endSection();
25064
24857
  context.result.error = `Import failed: ${importParsed.error || "Unknown error"}`;
25065
24858
  throw new Error("Import failed");
25066
24859
  }
25067
- logger?.endSection();
25068
24860
  }
25069
- async function executeReconcileStep(context, worktree, logger) {
25070
- logger?.startSection("Step 6: Reconcile Balance");
25071
- logger?.logStep("Reconcile", "start");
24861
+ async function executeReconcileStep(context, worktree) {
25072
24862
  const inWorktree = () => true;
25073
24863
  const reconcileResult = await reconcileStatement(worktree.path, context.agent, {
25074
24864
  provider: context.options.provider,
@@ -25078,11 +24868,6 @@ async function executeReconcileStep(context, worktree, logger) {
25078
24868
  }, context.configLoader, context.hledgerExecutor, inWorktree);
25079
24869
  const reconcileParsed = JSON.parse(reconcileResult);
25080
24870
  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
- }
25086
24871
  context.result.steps.reconcile = buildStepResult(reconcileParsed.success, message, {
25087
24872
  success: reconcileParsed.success,
25088
24873
  actualBalance: reconcileParsed.actualBalance,
@@ -25091,17 +24876,12 @@ async function executeReconcileStep(context, worktree, logger) {
25091
24876
  error: reconcileParsed.error
25092
24877
  });
25093
24878
  if (!reconcileParsed.success) {
25094
- logger?.error("Reconciliation failed", new Error(reconcileParsed.error || "Balance mismatch"));
25095
- logger?.endSection();
25096
24879
  context.result.error = `Reconciliation failed: ${reconcileParsed.error || "Balance mismatch"}`;
25097
24880
  context.result.hint = "Check for missing transactions or incorrect rules";
25098
24881
  throw new Error("Reconciliation failed");
25099
24882
  }
25100
- logger?.endSection();
25101
24883
  }
25102
- async function executeMergeStep(context, worktree, logger) {
25103
- logger?.startSection("Step 7: Merge to Main");
25104
- logger?.logStep("Merge", "start");
24884
+ async function executeMergeStep(context, worktree) {
25105
24885
  const importDetails = context.result.steps.import?.details;
25106
24886
  const reconcileDetails = context.result.steps.reconcile?.details;
25107
24887
  if (!importDetails || !reconcileDetails) {
@@ -25114,17 +24894,11 @@ async function executeMergeStep(context, worktree, logger) {
25114
24894
  const transactionCount = importDetails.summary?.totalTransactions || 0;
25115
24895
  const commitMessage = buildCommitMessage(context.options.provider, context.options.currency, commitInfo.fromDate, commitInfo.untilDate, transactionCount);
25116
24896
  try {
25117
- logger?.info(`Commit message: "${commitMessage}"`);
25118
24897
  mergeWorktree(worktree, commitMessage);
25119
- logger?.logStep("Merge", "success", "Merged to main branch");
25120
24898
  const mergeDetails = { commitMessage };
25121
24899
  context.result.steps.merge = buildStepResult(true, `Merged to main: "${commitMessage}"`, mergeDetails);
25122
24900
  cleanupIncomingFiles(worktree, context);
25123
- logger?.endSection();
25124
24901
  } catch (error45) {
25125
- logger?.logStep("Merge", "error");
25126
- logger?.error("Merge to main branch failed", error45);
25127
- logger?.endSection();
25128
24902
  const message = `Merge failed: ${error45 instanceof Error ? error45.message : String(error45)}`;
25129
24903
  context.result.steps.merge = buildStepResult(false, message);
25130
24904
  context.result.error = "Merge to main branch failed";
@@ -25142,13 +24916,6 @@ async function importPipeline(directory, agent, options, configLoader = loadImpo
25142
24916
  if (restrictionError) {
25143
24917
  return restrictionError;
25144
24918
  }
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("");
25152
24919
  const result = {
25153
24920
  success: false,
25154
24921
  steps: {}
@@ -25163,47 +24930,33 @@ async function importPipeline(directory, agent, options, configLoader = loadImpo
25163
24930
  };
25164
24931
  try {
25165
24932
  return await withWorktree(directory, async (worktree) => {
25166
- logger.setContext("worktreeId", worktree.uuid);
25167
- logger.setContext("worktreePath", worktree.path);
25168
24933
  result.worktreeId = worktree.uuid;
25169
24934
  result.steps.worktree = buildStepResult(true, `Created worktree at ${worktree.path}`, {
25170
24935
  path: worktree.path,
25171
24936
  branch: worktree.branch
25172
24937
  });
25173
- logger.startSection("Step 1: Sync Files");
25174
- logger.logStep("Sync Files", "start");
25175
24938
  try {
25176
24939
  const config2 = configLoader(directory);
25177
24940
  const syncResult = syncCSVFilesToWorktree(directory, worktree.path, config2.paths.import);
25178
24941
  if (syncResult.synced.length === 0 && syncResult.errors.length === 0) {
25179
- logger.logStep("Sync Files", "success", "No CSV files to sync");
25180
24942
  result.steps.sync = buildStepResult(true, "No CSV files to sync", {
25181
24943
  synced: []
25182
24944
  });
25183
24945
  } else if (syncResult.errors.length > 0) {
25184
- logger.warn(`Synced ${syncResult.synced.length} file(s) with ${syncResult.errors.length} error(s)`);
25185
24946
  result.steps.sync = buildStepResult(true, `Synced ${syncResult.synced.length} file(s) with ${syncResult.errors.length} error(s)`, { synced: syncResult.synced, errors: syncResult.errors });
25186
24947
  } 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
- }
25191
24948
  result.steps.sync = buildStepResult(true, `Synced ${syncResult.synced.length} CSV file(s) to worktree`, { synced: syncResult.synced });
25192
24949
  }
25193
- logger.endSection();
25194
24950
  } catch (error45) {
25195
- logger.logStep("Sync Files", "error");
25196
- logger.error("Failed to sync CSV files", error45);
25197
- logger.endSection();
25198
24951
  const errorMsg = error45 instanceof Error ? error45.message : String(error45);
25199
24952
  result.steps.sync = buildStepResult(false, `Failed to sync CSV files: ${errorMsg}`, { synced: [], errors: [{ file: "unknown", error: errorMsg }] });
25200
24953
  }
25201
24954
  try {
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);
24955
+ await executeClassifyStep(context, worktree);
24956
+ await executeAccountDeclarationsStep(context, worktree);
24957
+ await executeDryRunStep(context, worktree);
24958
+ await executeImportStep(context, worktree);
24959
+ await executeReconcileStep(context, worktree);
25207
24960
  try {
25208
24961
  const config2 = configLoader(directory);
25209
24962
  const cleanupResult = cleanupProcessedCSVFiles(directory, config2.paths.import);
@@ -25228,7 +24981,7 @@ async function importPipeline(directory, agent, options, configLoader = loadImpo
25228
24981
  }
25229
24982
  });
25230
24983
  }
25231
- await executeMergeStep(context, worktree, logger);
24984
+ await executeMergeStep(context, worktree);
25232
24985
  const existingCleanup = result.steps.cleanup;
25233
24986
  if (existingCleanup) {
25234
24987
  existingCleanup.message += ", worktree cleaned up";
@@ -25238,30 +24991,10 @@ async function importPipeline(directory, agent, options, configLoader = loadImpo
25238
24991
  };
25239
24992
  }
25240
24993
  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();
25249
24994
  return buildSuccessResult4(result, `Successfully imported ${transactionCount} transaction(s)`);
25250
24995
  } catch (error45) {
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,
24996
+ result.steps.cleanup = buildStepResult(true, "Worktree cleaned up after failure (CSV files preserved for retry)", {
24997
+ cleanedAfterFailure: true,
25265
24998
  csvCleanup: { deleted: [] }
25266
24999
  });
25267
25000
  if (error45 instanceof NoTransactionsError) {
@@ -25272,18 +25005,11 @@ async function importPipeline(directory, agent, options, configLoader = loadImpo
25272
25005
  }
25273
25006
  return buildErrorResult5(result, result.error, result.hint);
25274
25007
  }
25275
- }, {
25276
- keepOnError: options.keepWorktreeOnError ?? true,
25277
- logger
25278
25008
  });
25279
25009
  } catch (error45) {
25280
- logger.error("Pipeline failed", error45);
25281
25010
  result.steps.worktree = buildStepResult(false, `Failed to create worktree: ${error45 instanceof Error ? error45.message : String(error45)}`);
25282
25011
  result.error = "Failed to create worktree";
25283
25012
  return buildErrorResult5(result, result.error);
25284
- } finally {
25285
- logger.endSection();
25286
- await logger.flush();
25287
25013
  }
25288
25014
  }
25289
25015
  var import_pipeline_default = tool({
@@ -25298,40 +25024,25 @@ This tool orchestrates the full import workflow in an isolated git worktree:
25298
25024
  4. **Import**: Imports transactions to the journal
25299
25025
  5. **Reconcile**: Validates closing balance matches CSV metadata
25300
25026
  6. **Merge**: Merges worktree to main with --no-ff
25301
- 7. **Cleanup**: Removes worktree (or preserves on error)
25027
+ 7. **Cleanup**: Removes worktree
25302
25028
 
25303
25029
  **Safety Features:**
25304
25030
  - All changes happen in isolated worktree
25305
- - If any step fails, worktree is preserved by default for debugging
25031
+ - If any step fails, worktree is discarded (main branch untouched)
25306
25032
  - Balance reconciliation ensures data integrity
25307
25033
  - Atomic commit with merge --no-ff preserves history
25308
25034
 
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
-
25322
25035
  **Usage:**
25323
25036
  - Basic: import-pipeline (processes all pending CSVs)
25324
25037
  - Filtered: import-pipeline --provider ubs --currency chf
25325
25038
  - With manual balance: import-pipeline --closingBalance "CHF 1234.56"
25326
- - Skip classify: import-pipeline --skipClassify true
25327
- - Always cleanup: import-pipeline --keepWorktreeOnError false`,
25039
+ - Skip classify: import-pipeline --skipClassify true`,
25328
25040
  args: {
25329
25041
  provider: tool.schema.string().optional().describe('Filter by provider (e.g., "ubs", "revolut")'),
25330
25042
  currency: tool.schema.string().optional().describe('Filter by currency (e.g., "chf", "eur")'),
25331
25043
  closingBalance: tool.schema.string().optional().describe("Manual closing balance override (if not in CSV metadata)"),
25332
25044
  account: tool.schema.string().optional().describe("Manual account override (auto-detected from rules file if not provided)"),
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)")
25045
+ skipClassify: tool.schema.boolean().optional().describe("Skip the classify step (default: false)")
25335
25046
  },
25336
25047
  async execute(params, context) {
25337
25048
  const { directory, agent } = context;
@@ -25340,22 +25051,21 @@ This tool orchestrates the full import workflow in an isolated git worktree:
25340
25051
  currency: params.currency,
25341
25052
  closingBalance: params.closingBalance,
25342
25053
  account: params.account,
25343
- skipClassify: params.skipClassify,
25344
- keepWorktreeOnError: params.keepWorktreeOnError
25054
+ skipClassify: params.skipClassify
25345
25055
  });
25346
25056
  }
25347
25057
  });
25348
25058
  // src/tools/init-directories.ts
25349
- import * as fs15 from "fs";
25350
- import * as path14 from "path";
25059
+ import * as fs14 from "fs";
25060
+ import * as path13 from "path";
25351
25061
  async function initDirectories(directory) {
25352
25062
  try {
25353
25063
  const config2 = loadImportConfig(directory);
25354
25064
  const directoriesCreated = [];
25355
25065
  const gitkeepFiles = [];
25356
- const importBase = path14.join(directory, "import");
25357
- if (!fs15.existsSync(importBase)) {
25358
- fs15.mkdirSync(importBase, { recursive: true });
25066
+ const importBase = path13.join(directory, "import");
25067
+ if (!fs14.existsSync(importBase)) {
25068
+ fs14.mkdirSync(importBase, { recursive: true });
25359
25069
  directoriesCreated.push("import");
25360
25070
  }
25361
25071
  const pathsToCreate = [
@@ -25365,20 +25075,20 @@ async function initDirectories(directory) {
25365
25075
  { key: "unrecognized", path: config2.paths.unrecognized }
25366
25076
  ];
25367
25077
  for (const { path: dirPath } of pathsToCreate) {
25368
- const fullPath = path14.join(directory, dirPath);
25369
- if (!fs15.existsSync(fullPath)) {
25370
- fs15.mkdirSync(fullPath, { recursive: true });
25078
+ const fullPath = path13.join(directory, dirPath);
25079
+ if (!fs14.existsSync(fullPath)) {
25080
+ fs14.mkdirSync(fullPath, { recursive: true });
25371
25081
  directoriesCreated.push(dirPath);
25372
25082
  }
25373
- const gitkeepPath = path14.join(fullPath, ".gitkeep");
25374
- if (!fs15.existsSync(gitkeepPath)) {
25375
- fs15.writeFileSync(gitkeepPath, "");
25376
- gitkeepFiles.push(path14.join(dirPath, ".gitkeep"));
25083
+ const gitkeepPath = path13.join(fullPath, ".gitkeep");
25084
+ if (!fs14.existsSync(gitkeepPath)) {
25085
+ fs14.writeFileSync(gitkeepPath, "");
25086
+ gitkeepFiles.push(path13.join(dirPath, ".gitkeep"));
25377
25087
  }
25378
25088
  }
25379
- const gitignorePath = path14.join(importBase, ".gitignore");
25089
+ const gitignorePath = path13.join(importBase, ".gitignore");
25380
25090
  let gitignoreCreated = false;
25381
- if (!fs15.existsSync(gitignorePath)) {
25091
+ if (!fs14.existsSync(gitignorePath)) {
25382
25092
  const gitignoreContent = `# Ignore CSV/PDF files in temporary directories
25383
25093
  /incoming/*.csv
25384
25094
  /incoming/*.pdf
@@ -25396,7 +25106,7 @@ async function initDirectories(directory) {
25396
25106
  .DS_Store
25397
25107
  Thumbs.db
25398
25108
  `;
25399
- fs15.writeFileSync(gitignorePath, gitignoreContent);
25109
+ fs14.writeFileSync(gitignorePath, gitignoreContent);
25400
25110
  gitignoreCreated = true;
25401
25111
  }
25402
25112
  const parts = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fuzzle/opencode-accountant",
3
- "version": "0.4.1-next.1",
3
+ "version": "0.4.1",
4
4
  "description": "An OpenCode accounting agent, specialized in double-entry-bookkepping with hledger",
5
5
  "author": {
6
6
  "name": "ali bengali",