@fuzzle/opencode-accountant 0.1.1 → 0.1.2-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.
Files changed (2) hide show
  1. package/dist/index.js +652 -88
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1342,18 +1342,18 @@ var require_papaparse = __commonJS((exports, module) => {
1342
1342
 
1343
1343
  // node_modules/convert-csv-to-json/src/util/fileUtils.js
1344
1344
  var require_fileUtils = __commonJS((exports, module) => {
1345
- var fs8 = __require("fs");
1345
+ var fs9 = __require("fs");
1346
1346
 
1347
1347
  class FileUtils {
1348
1348
  readFile(fileInputName, encoding) {
1349
- return fs8.readFileSync(fileInputName, encoding).toString();
1349
+ return fs9.readFileSync(fileInputName, encoding).toString();
1350
1350
  }
1351
1351
  readFileAsync(fileInputName, encoding = "utf8") {
1352
- if (fs8.promises && typeof fs8.promises.readFile === "function") {
1353
- return fs8.promises.readFile(fileInputName, encoding).then((buf) => buf.toString());
1352
+ if (fs9.promises && typeof fs9.promises.readFile === "function") {
1353
+ return fs9.promises.readFile(fileInputName, encoding).then((buf) => buf.toString());
1354
1354
  }
1355
1355
  return new Promise((resolve2, reject) => {
1356
- fs8.readFile(fileInputName, encoding, (err, data) => {
1356
+ fs9.readFile(fileInputName, encoding, (err, data) => {
1357
1357
  if (err) {
1358
1358
  reject(err);
1359
1359
  return;
@@ -1363,7 +1363,7 @@ var require_fileUtils = __commonJS((exports, module) => {
1363
1363
  });
1364
1364
  }
1365
1365
  writeFile(json3, fileOutputName) {
1366
- fs8.writeFile(fileOutputName, json3, function(err) {
1366
+ fs9.writeFile(fileOutputName, json3, function(err) {
1367
1367
  if (err) {
1368
1368
  throw err;
1369
1369
  } else {
@@ -1372,11 +1372,11 @@ var require_fileUtils = __commonJS((exports, module) => {
1372
1372
  });
1373
1373
  }
1374
1374
  writeFileAsync(json3, fileOutputName) {
1375
- if (fs8.promises && typeof fs8.promises.writeFile === "function") {
1376
- return fs8.promises.writeFile(fileOutputName, json3);
1375
+ if (fs9.promises && typeof fs9.promises.writeFile === "function") {
1376
+ return fs9.promises.writeFile(fileOutputName, json3);
1377
1377
  }
1378
1378
  return new Promise((resolve2, reject) => {
1379
- fs8.writeFile(fileOutputName, json3, (err) => {
1379
+ fs9.writeFile(fileOutputName, json3, (err) => {
1380
1380
  if (err)
1381
1381
  return reject(err);
1382
1382
  resolve2();
@@ -1941,7 +1941,7 @@ var require_convert_csv_to_json = __commonJS((exports) => {
1941
1941
  });
1942
1942
 
1943
1943
  // src/index.ts
1944
- import { dirname as dirname5, join as join10 } from "path";
1944
+ import { dirname as dirname6, join as join13 } from "path";
1945
1945
  import { fileURLToPath } from "url";
1946
1946
 
1947
1947
  // src/utils/agentLoader.ts
@@ -17273,8 +17273,8 @@ var fetch_currency_prices_default = tool({
17273
17273
  }
17274
17274
  });
17275
17275
  // src/tools/classify-statements.ts
17276
- import * as fs5 from "fs";
17277
- import * as path6 from "path";
17276
+ import * as fs6 from "fs";
17277
+ import * as path7 from "path";
17278
17278
 
17279
17279
  // src/utils/importConfig.ts
17280
17280
  import * as fs3 from "fs";
@@ -17543,6 +17543,63 @@ function detectProvider(filename, content, config2) {
17543
17543
 
17544
17544
  // src/utils/worktreeManager.ts
17545
17545
  import { spawnSync } from "child_process";
17546
+
17547
+ // node_modules/uuid/dist-node/stringify.js
17548
+ var byteToHex = [];
17549
+ for (let i2 = 0;i2 < 256; ++i2) {
17550
+ byteToHex.push((i2 + 256).toString(16).slice(1));
17551
+ }
17552
+ function unsafeStringify(arr, offset = 0) {
17553
+ return (byteToHex[arr[offset + 0]] + byteToHex[arr[offset + 1]] + byteToHex[arr[offset + 2]] + byteToHex[arr[offset + 3]] + "-" + byteToHex[arr[offset + 4]] + byteToHex[arr[offset + 5]] + "-" + byteToHex[arr[offset + 6]] + byteToHex[arr[offset + 7]] + "-" + byteToHex[arr[offset + 8]] + byteToHex[arr[offset + 9]] + "-" + byteToHex[arr[offset + 10]] + byteToHex[arr[offset + 11]] + byteToHex[arr[offset + 12]] + byteToHex[arr[offset + 13]] + byteToHex[arr[offset + 14]] + byteToHex[arr[offset + 15]]).toLowerCase();
17554
+ }
17555
+
17556
+ // node_modules/uuid/dist-node/rng.js
17557
+ import { randomFillSync } from "crypto";
17558
+ var rnds8Pool = new Uint8Array(256);
17559
+ var poolPtr = rnds8Pool.length;
17560
+ function rng() {
17561
+ if (poolPtr > rnds8Pool.length - 16) {
17562
+ randomFillSync(rnds8Pool);
17563
+ poolPtr = 0;
17564
+ }
17565
+ return rnds8Pool.slice(poolPtr, poolPtr += 16);
17566
+ }
17567
+
17568
+ // node_modules/uuid/dist-node/native.js
17569
+ import { randomUUID } from "crypto";
17570
+ var native_default = { randomUUID };
17571
+
17572
+ // node_modules/uuid/dist-node/v4.js
17573
+ function _v4(options, buf, offset) {
17574
+ options = options || {};
17575
+ const rnds = options.random ?? options.rng?.() ?? rng();
17576
+ if (rnds.length < 16) {
17577
+ throw new Error("Random bytes length must be >= 16");
17578
+ }
17579
+ rnds[6] = rnds[6] & 15 | 64;
17580
+ rnds[8] = rnds[8] & 63 | 128;
17581
+ if (buf) {
17582
+ offset = offset || 0;
17583
+ if (offset < 0 || offset + 16 > buf.length) {
17584
+ throw new RangeError(`UUID byte range ${offset}:${offset + 15} is out of buffer bounds`);
17585
+ }
17586
+ for (let i2 = 0;i2 < 16; ++i2) {
17587
+ buf[offset + i2] = rnds[i2];
17588
+ }
17589
+ return buf;
17590
+ }
17591
+ return unsafeStringify(rnds);
17592
+ }
17593
+ function v4(options, buf, offset) {
17594
+ if (native_default.randomUUID && !buf && !options) {
17595
+ return native_default.randomUUID();
17596
+ }
17597
+ return _v4(options, buf, offset);
17598
+ }
17599
+ var v4_default = v4;
17600
+ // src/utils/worktreeManager.ts
17601
+ import * as fs4 from "fs";
17602
+ import * as path5 from "path";
17546
17603
  function execGit(args, cwd) {
17547
17604
  const result = spawnSync("git", args, { cwd, encoding: "utf-8" });
17548
17605
  if (result.status !== 0) {
@@ -17550,6 +17607,92 @@ function execGit(args, cwd) {
17550
17607
  }
17551
17608
  return (result.stdout || "").trim();
17552
17609
  }
17610
+ function execGitSafe(args, cwd) {
17611
+ const result = spawnSync("git", args, { cwd, encoding: "utf-8" });
17612
+ if (result.status !== 0) {
17613
+ return { success: false, output: result.stderr || result.stdout || `git ${args[0]} failed` };
17614
+ }
17615
+ return { success: true, output: (result.stdout || "").trim() };
17616
+ }
17617
+ function copyIncomingFiles(mainRepoPath, worktreePath) {
17618
+ const sourceDir = path5.join(mainRepoPath, "import/incoming");
17619
+ const targetDir = path5.join(worktreePath, "import/incoming");
17620
+ if (!fs4.existsSync(sourceDir)) {
17621
+ return;
17622
+ }
17623
+ fs4.mkdirSync(targetDir, { recursive: true });
17624
+ const entries = fs4.readdirSync(sourceDir, { withFileTypes: true });
17625
+ let copiedCount = 0;
17626
+ for (const entry of entries) {
17627
+ if (entry.isFile() && !entry.name.startsWith(".")) {
17628
+ const srcPath = path5.join(sourceDir, entry.name);
17629
+ const destPath = path5.join(targetDir, entry.name);
17630
+ fs4.copyFileSync(srcPath, destPath);
17631
+ copiedCount++;
17632
+ }
17633
+ }
17634
+ if (copiedCount > 0) {
17635
+ console.log(`[INFO] Copied ${copiedCount} file(s) from import/incoming/ to worktree`);
17636
+ }
17637
+ }
17638
+ function createImportWorktree(mainRepoPath, options = {}) {
17639
+ const baseDir = options.baseDir ?? "/tmp";
17640
+ const uuid3 = v4_default();
17641
+ const branch = `import-${uuid3}`;
17642
+ const worktreePath = path5.join(baseDir, `import-worktree-${uuid3}`);
17643
+ try {
17644
+ execGit(["rev-parse", "--git-dir"], mainRepoPath);
17645
+ } catch {
17646
+ throw new Error(`Not a git repository: ${mainRepoPath}`);
17647
+ }
17648
+ execGit(["branch", branch], mainRepoPath);
17649
+ try {
17650
+ execGit(["worktree", "add", worktreePath, branch], mainRepoPath);
17651
+ } catch (error45) {
17652
+ execGitSafe(["branch", "-D", branch], mainRepoPath);
17653
+ throw error45;
17654
+ }
17655
+ copyIncomingFiles(mainRepoPath, worktreePath);
17656
+ return {
17657
+ path: worktreePath,
17658
+ branch,
17659
+ uuid: uuid3,
17660
+ mainRepoPath
17661
+ };
17662
+ }
17663
+ function mergeWorktree(context, commitMessage) {
17664
+ const status = execGit(["status", "--porcelain"], context.path);
17665
+ if (status.length > 0) {
17666
+ execGit(["add", "-A"], context.path);
17667
+ execGit(["commit", "-m", commitMessage], context.path);
17668
+ }
17669
+ const currentBranch = execGit(["rev-parse", "--abbrev-ref", "HEAD"], context.mainRepoPath);
17670
+ execGit(["merge", "--no-ff", context.branch, "-m", commitMessage], context.mainRepoPath);
17671
+ if (currentBranch !== "main" && currentBranch !== "master") {
17672
+ execGit(["checkout", currentBranch], context.mainRepoPath);
17673
+ }
17674
+ }
17675
+ function removeWorktree(context, force = false) {
17676
+ const forceFlag = force ? "--force" : "";
17677
+ const args = ["worktree", "remove", context.path];
17678
+ if (forceFlag) {
17679
+ args.push(forceFlag);
17680
+ }
17681
+ const removeResult = execGitSafe(args, context.mainRepoPath);
17682
+ if (!removeResult.success) {
17683
+ if (!fs4.existsSync(context.path)) {} else {
17684
+ return { success: false, error: `Failed to remove worktree: ${removeResult.output}` };
17685
+ }
17686
+ }
17687
+ execGitSafe(["worktree", "prune"], context.mainRepoPath);
17688
+ const branchResult = execGitSafe(["branch", "-D", context.branch], context.mainRepoPath);
17689
+ if (!branchResult.success) {
17690
+ if (!branchResult.output.includes("not found")) {
17691
+ return { success: false, error: `Failed to delete branch: ${branchResult.output}` };
17692
+ }
17693
+ }
17694
+ return { success: true };
17695
+ }
17553
17696
  function isInWorktree(directory) {
17554
17697
  try {
17555
17698
  const gitDir = execGit(["rev-parse", "--git-dir"], directory);
@@ -17558,22 +17701,34 @@ function isInWorktree(directory) {
17558
17701
  return false;
17559
17702
  }
17560
17703
  }
17704
+ async function withWorktree(directory, operation) {
17705
+ let createdWorktree = null;
17706
+ try {
17707
+ createdWorktree = createImportWorktree(directory);
17708
+ const result = await operation(createdWorktree);
17709
+ return result;
17710
+ } finally {
17711
+ if (createdWorktree) {
17712
+ removeWorktree(createdWorktree, true);
17713
+ }
17714
+ }
17715
+ }
17561
17716
 
17562
17717
  // src/utils/fileUtils.ts
17563
- import * as fs4 from "fs";
17564
- import * as path5 from "path";
17718
+ import * as fs5 from "fs";
17719
+ import * as path6 from "path";
17565
17720
  function findCSVFiles(importsDir) {
17566
- if (!fs4.existsSync(importsDir)) {
17721
+ if (!fs5.existsSync(importsDir)) {
17567
17722
  return [];
17568
17723
  }
17569
- return fs4.readdirSync(importsDir).filter((file2) => file2.toLowerCase().endsWith(".csv")).filter((file2) => {
17570
- const fullPath = path5.join(importsDir, file2);
17571
- return fs4.statSync(fullPath).isFile();
17724
+ return fs5.readdirSync(importsDir).filter((file2) => file2.toLowerCase().endsWith(".csv")).filter((file2) => {
17725
+ const fullPath = path6.join(importsDir, file2);
17726
+ return fs5.statSync(fullPath).isFile();
17572
17727
  });
17573
17728
  }
17574
17729
  function ensureDirectory(dirPath) {
17575
- if (!fs4.existsSync(dirPath)) {
17576
- fs4.mkdirSync(dirPath, { recursive: true });
17730
+ if (!fs5.existsSync(dirPath)) {
17731
+ fs5.mkdirSync(dirPath, { recursive: true });
17577
17732
  }
17578
17733
  }
17579
17734
 
@@ -17614,20 +17769,20 @@ function planMoves(csvFiles, importsDir, pendingDir, unrecognizedDir, config2) {
17614
17769
  const plannedMoves = [];
17615
17770
  const collisions = [];
17616
17771
  for (const filename of csvFiles) {
17617
- const sourcePath = path6.join(importsDir, filename);
17618
- const content = fs5.readFileSync(sourcePath, "utf-8");
17772
+ const sourcePath = path7.join(importsDir, filename);
17773
+ const content = fs6.readFileSync(sourcePath, "utf-8");
17619
17774
  const detection = detectProvider(filename, content, config2);
17620
17775
  let targetPath;
17621
17776
  let targetFilename;
17622
17777
  if (detection) {
17623
17778
  targetFilename = detection.outputFilename || filename;
17624
- const targetDir = path6.join(pendingDir, detection.provider, detection.currency);
17625
- targetPath = path6.join(targetDir, targetFilename);
17779
+ const targetDir = path7.join(pendingDir, detection.provider, detection.currency);
17780
+ targetPath = path7.join(targetDir, targetFilename);
17626
17781
  } else {
17627
17782
  targetFilename = filename;
17628
- targetPath = path6.join(unrecognizedDir, filename);
17783
+ targetPath = path7.join(unrecognizedDir, filename);
17629
17784
  }
17630
- if (fs5.existsSync(targetPath)) {
17785
+ if (fs6.existsSync(targetPath)) {
17631
17786
  collisions.push({
17632
17787
  filename,
17633
17788
  existingPath: targetPath
@@ -17648,22 +17803,22 @@ function executeMoves(plannedMoves, config2, unrecognizedDir) {
17648
17803
  const unrecognized = [];
17649
17804
  for (const move of plannedMoves) {
17650
17805
  if (move.detection) {
17651
- const targetDir = path6.dirname(move.targetPath);
17806
+ const targetDir = path7.dirname(move.targetPath);
17652
17807
  ensureDirectory(targetDir);
17653
- fs5.renameSync(move.sourcePath, move.targetPath);
17808
+ fs6.renameSync(move.sourcePath, move.targetPath);
17654
17809
  classified.push({
17655
17810
  filename: move.targetFilename,
17656
17811
  originalFilename: move.detection.outputFilename ? move.filename : undefined,
17657
17812
  provider: move.detection.provider,
17658
17813
  currency: move.detection.currency,
17659
- targetPath: path6.join(config2.paths.pending, move.detection.provider, move.detection.currency, move.targetFilename)
17814
+ targetPath: path7.join(config2.paths.pending, move.detection.provider, move.detection.currency, move.targetFilename)
17660
17815
  });
17661
17816
  } else {
17662
17817
  ensureDirectory(unrecognizedDir);
17663
- fs5.renameSync(move.sourcePath, move.targetPath);
17818
+ fs6.renameSync(move.sourcePath, move.targetPath);
17664
17819
  unrecognized.push({
17665
17820
  filename: move.filename,
17666
- targetPath: path6.join(config2.paths.unrecognized, move.filename)
17821
+ targetPath: path7.join(config2.paths.unrecognized, move.filename)
17667
17822
  });
17668
17823
  }
17669
17824
  }
@@ -17687,9 +17842,9 @@ async function classifyStatements(directory, agent, configLoader = loadImportCon
17687
17842
  const errorMessage = err instanceof Error ? err.message : String(err);
17688
17843
  return buildErrorResult2(errorMessage);
17689
17844
  }
17690
- const importsDir = path6.join(directory, config2.paths.import);
17691
- const pendingDir = path6.join(directory, config2.paths.pending);
17692
- const unrecognizedDir = path6.join(directory, config2.paths.unrecognized);
17845
+ const importsDir = path7.join(directory, config2.paths.import);
17846
+ const pendingDir = path7.join(directory, config2.paths.pending);
17847
+ const unrecognizedDir = path7.join(directory, config2.paths.unrecognized);
17693
17848
  const csvFiles = findCSVFiles(importsDir);
17694
17849
  if (csvFiles.length === 0) {
17695
17850
  return buildSuccessResult2([], [], `No CSV files found in ${config2.paths.import}`);
@@ -17710,12 +17865,12 @@ var classify_statements_default = tool({
17710
17865
  }
17711
17866
  });
17712
17867
  // src/tools/import-statements.ts
17713
- import * as fs9 from "fs";
17714
- import * as path8 from "path";
17868
+ import * as fs10 from "fs";
17869
+ import * as path9 from "path";
17715
17870
 
17716
17871
  // src/utils/rulesMatcher.ts
17717
- import * as fs6 from "fs";
17718
- import * as path7 from "path";
17872
+ import * as fs7 from "fs";
17873
+ import * as path8 from "path";
17719
17874
  function parseSourceDirective(content) {
17720
17875
  const match = content.match(/^source\s+([^\n#]+)/m);
17721
17876
  if (!match) {
@@ -17724,28 +17879,28 @@ function parseSourceDirective(content) {
17724
17879
  return match[1].trim();
17725
17880
  }
17726
17881
  function resolveSourcePath(sourcePath, rulesFilePath) {
17727
- if (path7.isAbsolute(sourcePath)) {
17882
+ if (path8.isAbsolute(sourcePath)) {
17728
17883
  return sourcePath;
17729
17884
  }
17730
- const rulesDir = path7.dirname(rulesFilePath);
17731
- return path7.resolve(rulesDir, sourcePath);
17885
+ const rulesDir = path8.dirname(rulesFilePath);
17886
+ return path8.resolve(rulesDir, sourcePath);
17732
17887
  }
17733
17888
  function loadRulesMapping(rulesDir) {
17734
17889
  const mapping = {};
17735
- if (!fs6.existsSync(rulesDir)) {
17890
+ if (!fs7.existsSync(rulesDir)) {
17736
17891
  return mapping;
17737
17892
  }
17738
- const files = fs6.readdirSync(rulesDir);
17893
+ const files = fs7.readdirSync(rulesDir);
17739
17894
  for (const file2 of files) {
17740
17895
  if (!file2.endsWith(".rules")) {
17741
17896
  continue;
17742
17897
  }
17743
- const rulesFilePath = path7.join(rulesDir, file2);
17744
- const stat = fs6.statSync(rulesFilePath);
17898
+ const rulesFilePath = path8.join(rulesDir, file2);
17899
+ const stat = fs7.statSync(rulesFilePath);
17745
17900
  if (!stat.isFile()) {
17746
17901
  continue;
17747
17902
  }
17748
- const content = fs6.readFileSync(rulesFilePath, "utf-8");
17903
+ const content = fs7.readFileSync(rulesFilePath, "utf-8");
17749
17904
  const sourcePath = parseSourceDirective(content);
17750
17905
  if (!sourcePath) {
17751
17906
  continue;
@@ -17759,9 +17914,9 @@ function findRulesForCsv(csvPath, mapping) {
17759
17914
  if (mapping[csvPath]) {
17760
17915
  return mapping[csvPath];
17761
17916
  }
17762
- const normalizedCsvPath = path7.normalize(csvPath);
17917
+ const normalizedCsvPath = path8.normalize(csvPath);
17763
17918
  for (const [mappedCsv, rulesFile] of Object.entries(mapping)) {
17764
- if (path7.normalize(mappedCsv) === normalizedCsvPath) {
17919
+ if (path8.normalize(mappedCsv) === normalizedCsvPath) {
17765
17920
  return rulesFile;
17766
17921
  }
17767
17922
  }
@@ -17887,7 +18042,7 @@ async function getAccountBalance(mainJournalPath, account, asOfDate, executor =
17887
18042
  }
17888
18043
 
17889
18044
  // src/utils/rulesParser.ts
17890
- import * as fs7 from "fs";
18045
+ import * as fs8 from "fs";
17891
18046
  function parseSkipRows(rulesContent) {
17892
18047
  const match = rulesContent.match(/^skip\s+(\d+)/m);
17893
18048
  return match ? parseInt(match[1], 10) : 0;
@@ -17953,7 +18108,7 @@ function parseAccount1(rulesContent) {
17953
18108
  }
17954
18109
  function getAccountFromRulesFile(rulesFilePath) {
17955
18110
  try {
17956
- const content = fs7.readFileSync(rulesFilePath, "utf-8");
18111
+ const content = fs8.readFileSync(rulesFilePath, "utf-8");
17957
18112
  return parseAccount1(content);
17958
18113
  } catch {
17959
18114
  return null;
@@ -17973,7 +18128,7 @@ function parseRulesFile(rulesContent) {
17973
18128
 
17974
18129
  // src/utils/csvParser.ts
17975
18130
  var import_convert_csv_to_json = __toESM(require_convert_csv_to_json(), 1);
17976
- import * as fs8 from "fs";
18131
+ import * as fs9 from "fs";
17977
18132
 
17978
18133
  // src/utils/balanceUtils.ts
17979
18134
  function parseAmountValue(amountStr) {
@@ -18022,7 +18177,7 @@ function balancesMatch(balance1, balance2) {
18022
18177
 
18023
18178
  // src/utils/csvParser.ts
18024
18179
  function parseCsvFile(csvPath, config2) {
18025
- const csvContent = fs8.readFileSync(csvPath, "utf-8");
18180
+ const csvContent = fs9.readFileSync(csvPath, "utf-8");
18026
18181
  const lines = csvContent.split(`
18027
18182
  `);
18028
18183
  const headerIndex = config2.skipRows;
@@ -18182,8 +18337,8 @@ function buildSuccessResult3(files, summary, message) {
18182
18337
  async function executeImports(fileResults, directory, pendingDir, doneDir, hledgerExecutor) {
18183
18338
  const importedFiles = [];
18184
18339
  for (const fileResult of fileResults) {
18185
- const csvFile = path8.join(directory, fileResult.csv);
18186
- const rulesFile = fileResult.rulesFile ? path8.join(directory, fileResult.rulesFile) : null;
18340
+ const csvFile = path9.join(directory, fileResult.csv);
18341
+ const rulesFile = fileResult.rulesFile ? path9.join(directory, fileResult.rulesFile) : null;
18187
18342
  if (!rulesFile)
18188
18343
  continue;
18189
18344
  const year = fileResult.transactionYear;
@@ -18219,7 +18374,7 @@ async function executeImports(fileResults, directory, pendingDir, doneDir, hledg
18219
18374
  }
18220
18375
  importedFiles.push(csvFile);
18221
18376
  }
18222
- const mainJournalPath = path8.join(directory, ".hledger.journal");
18377
+ const mainJournalPath = path9.join(directory, ".hledger.journal");
18223
18378
  const validationResult = await validateLedger(mainJournalPath, hledgerExecutor);
18224
18379
  if (!validationResult.valid) {
18225
18380
  return {
@@ -18229,13 +18384,13 @@ async function executeImports(fileResults, directory, pendingDir, doneDir, hledg
18229
18384
  };
18230
18385
  }
18231
18386
  for (const csvFile of importedFiles) {
18232
- const relativePath = path8.relative(pendingDir, csvFile);
18233
- const destPath = path8.join(doneDir, relativePath);
18234
- const destDir = path8.dirname(destPath);
18235
- if (!fs9.existsSync(destDir)) {
18236
- fs9.mkdirSync(destDir, { recursive: true });
18387
+ const relativePath = path9.relative(pendingDir, csvFile);
18388
+ const destPath = path9.join(doneDir, relativePath);
18389
+ const destDir = path9.dirname(destPath);
18390
+ if (!fs10.existsSync(destDir)) {
18391
+ fs10.mkdirSync(destDir, { recursive: true });
18237
18392
  }
18238
- fs9.renameSync(csvFile, destPath);
18393
+ fs10.renameSync(csvFile, destPath);
18239
18394
  }
18240
18395
  return {
18241
18396
  success: true,
@@ -18246,7 +18401,7 @@ async function processCsvFile(csvFile, rulesMapping, directory, hledgerExecutor)
18246
18401
  const rulesFile = findRulesForCsv(csvFile, rulesMapping);
18247
18402
  if (!rulesFile) {
18248
18403
  return {
18249
- csv: path8.relative(directory, csvFile),
18404
+ csv: path9.relative(directory, csvFile),
18250
18405
  rulesFile: null,
18251
18406
  totalTransactions: 0,
18252
18407
  matchedTransactions: 0,
@@ -18257,8 +18412,8 @@ async function processCsvFile(csvFile, rulesMapping, directory, hledgerExecutor)
18257
18412
  const result = await hledgerExecutor(["print", "-f", csvFile, "--rules-file", rulesFile]);
18258
18413
  if (result.exitCode !== 0) {
18259
18414
  return {
18260
- csv: path8.relative(directory, csvFile),
18261
- rulesFile: path8.relative(directory, rulesFile),
18415
+ csv: path9.relative(directory, csvFile),
18416
+ rulesFile: path9.relative(directory, rulesFile),
18262
18417
  totalTransactions: 0,
18263
18418
  matchedTransactions: 0,
18264
18419
  unknownPostings: [],
@@ -18272,8 +18427,8 @@ async function processCsvFile(csvFile, rulesMapping, directory, hledgerExecutor)
18272
18427
  if (years.size > 1) {
18273
18428
  const yearList = Array.from(years).sort().join(", ");
18274
18429
  return {
18275
- csv: path8.relative(directory, csvFile),
18276
- rulesFile: path8.relative(directory, rulesFile),
18430
+ csv: path9.relative(directory, csvFile),
18431
+ rulesFile: path9.relative(directory, rulesFile),
18277
18432
  totalTransactions: transactionCount,
18278
18433
  matchedTransactions: matchedCount,
18279
18434
  unknownPostings: [],
@@ -18283,7 +18438,7 @@ async function processCsvFile(csvFile, rulesMapping, directory, hledgerExecutor)
18283
18438
  const transactionYear = years.size === 1 ? Array.from(years)[0] : undefined;
18284
18439
  if (unknownPostings.length > 0) {
18285
18440
  try {
18286
- const rulesContent = fs9.readFileSync(rulesFile, "utf-8");
18441
+ const rulesContent = fs10.readFileSync(rulesFile, "utf-8");
18287
18442
  const rulesConfig = parseRulesFile(rulesContent);
18288
18443
  const csvRows = parseCsvFile(csvFile, rulesConfig);
18289
18444
  for (const posting of unknownPostings) {
@@ -18300,8 +18455,8 @@ async function processCsvFile(csvFile, rulesMapping, directory, hledgerExecutor)
18300
18455
  }
18301
18456
  }
18302
18457
  return {
18303
- csv: path8.relative(directory, csvFile),
18304
- rulesFile: path8.relative(directory, rulesFile),
18458
+ csv: path9.relative(directory, csvFile),
18459
+ rulesFile: path9.relative(directory, rulesFile),
18305
18460
  totalTransactions: transactionCount,
18306
18461
  matchedTransactions: matchedCount,
18307
18462
  unknownPostings,
@@ -18323,9 +18478,9 @@ async function importStatements(directory, agent, options, configLoader = loadIm
18323
18478
  const errorMessage = `Failed to load configuration: ${error45 instanceof Error ? error45.message : String(error45)}`;
18324
18479
  return buildErrorResult3(errorMessage, 'Ensure config/import/providers.yaml exists with required paths including "rules"');
18325
18480
  }
18326
- const pendingDir = path8.join(directory, config2.paths.pending);
18327
- const rulesDir = path8.join(directory, config2.paths.rules);
18328
- const doneDir = path8.join(directory, config2.paths.done);
18481
+ const pendingDir = path9.join(directory, config2.paths.pending);
18482
+ const rulesDir = path9.join(directory, config2.paths.rules);
18483
+ const doneDir = path9.join(directory, config2.paths.done);
18329
18484
  const rulesMapping = loadRulesMapping(rulesDir);
18330
18485
  const csvFiles = findCsvFiles(pendingDir, options.provider, options.currency);
18331
18486
  if (csvFiles.length === 0) {
@@ -18450,8 +18605,8 @@ This tool processes CSV files in the pending import directory and uses hledger's
18450
18605
  }
18451
18606
  });
18452
18607
  // src/tools/reconcile-statement.ts
18453
- import * as fs10 from "fs";
18454
- import * as path9 from "path";
18608
+ import * as fs11 from "fs";
18609
+ import * as path10 from "path";
18455
18610
  function buildErrorResult4(params) {
18456
18611
  return JSON.stringify({
18457
18612
  success: false,
@@ -18499,14 +18654,14 @@ function findCsvToReconcile(doneDir, options) {
18499
18654
  };
18500
18655
  }
18501
18656
  const csvFile = csvFiles[csvFiles.length - 1];
18502
- const relativePath = path9.relative(path9.dirname(path9.dirname(doneDir)), csvFile);
18657
+ const relativePath = path10.relative(path10.dirname(path10.dirname(doneDir)), csvFile);
18503
18658
  return { csvFile, relativePath };
18504
18659
  }
18505
18660
  function determineClosingBalance(csvFile, config2, options, relativeCsvPath) {
18506
18661
  let metadata;
18507
18662
  try {
18508
- const content = fs10.readFileSync(csvFile, "utf-8");
18509
- const filename = path9.basename(csvFile);
18663
+ const content = fs11.readFileSync(csvFile, "utf-8");
18664
+ const filename = path10.basename(csvFile);
18510
18665
  const detectionResult = detectProvider(filename, content, config2);
18511
18666
  metadata = detectionResult?.metadata;
18512
18667
  } catch {
@@ -18514,9 +18669,10 @@ function determineClosingBalance(csvFile, config2, options, relativeCsvPath) {
18514
18669
  }
18515
18670
  let closingBalance = options.closingBalance;
18516
18671
  if (!closingBalance && metadata?.closing_balance) {
18517
- closingBalance = metadata.closing_balance;
18518
- if (metadata.currency && !closingBalance.includes(metadata.currency)) {
18519
- closingBalance = `${metadata.currency} ${closingBalance}`;
18672
+ const { closing_balance, currency } = metadata;
18673
+ closingBalance = closing_balance;
18674
+ if (currency && !closingBalance.includes(currency)) {
18675
+ closingBalance = `${currency} ${closingBalance}`;
18520
18676
  }
18521
18677
  }
18522
18678
  if (!closingBalance) {
@@ -18555,7 +18711,7 @@ function determineAccount(csvFile, rulesDir, options, relativeCsvPath, metadata)
18555
18711
  }
18556
18712
  return { account };
18557
18713
  }
18558
- async function reconcileStatementCore(directory, agent, options, configLoader = loadImportConfig, hledgerExecutor = defaultHledgerExecutor, worktreeChecker = isInWorktree) {
18714
+ async function reconcileStatement(directory, agent, options, configLoader = loadImportConfig, hledgerExecutor = defaultHledgerExecutor, worktreeChecker = isInWorktree) {
18559
18715
  const restrictionError = checkAccountantAgent(agent, "reconcile statement");
18560
18716
  if (restrictionError) {
18561
18717
  return restrictionError;
@@ -18569,9 +18725,9 @@ async function reconcileStatementCore(directory, agent, options, configLoader =
18569
18725
  return configResult.error;
18570
18726
  }
18571
18727
  const { config: config2 } = configResult;
18572
- const doneDir = path9.join(directory, config2.paths.done);
18573
- const rulesDir = path9.join(directory, config2.paths.rules);
18574
- const mainJournalPath = path9.join(directory, ".hledger.journal");
18728
+ const doneDir = path10.join(directory, config2.paths.done);
18729
+ const rulesDir = path10.join(directory, config2.paths.rules);
18730
+ const mainJournalPath = path10.join(directory, ".hledger.journal");
18575
18731
  const csvResult = findCsvToReconcile(doneDir, options);
18576
18732
  if ("error" in csvResult) {
18577
18733
  return csvResult.error;
@@ -18686,7 +18842,7 @@ It must be run inside an import worktree (use import-pipeline for the full workf
18686
18842
  },
18687
18843
  async execute(params, context) {
18688
18844
  const { directory, agent } = context;
18689
- return reconcileStatementCore(directory, agent, {
18845
+ return reconcileStatement(directory, agent, {
18690
18846
  provider: params.provider,
18691
18847
  currency: params.currency,
18692
18848
  closingBalance: params.closingBalance,
@@ -18694,9 +18850,416 @@ It must be run inside an import worktree (use import-pipeline for the full workf
18694
18850
  });
18695
18851
  }
18696
18852
  });
18853
+ // src/tools/import-pipeline.ts
18854
+ import * as fs12 from "fs";
18855
+ import * as path11 from "path";
18856
+ class NoTransactionsError extends Error {
18857
+ constructor() {
18858
+ super("No transactions to import");
18859
+ this.name = "NoTransactionsError";
18860
+ }
18861
+ }
18862
+ function buildStepResult(success2, message, details) {
18863
+ const result = { success: success2, message };
18864
+ if (details !== undefined) {
18865
+ result.details = details;
18866
+ }
18867
+ return result;
18868
+ }
18869
+ function buildSuccessResult5(result, summary) {
18870
+ result.success = true;
18871
+ result.summary = summary;
18872
+ return JSON.stringify(result);
18873
+ }
18874
+ function buildErrorResult5(result, error45, hint) {
18875
+ result.success = false;
18876
+ result.error = error45;
18877
+ if (hint) {
18878
+ result.hint = hint;
18879
+ }
18880
+ return JSON.stringify(result);
18881
+ }
18882
+ function buildCommitMessage(provider, currency, fromDate, untilDate, transactionCount) {
18883
+ const providerStr = provider?.toUpperCase() || "statements";
18884
+ const currencyStr = currency?.toUpperCase();
18885
+ const dateRange = fromDate && untilDate ? ` ${fromDate} to ${untilDate}` : "";
18886
+ const txStr = transactionCount > 0 ? ` (${transactionCount} transactions)` : "";
18887
+ const parts = ["Import:", providerStr];
18888
+ if (currencyStr) {
18889
+ parts.push(currencyStr);
18890
+ }
18891
+ return `${parts.join(" ")}${dateRange}${txStr}`;
18892
+ }
18893
+ function cleanupIncomingFiles(worktree, context) {
18894
+ const incomingDir = path11.join(worktree.mainRepoPath, "import/incoming");
18895
+ if (!fs12.existsSync(incomingDir)) {
18896
+ return;
18897
+ }
18898
+ const importStep = context.result.steps.import;
18899
+ if (!importStep?.success || !importStep.details) {
18900
+ return;
18901
+ }
18902
+ const importResult = importStep.details;
18903
+ if (!importResult.files || !Array.isArray(importResult.files)) {
18904
+ return;
18905
+ }
18906
+ let deletedCount = 0;
18907
+ for (const fileResult of importResult.files) {
18908
+ if (!fileResult.csv)
18909
+ continue;
18910
+ const filename = path11.basename(fileResult.csv);
18911
+ const filePath = path11.join(incomingDir, filename);
18912
+ if (fs12.existsSync(filePath)) {
18913
+ try {
18914
+ fs12.unlinkSync(filePath);
18915
+ deletedCount++;
18916
+ } catch (error45) {
18917
+ console.error(`[ERROR] Failed to delete ${filename}: ${error45 instanceof Error ? error45.message : String(error45)}`);
18918
+ }
18919
+ }
18920
+ }
18921
+ if (deletedCount > 0) {
18922
+ console.log(`[INFO] Cleaned up ${deletedCount} file(s) from import/incoming/`);
18923
+ }
18924
+ }
18925
+ async function executeClassifyStep(context, worktree) {
18926
+ if (context.options.skipClassify) {
18927
+ context.result.steps.classify = buildStepResult(true, "Classification skipped (skipClassify: true)");
18928
+ return;
18929
+ }
18930
+ const inWorktree = () => true;
18931
+ const classifyResult = await classifyStatements(worktree.path, context.agent, context.configLoader, inWorktree);
18932
+ const classifyParsed = JSON.parse(classifyResult);
18933
+ const success2 = classifyParsed.success !== false;
18934
+ let message = success2 ? "Classification complete" : "Classification had issues";
18935
+ if (classifyParsed.unrecognized?.length > 0) {
18936
+ message = `Classification complete with ${classifyParsed.unrecognized.length} unrecognized file(s)`;
18937
+ }
18938
+ const details = {
18939
+ success: success2,
18940
+ unrecognized: classifyParsed.unrecognized,
18941
+ classified: classifyParsed
18942
+ };
18943
+ context.result.steps.classify = buildStepResult(success2, message, details);
18944
+ }
18945
+ async function executeDryRunStep(context, worktree) {
18946
+ const inWorktree = () => true;
18947
+ const dryRunResult = await importStatements(worktree.path, context.agent, {
18948
+ provider: context.options.provider,
18949
+ currency: context.options.currency,
18950
+ checkOnly: true
18951
+ }, context.configLoader, context.hledgerExecutor, inWorktree);
18952
+ const dryRunParsed = JSON.parse(dryRunResult);
18953
+ const message = dryRunParsed.success ? `Dry run passed: ${dryRunParsed.summary?.totalTransactions || 0} transactions ready` : `Dry run failed: ${dryRunParsed.summary?.unknown || 0} unknown account(s)`;
18954
+ context.result.steps.dryRun = buildStepResult(dryRunParsed.success, message, {
18955
+ success: dryRunParsed.success,
18956
+ summary: dryRunParsed.summary
18957
+ });
18958
+ if (!dryRunParsed.success) {
18959
+ context.result.error = "Dry run found unknown accounts or errors";
18960
+ context.result.hint = "Add rules to categorize unknown transactions, then retry";
18961
+ throw new Error("Dry run failed");
18962
+ }
18963
+ if (dryRunParsed.summary?.totalTransactions === 0) {
18964
+ throw new NoTransactionsError;
18965
+ }
18966
+ }
18967
+ async function executeImportStep(context, worktree) {
18968
+ const inWorktree = () => true;
18969
+ const importResult = await importStatements(worktree.path, context.agent, {
18970
+ provider: context.options.provider,
18971
+ currency: context.options.currency,
18972
+ checkOnly: false
18973
+ }, context.configLoader, context.hledgerExecutor, inWorktree);
18974
+ const importParsed = JSON.parse(importResult);
18975
+ const message = importParsed.success ? `Imported ${importParsed.summary?.totalTransactions || 0} transactions` : `Import failed: ${importParsed.error || "Unknown error"}`;
18976
+ context.result.steps.import = buildStepResult(importParsed.success, message, {
18977
+ success: importParsed.success,
18978
+ summary: importParsed.summary,
18979
+ error: importParsed.error
18980
+ });
18981
+ if (!importParsed.success) {
18982
+ context.result.error = `Import failed: ${importParsed.error || "Unknown error"}`;
18983
+ throw new Error("Import failed");
18984
+ }
18985
+ }
18986
+ async function executeReconcileStep(context, worktree) {
18987
+ const inWorktree = () => true;
18988
+ const reconcileResult = await reconcileStatement(worktree.path, context.agent, {
18989
+ provider: context.options.provider,
18990
+ currency: context.options.currency,
18991
+ closingBalance: context.options.closingBalance,
18992
+ account: context.options.account
18993
+ }, context.configLoader, context.hledgerExecutor, inWorktree);
18994
+ const reconcileParsed = JSON.parse(reconcileResult);
18995
+ const message = reconcileParsed.success ? `Balance reconciled: ${reconcileParsed.actualBalance}` : `Balance mismatch: expected ${reconcileParsed.expectedBalance}, got ${reconcileParsed.actualBalance}`;
18996
+ context.result.steps.reconcile = buildStepResult(reconcileParsed.success, message, {
18997
+ success: reconcileParsed.success,
18998
+ actualBalance: reconcileParsed.actualBalance,
18999
+ expectedBalance: reconcileParsed.expectedBalance,
19000
+ metadata: reconcileParsed.metadata,
19001
+ error: reconcileParsed.error
19002
+ });
19003
+ if (!reconcileParsed.success) {
19004
+ context.result.error = `Reconciliation failed: ${reconcileParsed.error || "Balance mismatch"}`;
19005
+ context.result.hint = "Check for missing transactions or incorrect rules";
19006
+ throw new Error("Reconciliation failed");
19007
+ }
19008
+ }
19009
+ async function executeMergeStep(context, worktree) {
19010
+ const importDetails = context.result.steps.import?.details;
19011
+ const reconcileDetails = context.result.steps.reconcile?.details;
19012
+ if (!importDetails || !reconcileDetails) {
19013
+ throw new Error("Import or reconcile step not completed before merge");
19014
+ }
19015
+ const commitInfo = {
19016
+ fromDate: reconcileDetails.metadata?.from_date,
19017
+ untilDate: reconcileDetails.metadata?.until_date
19018
+ };
19019
+ const transactionCount = importDetails.summary?.totalTransactions || 0;
19020
+ const commitMessage = buildCommitMessage(context.options.provider, context.options.currency, commitInfo.fromDate, commitInfo.untilDate, transactionCount);
19021
+ try {
19022
+ mergeWorktree(worktree, commitMessage);
19023
+ const mergeDetails = { commitMessage };
19024
+ context.result.steps.merge = buildStepResult(true, `Merged to main: "${commitMessage}"`, mergeDetails);
19025
+ cleanupIncomingFiles(worktree, context);
19026
+ } catch (error45) {
19027
+ const message = `Merge failed: ${error45 instanceof Error ? error45.message : String(error45)}`;
19028
+ context.result.steps.merge = buildStepResult(false, message);
19029
+ context.result.error = "Merge to main branch failed";
19030
+ throw new Error("Merge failed");
19031
+ }
19032
+ }
19033
+ function handleNoTransactions(result) {
19034
+ result.steps.import = buildStepResult(true, "No transactions to import");
19035
+ result.steps.reconcile = buildStepResult(true, "Reconciliation skipped (no transactions)");
19036
+ result.steps.merge = buildStepResult(true, "Merge skipped (no changes)");
19037
+ return buildSuccessResult5(result, "No transactions found to import");
19038
+ }
19039
+ async function importPipeline(directory, agent, options, configLoader = loadImportConfig, hledgerExecutor = defaultHledgerExecutor) {
19040
+ const restrictionError = checkAccountantAgent(agent, "import pipeline");
19041
+ if (restrictionError) {
19042
+ return restrictionError;
19043
+ }
19044
+ const result = {
19045
+ success: false,
19046
+ steps: {}
19047
+ };
19048
+ const context = {
19049
+ directory,
19050
+ agent,
19051
+ options,
19052
+ configLoader,
19053
+ hledgerExecutor,
19054
+ result
19055
+ };
19056
+ try {
19057
+ return await withWorktree(directory, async (worktree) => {
19058
+ result.worktreeId = worktree.uuid;
19059
+ result.steps.worktree = buildStepResult(true, `Created worktree at ${worktree.path}`, {
19060
+ path: worktree.path,
19061
+ branch: worktree.branch
19062
+ });
19063
+ try {
19064
+ await executeClassifyStep(context, worktree);
19065
+ await executeDryRunStep(context, worktree);
19066
+ await executeImportStep(context, worktree);
19067
+ await executeReconcileStep(context, worktree);
19068
+ await executeMergeStep(context, worktree);
19069
+ result.steps.cleanup = buildStepResult(true, "Worktree cleaned up", {
19070
+ cleanedAfterSuccess: true
19071
+ });
19072
+ const transactionCount = context.result.steps.import?.details?.summary?.totalTransactions || 0;
19073
+ return buildSuccessResult5(result, `Successfully imported ${transactionCount} transaction(s)`);
19074
+ } catch (error45) {
19075
+ result.steps.cleanup = buildStepResult(true, "Worktree cleaned up after failure", { cleanedAfterFailure: true });
19076
+ if (error45 instanceof NoTransactionsError) {
19077
+ return handleNoTransactions(result);
19078
+ }
19079
+ if (!result.error) {
19080
+ result.error = error45 instanceof Error ? error45.message : String(error45);
19081
+ }
19082
+ return buildErrorResult5(result, result.error, result.hint);
19083
+ }
19084
+ });
19085
+ } catch (error45) {
19086
+ result.steps.worktree = buildStepResult(false, `Failed to create worktree: ${error45 instanceof Error ? error45.message : String(error45)}`);
19087
+ result.error = "Failed to create worktree";
19088
+ return buildErrorResult5(result, result.error);
19089
+ }
19090
+ }
19091
+ var import_pipeline_default = tool({
19092
+ description: `ACCOUNTANT AGENT ONLY: Complete import pipeline with git worktree isolation and balance reconciliation.
19093
+
19094
+ This tool orchestrates the full import workflow in an isolated git worktree:
19095
+
19096
+ **Pipeline Steps:**
19097
+ 1. **Create Worktree**: Creates an isolated git worktree for safe import
19098
+ 2. **Classify**: Moves CSVs from import to pending directory (optional, skip with skipClassify)
19099
+ 3. **Dry Run**: Validates all transactions have known accounts
19100
+ 4. **Import**: Imports transactions to the journal
19101
+ 5. **Reconcile**: Validates closing balance matches CSV metadata
19102
+ 6. **Merge**: Merges worktree to main with --no-ff
19103
+ 7. **Cleanup**: Removes worktree
19104
+
19105
+ **Safety Features:**
19106
+ - All changes happen in isolated worktree
19107
+ - If any step fails, worktree is discarded (main branch untouched)
19108
+ - Balance reconciliation ensures data integrity
19109
+ - Atomic commit with merge --no-ff preserves history
19110
+
19111
+ **Usage:**
19112
+ - Basic: import-pipeline (processes all pending CSVs)
19113
+ - Filtered: import-pipeline --provider ubs --currency chf
19114
+ - With manual balance: import-pipeline --closingBalance "CHF 1234.56"
19115
+ - Skip classify: import-pipeline --skipClassify true`,
19116
+ args: {
19117
+ provider: tool.schema.string().optional().describe('Filter by provider (e.g., "ubs", "revolut")'),
19118
+ currency: tool.schema.string().optional().describe('Filter by currency (e.g., "chf", "eur")'),
19119
+ closingBalance: tool.schema.string().optional().describe("Manual closing balance override (if not in CSV metadata)"),
19120
+ account: tool.schema.string().optional().describe("Manual account override (auto-detected from rules file if not provided)"),
19121
+ skipClassify: tool.schema.boolean().optional().describe("Skip the classify step (default: false)")
19122
+ },
19123
+ async execute(params, context) {
19124
+ const { directory, agent } = context;
19125
+ return importPipeline(directory, agent, {
19126
+ provider: params.provider,
19127
+ currency: params.currency,
19128
+ closingBalance: params.closingBalance,
19129
+ account: params.account,
19130
+ skipClassify: params.skipClassify
19131
+ });
19132
+ }
19133
+ });
19134
+ // src/tools/init-directories.ts
19135
+ import * as fs13 from "fs";
19136
+ import * as path12 from "path";
19137
+ async function initDirectories(directory) {
19138
+ try {
19139
+ const config2 = loadImportConfig(directory);
19140
+ const directoriesCreated = [];
19141
+ const gitkeepFiles = [];
19142
+ const importBase = path12.join(directory, "import");
19143
+ if (!fs13.existsSync(importBase)) {
19144
+ fs13.mkdirSync(importBase, { recursive: true });
19145
+ directoriesCreated.push("import");
19146
+ }
19147
+ const pathsToCreate = [
19148
+ { key: "import", path: config2.paths.import },
19149
+ { key: "pending", path: config2.paths.pending },
19150
+ { key: "done", path: config2.paths.done },
19151
+ { key: "unrecognized", path: config2.paths.unrecognized }
19152
+ ];
19153
+ for (const { path: dirPath } of pathsToCreate) {
19154
+ const fullPath = path12.join(directory, dirPath);
19155
+ if (!fs13.existsSync(fullPath)) {
19156
+ fs13.mkdirSync(fullPath, { recursive: true });
19157
+ directoriesCreated.push(dirPath);
19158
+ }
19159
+ const gitkeepPath = path12.join(fullPath, ".gitkeep");
19160
+ if (!fs13.existsSync(gitkeepPath)) {
19161
+ fs13.writeFileSync(gitkeepPath, "");
19162
+ gitkeepFiles.push(path12.join(dirPath, ".gitkeep"));
19163
+ }
19164
+ }
19165
+ const gitignorePath = path12.join(importBase, ".gitignore");
19166
+ let gitignoreCreated = false;
19167
+ if (!fs13.existsSync(gitignorePath)) {
19168
+ const gitignoreContent = `# Ignore CSV/PDF files in temporary directories
19169
+ /incoming/*.csv
19170
+ /incoming/*.pdf
19171
+ /pending/**/*.csv
19172
+ /pending/**/*.pdf
19173
+ /unrecognized/**/*.csv
19174
+ /unrecognized/**/*.pdf
19175
+
19176
+ # Track processed files in done/ (audit trail)
19177
+ # No ignore rule needed - tracked by default
19178
+
19179
+ # Ignore temporary files
19180
+ *.tmp
19181
+ *.temp
19182
+ .DS_Store
19183
+ Thumbs.db
19184
+ `;
19185
+ fs13.writeFileSync(gitignorePath, gitignoreContent);
19186
+ gitignoreCreated = true;
19187
+ }
19188
+ const parts = [];
19189
+ if (directoriesCreated.length > 0) {
19190
+ parts.push(`Created ${directoriesCreated.length} director${directoriesCreated.length === 1 ? "y" : "ies"}`);
19191
+ }
19192
+ if (gitkeepFiles.length > 0) {
19193
+ parts.push(`added ${gitkeepFiles.length} .gitkeep file${gitkeepFiles.length === 1 ? "" : "s"}`);
19194
+ }
19195
+ if (gitignoreCreated) {
19196
+ parts.push("created .gitignore");
19197
+ }
19198
+ const message = parts.length > 0 ? `Import directory structure initialized: ${parts.join(", ")}` : "Import directory structure already exists (no changes needed)";
19199
+ return {
19200
+ success: true,
19201
+ directoriesCreated,
19202
+ gitkeepFiles,
19203
+ gitignoreCreated,
19204
+ message
19205
+ };
19206
+ } catch (error45) {
19207
+ return {
19208
+ success: false,
19209
+ directoriesCreated: [],
19210
+ gitkeepFiles: [],
19211
+ gitignoreCreated: false,
19212
+ error: error45 instanceof Error ? error45.message : String(error45),
19213
+ message: "Failed to initialize import directory structure"
19214
+ };
19215
+ }
19216
+ }
19217
+ var init_directories_default = tool({
19218
+ description: "ACCOUNTANT AGENT ONLY: Initialize the import directory structure needed for processing bank statements. Creates import/incoming, import/pending, import/done, and import/unrecognized directories with .gitkeep files and appropriate .gitignore rules. Reads directory paths from config/import/providers.yaml. Safe to run multiple times (idempotent).",
19219
+ args: {},
19220
+ async execute(_params, context) {
19221
+ const restrictionError = checkAccountantAgent(context.agent, "init directories");
19222
+ if (restrictionError) {
19223
+ throw new Error(restrictionError);
19224
+ }
19225
+ const { directory } = context;
19226
+ const result = await initDirectories(directory);
19227
+ if (!result.success) {
19228
+ return `Error: ${result.error}
19229
+
19230
+ ${result.message}`;
19231
+ }
19232
+ const output = [];
19233
+ output.push(result.message || "");
19234
+ if (result.directoriesCreated.length > 0) {
19235
+ output.push(`
19236
+ Directories created:`);
19237
+ for (const dir of result.directoriesCreated) {
19238
+ output.push(` - ${dir}`);
19239
+ }
19240
+ }
19241
+ if (result.gitkeepFiles.length > 0) {
19242
+ output.push(`
19243
+ .gitkeep files added:`);
19244
+ for (const file2 of result.gitkeepFiles) {
19245
+ output.push(` - ${file2}`);
19246
+ }
19247
+ }
19248
+ if (result.gitignoreCreated) {
19249
+ output.push(`
19250
+ Created import/.gitignore with rules to:`);
19251
+ output.push(" - Ignore CSV/PDF files in incoming/, pending/, unrecognized/");
19252
+ output.push(" - Track processed files in done/ for audit trail");
19253
+ }
19254
+ output.push(`
19255
+ You can now drop CSV files into import/incoming/ and run import-pipeline.`);
19256
+ return output.join(`
19257
+ `);
19258
+ }
19259
+ });
18697
19260
  // src/index.ts
18698
- var __dirname2 = dirname5(fileURLToPath(import.meta.url));
18699
- var AGENT_FILE = join10(__dirname2, "..", "agent", "accountant.md");
19261
+ var __dirname2 = dirname6(fileURLToPath(import.meta.url));
19262
+ var AGENT_FILE = join13(__dirname2, "..", "agent", "accountant.md");
18700
19263
  var AccountantPlugin = async () => {
18701
19264
  const agent = loadAgent(AGENT_FILE);
18702
19265
  return {
@@ -18704,7 +19267,8 @@ var AccountantPlugin = async () => {
18704
19267
  "fetch-currency-prices": fetch_currency_prices_default,
18705
19268
  "classify-statements": classify_statements_default,
18706
19269
  "import-statements": import_statements_default,
18707
- "reconcile-statements": reconcile_statement_default
19270
+ "reconcile-statements": reconcile_statement_default,
19271
+ "import-piprline": import_pipeline_default
18708
19272
  },
18709
19273
  config: async (config2) => {
18710
19274
  if (agent) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fuzzle/opencode-accountant",
3
- "version": "0.1.1",
3
+ "version": "0.1.2-next.1",
4
4
  "description": "An OpenCode accounting agent, specialized in double-entry-bookkepping with hledger",
5
5
  "author": {
6
6
  "name": "ali bengali",