@fuzzle/opencode-accountant 0.4.2-next.1 → 0.4.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.
Files changed (2) hide show
  1. package/dist/index.js +677 -228
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1548,18 +1548,18 @@ var require_brace_expansion = __commonJS((exports, module) => {
1548
1548
 
1549
1549
  // node_modules/convert-csv-to-json/src/util/fileUtils.js
1550
1550
  var require_fileUtils = __commonJS((exports, module) => {
1551
- var fs8 = __require("fs");
1551
+ var fs9 = __require("fs");
1552
1552
 
1553
1553
  class FileUtils {
1554
1554
  readFile(fileInputName, encoding) {
1555
- return fs8.readFileSync(fileInputName, encoding).toString();
1555
+ return fs9.readFileSync(fileInputName, encoding).toString();
1556
1556
  }
1557
1557
  readFileAsync(fileInputName, encoding = "utf8") {
1558
- if (fs8.promises && typeof fs8.promises.readFile === "function") {
1559
- return fs8.promises.readFile(fileInputName, encoding).then((buf) => buf.toString());
1558
+ if (fs9.promises && typeof fs9.promises.readFile === "function") {
1559
+ return fs9.promises.readFile(fileInputName, encoding).then((buf) => buf.toString());
1560
1560
  }
1561
1561
  return new Promise((resolve2, reject) => {
1562
- fs8.readFile(fileInputName, encoding, (err, data) => {
1562
+ fs9.readFile(fileInputName, encoding, (err, data) => {
1563
1563
  if (err) {
1564
1564
  reject(err);
1565
1565
  return;
@@ -1569,7 +1569,7 @@ var require_fileUtils = __commonJS((exports, module) => {
1569
1569
  });
1570
1570
  }
1571
1571
  writeFile(json3, fileOutputName) {
1572
- fs8.writeFile(fileOutputName, json3, function(err) {
1572
+ fs9.writeFile(fileOutputName, json3, function(err) {
1573
1573
  if (err) {
1574
1574
  throw err;
1575
1575
  } else {
@@ -1578,11 +1578,11 @@ var require_fileUtils = __commonJS((exports, module) => {
1578
1578
  });
1579
1579
  }
1580
1580
  writeFileAsync(json3, fileOutputName) {
1581
- if (fs8.promises && typeof fs8.promises.writeFile === "function") {
1582
- return fs8.promises.writeFile(fileOutputName, json3);
1581
+ if (fs9.promises && typeof fs9.promises.writeFile === "function") {
1582
+ return fs9.promises.writeFile(fileOutputName, json3);
1583
1583
  }
1584
1584
  return new Promise((resolve2, reject) => {
1585
- fs8.writeFile(fileOutputName, json3, (err) => {
1585
+ fs9.writeFile(fileOutputName, json3, (err) => {
1586
1586
  if (err)
1587
1587
  return reject(err);
1588
1588
  resolve2();
@@ -2147,7 +2147,7 @@ var require_convert_csv_to_json = __commonJS((exports) => {
2147
2147
  });
2148
2148
 
2149
2149
  // src/index.ts
2150
- import { dirname as dirname5, join as join12 } from "path";
2150
+ import { dirname as dirname6, join as join13 } from "path";
2151
2151
  import { fileURLToPath as fileURLToPath3 } from "url";
2152
2152
 
2153
2153
  // src/utils/agentLoader.ts
@@ -17479,8 +17479,8 @@ var fetch_currency_prices_default = tool({
17479
17479
  }
17480
17480
  });
17481
17481
  // src/tools/classify-statements.ts
17482
- import * as fs5 from "fs";
17483
- import * as path6 from "path";
17482
+ import * as fs6 from "fs";
17483
+ import * as path7 from "path";
17484
17484
 
17485
17485
  // src/utils/importConfig.ts
17486
17486
  import * as fs3 from "fs";
@@ -17751,23 +17751,263 @@ function detectProvider(filename, content, config2) {
17751
17751
  return null;
17752
17752
  }
17753
17753
 
17754
- // src/utils/fileUtils.ts
17754
+ // src/utils/worktreeManager.ts
17755
+ import { spawnSync } from "child_process";
17756
+
17757
+ // node_modules/uuid/dist-node/stringify.js
17758
+ var byteToHex = [];
17759
+ for (let i2 = 0;i2 < 256; ++i2) {
17760
+ byteToHex.push((i2 + 256).toString(16).slice(1));
17761
+ }
17762
+ function unsafeStringify(arr, offset = 0) {
17763
+ 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();
17764
+ }
17765
+
17766
+ // node_modules/uuid/dist-node/rng.js
17767
+ import { randomFillSync } from "crypto";
17768
+ var rnds8Pool = new Uint8Array(256);
17769
+ var poolPtr = rnds8Pool.length;
17770
+ function rng() {
17771
+ if (poolPtr > rnds8Pool.length - 16) {
17772
+ randomFillSync(rnds8Pool);
17773
+ poolPtr = 0;
17774
+ }
17775
+ return rnds8Pool.slice(poolPtr, poolPtr += 16);
17776
+ }
17777
+
17778
+ // node_modules/uuid/dist-node/native.js
17779
+ import { randomUUID } from "crypto";
17780
+ var native_default = { randomUUID };
17781
+
17782
+ // node_modules/uuid/dist-node/v4.js
17783
+ function _v4(options, buf, offset) {
17784
+ options = options || {};
17785
+ const rnds = options.random ?? options.rng?.() ?? rng();
17786
+ if (rnds.length < 16) {
17787
+ throw new Error("Random bytes length must be >= 16");
17788
+ }
17789
+ rnds[6] = rnds[6] & 15 | 64;
17790
+ rnds[8] = rnds[8] & 63 | 128;
17791
+ if (buf) {
17792
+ offset = offset || 0;
17793
+ if (offset < 0 || offset + 16 > buf.length) {
17794
+ throw new RangeError(`UUID byte range ${offset}:${offset + 15} is out of buffer bounds`);
17795
+ }
17796
+ for (let i2 = 0;i2 < 16; ++i2) {
17797
+ buf[offset + i2] = rnds[i2];
17798
+ }
17799
+ return buf;
17800
+ }
17801
+ return unsafeStringify(rnds);
17802
+ }
17803
+ function v4(options, buf, offset) {
17804
+ if (native_default.randomUUID && !buf && !options) {
17805
+ return native_default.randomUUID();
17806
+ }
17807
+ return _v4(options, buf, offset);
17808
+ }
17809
+ var v4_default = v4;
17810
+ // src/utils/worktreeManager.ts
17755
17811
  import * as fs4 from "fs";
17756
17812
  import * as path5 from "path";
17813
+ function execGit(args, cwd) {
17814
+ const result = spawnSync("git", args, { cwd, encoding: "utf-8" });
17815
+ if (result.status !== 0) {
17816
+ throw new Error(result.stderr || result.stdout || `git ${args[0]} failed`);
17817
+ }
17818
+ return (result.stdout || "").trim();
17819
+ }
17820
+ function execGitSafe(args, cwd) {
17821
+ const result = spawnSync("git", args, { cwd, encoding: "utf-8" });
17822
+ if (result.status !== 0) {
17823
+ return { success: false, output: result.stderr || result.stdout || `git ${args[0]} failed` };
17824
+ }
17825
+ return { success: true, output: (result.stdout || "").trim() };
17826
+ }
17827
+ function copyIncomingFiles(mainRepoPath, worktreePath) {
17828
+ const sourceDir = path5.join(mainRepoPath, "import/incoming");
17829
+ const targetDir = path5.join(worktreePath, "import/incoming");
17830
+ if (!fs4.existsSync(sourceDir)) {
17831
+ return;
17832
+ }
17833
+ fs4.mkdirSync(targetDir, { recursive: true });
17834
+ const entries = fs4.readdirSync(sourceDir, { withFileTypes: true });
17835
+ let copiedCount = 0;
17836
+ for (const entry of entries) {
17837
+ if (entry.isFile() && !entry.name.startsWith(".")) {
17838
+ const srcPath = path5.join(sourceDir, entry.name);
17839
+ const destPath = path5.join(targetDir, entry.name);
17840
+ fs4.copyFileSync(srcPath, destPath);
17841
+ copiedCount++;
17842
+ }
17843
+ }
17844
+ if (copiedCount > 0) {
17845
+ console.log(`[INFO] Copied ${copiedCount} file(s) from import/incoming/ to worktree`);
17846
+ }
17847
+ }
17848
+ function createImportWorktree(mainRepoPath, options = {}) {
17849
+ const baseDir = options.baseDir ?? "/tmp";
17850
+ const uuid3 = v4_default();
17851
+ const branch = `import-${uuid3}`;
17852
+ const worktreePath = path5.join(baseDir, `import-worktree-${uuid3}`);
17853
+ try {
17854
+ execGit(["rev-parse", "--git-dir"], mainRepoPath);
17855
+ } catch {
17856
+ throw new Error(`Not a git repository: ${mainRepoPath}`);
17857
+ }
17858
+ execGit(["branch", branch], mainRepoPath);
17859
+ try {
17860
+ execGit(["worktree", "add", worktreePath, branch], mainRepoPath);
17861
+ } catch (error45) {
17862
+ execGitSafe(["branch", "-D", branch], mainRepoPath);
17863
+ throw error45;
17864
+ }
17865
+ copyIncomingFiles(mainRepoPath, worktreePath);
17866
+ return {
17867
+ path: worktreePath,
17868
+ branch,
17869
+ uuid: uuid3,
17870
+ mainRepoPath
17871
+ };
17872
+ }
17873
+ function mergeWorktree(context, commitMessage) {
17874
+ const status = execGit(["status", "--porcelain"], context.path);
17875
+ if (status.length > 0) {
17876
+ execGit(["add", "-A"], context.path);
17877
+ execGit(["commit", "-m", commitMessage], context.path);
17878
+ }
17879
+ const currentBranch = execGit(["rev-parse", "--abbrev-ref", "HEAD"], context.mainRepoPath);
17880
+ execGit(["merge", "--no-ff", context.branch, "-m", commitMessage], context.mainRepoPath);
17881
+ if (currentBranch !== "main" && currentBranch !== "master") {
17882
+ execGit(["checkout", currentBranch], context.mainRepoPath);
17883
+ }
17884
+ }
17885
+ function removeWorktree(context, force = false) {
17886
+ const forceFlag = force ? "--force" : "";
17887
+ const args = ["worktree", "remove", context.path];
17888
+ if (forceFlag) {
17889
+ args.push(forceFlag);
17890
+ }
17891
+ const removeResult = execGitSafe(args, context.mainRepoPath);
17892
+ if (!removeResult.success) {
17893
+ if (!fs4.existsSync(context.path)) {} else {
17894
+ return { success: false, error: `Failed to remove worktree: ${removeResult.output}` };
17895
+ }
17896
+ }
17897
+ execGitSafe(["worktree", "prune"], context.mainRepoPath);
17898
+ const branchResult = execGitSafe(["branch", "-D", context.branch], context.mainRepoPath);
17899
+ if (!branchResult.success) {
17900
+ if (!branchResult.output.includes("not found")) {
17901
+ return { success: false, error: `Failed to delete branch: ${branchResult.output}` };
17902
+ }
17903
+ }
17904
+ return { success: true };
17905
+ }
17906
+ function isInWorktree(directory) {
17907
+ try {
17908
+ const gitDir = execGit(["rev-parse", "--git-dir"], directory);
17909
+ return gitDir.includes(".git/worktrees/");
17910
+ } catch {
17911
+ return false;
17912
+ }
17913
+ }
17914
+ async function withWorktree(directory, operation, options) {
17915
+ let createdWorktree = null;
17916
+ let operationSucceeded = false;
17917
+ const logger = options?.logger;
17918
+ const keepOnError = options?.keepOnError ?? true;
17919
+ try {
17920
+ logger?.logStep("Create Worktree", "start");
17921
+ 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
+ const result = await operation(createdWorktree);
17926
+ operationSucceeded = true;
17927
+ return result;
17928
+ } finally {
17929
+ 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
+ }
17943
+ }
17944
+ }
17945
+ }
17946
+
17947
+ // src/utils/fileUtils.ts
17948
+ import * as fs5 from "fs";
17949
+ import * as path6 from "path";
17757
17950
  function findCSVFiles(importsDir) {
17758
- if (!fs4.existsSync(importsDir)) {
17951
+ if (!fs5.existsSync(importsDir)) {
17759
17952
  return [];
17760
17953
  }
17761
- return fs4.readdirSync(importsDir).filter((file2) => file2.toLowerCase().endsWith(".csv")).filter((file2) => {
17762
- const fullPath = path5.join(importsDir, file2);
17763
- return fs4.statSync(fullPath).isFile();
17954
+ return fs5.readdirSync(importsDir).filter((file2) => file2.toLowerCase().endsWith(".csv")).filter((file2) => {
17955
+ const fullPath = path6.join(importsDir, file2);
17956
+ return fs5.statSync(fullPath).isFile();
17764
17957
  });
17765
17958
  }
17766
17959
  function ensureDirectory(dirPath) {
17767
- if (!fs4.existsSync(dirPath)) {
17768
- fs4.mkdirSync(dirPath, { recursive: true });
17960
+ if (!fs5.existsSync(dirPath)) {
17961
+ fs5.mkdirSync(dirPath, { recursive: true });
17769
17962
  }
17770
17963
  }
17964
+ function syncCSVFilesToWorktree(mainRepoPath, worktreePath, importDir) {
17965
+ const result = {
17966
+ synced: [],
17967
+ errors: []
17968
+ };
17969
+ const mainImportDir = path6.join(mainRepoPath, importDir);
17970
+ const worktreeImportDir = path6.join(worktreePath, importDir);
17971
+ const csvFiles = findCSVFiles(mainImportDir);
17972
+ if (csvFiles.length === 0) {
17973
+ return result;
17974
+ }
17975
+ ensureDirectory(worktreeImportDir);
17976
+ for (const file2 of csvFiles) {
17977
+ try {
17978
+ const sourcePath = path6.join(mainImportDir, file2);
17979
+ const destPath = path6.join(worktreeImportDir, file2);
17980
+ fs5.copyFileSync(sourcePath, destPath);
17981
+ result.synced.push(file2);
17982
+ } catch (error45) {
17983
+ const errorMsg = error45 instanceof Error ? error45.message : String(error45);
17984
+ result.errors.push({ file: file2, error: errorMsg });
17985
+ }
17986
+ }
17987
+ return result;
17988
+ }
17989
+ function cleanupProcessedCSVFiles(mainRepoPath, importDir) {
17990
+ const result = {
17991
+ deleted: [],
17992
+ errors: []
17993
+ };
17994
+ const mainImportDir = path6.join(mainRepoPath, importDir);
17995
+ const csvFiles = findCSVFiles(mainImportDir);
17996
+ if (csvFiles.length === 0) {
17997
+ return result;
17998
+ }
17999
+ for (const file2 of csvFiles) {
18000
+ try {
18001
+ const filePath = path6.join(mainImportDir, file2);
18002
+ fs5.unlinkSync(filePath);
18003
+ result.deleted.push(file2);
18004
+ } catch (error45) {
18005
+ const errorMsg = error45 instanceof Error ? error45.message : String(error45);
18006
+ result.errors.push({ file: file2, error: errorMsg });
18007
+ }
18008
+ }
18009
+ return result;
18010
+ }
17771
18011
 
17772
18012
  // src/tools/classify-statements.ts
17773
18013
  function buildSuccessResult2(classified, unrecognized, message) {
@@ -17806,20 +18046,20 @@ function planMoves(csvFiles, importsDir, pendingDir, unrecognizedDir, config2) {
17806
18046
  const plannedMoves = [];
17807
18047
  const collisions = [];
17808
18048
  for (const filename of csvFiles) {
17809
- const sourcePath = path6.join(importsDir, filename);
17810
- const content = fs5.readFileSync(sourcePath, "utf-8");
18049
+ const sourcePath = path7.join(importsDir, filename);
18050
+ const content = fs6.readFileSync(sourcePath, "utf-8");
17811
18051
  const detection = detectProvider(filename, content, config2);
17812
18052
  let targetPath;
17813
18053
  let targetFilename;
17814
18054
  if (detection) {
17815
18055
  targetFilename = detection.outputFilename || filename;
17816
- const targetDir = path6.join(pendingDir, detection.provider, detection.currency);
17817
- targetPath = path6.join(targetDir, targetFilename);
18056
+ const targetDir = path7.join(pendingDir, detection.provider, detection.currency);
18057
+ targetPath = path7.join(targetDir, targetFilename);
17818
18058
  } else {
17819
18059
  targetFilename = filename;
17820
- targetPath = path6.join(unrecognizedDir, filename);
18060
+ targetPath = path7.join(unrecognizedDir, filename);
17821
18061
  }
17822
- if (fs5.existsSync(targetPath)) {
18062
+ if (fs6.existsSync(targetPath)) {
17823
18063
  collisions.push({
17824
18064
  filename,
17825
18065
  existingPath: targetPath
@@ -17840,28 +18080,28 @@ function executeMoves(plannedMoves, config2, unrecognizedDir) {
17840
18080
  const unrecognized = [];
17841
18081
  for (const move of plannedMoves) {
17842
18082
  if (move.detection) {
17843
- const targetDir = path6.dirname(move.targetPath);
18083
+ const targetDir = path7.dirname(move.targetPath);
17844
18084
  ensureDirectory(targetDir);
17845
- fs5.renameSync(move.sourcePath, move.targetPath);
18085
+ fs6.renameSync(move.sourcePath, move.targetPath);
17846
18086
  classified.push({
17847
18087
  filename: move.targetFilename,
17848
18088
  originalFilename: move.detection.outputFilename ? move.filename : undefined,
17849
18089
  provider: move.detection.provider,
17850
18090
  currency: move.detection.currency,
17851
- targetPath: path6.join(config2.paths.pending, move.detection.provider, move.detection.currency, move.targetFilename)
18091
+ targetPath: path7.join(config2.paths.pending, move.detection.provider, move.detection.currency, move.targetFilename)
17852
18092
  });
17853
18093
  } else {
17854
18094
  ensureDirectory(unrecognizedDir);
17855
- fs5.renameSync(move.sourcePath, move.targetPath);
18095
+ fs6.renameSync(move.sourcePath, move.targetPath);
17856
18096
  unrecognized.push({
17857
18097
  filename: move.filename,
17858
- targetPath: path6.join(config2.paths.unrecognized, move.filename)
18098
+ targetPath: path7.join(config2.paths.unrecognized, move.filename)
17859
18099
  });
17860
18100
  }
17861
18101
  }
17862
18102
  return { classified, unrecognized };
17863
18103
  }
17864
- async function classifyStatements(directory, agent, configLoader = loadImportConfig) {
18104
+ async function classifyStatements(directory, agent, configLoader = loadImportConfig, worktreeChecker = isInWorktree) {
17865
18105
  const restrictionError = checkAccountantAgent(agent, "classify statements", {
17866
18106
  classified: [],
17867
18107
  unrecognized: []
@@ -17869,6 +18109,9 @@ async function classifyStatements(directory, agent, configLoader = loadImportCon
17869
18109
  if (restrictionError) {
17870
18110
  return restrictionError;
17871
18111
  }
18112
+ if (!worktreeChecker(directory)) {
18113
+ return buildErrorResult2("classify-statements must be run inside an import worktree", "Use import-pipeline tool to orchestrate the full workflow");
18114
+ }
17872
18115
  let config2;
17873
18116
  try {
17874
18117
  config2 = configLoader(directory);
@@ -17876,9 +18119,9 @@ async function classifyStatements(directory, agent, configLoader = loadImportCon
17876
18119
  const errorMessage = err instanceof Error ? err.message : String(err);
17877
18120
  return buildErrorResult2(errorMessage);
17878
18121
  }
17879
- const importsDir = path6.join(directory, config2.paths.import);
17880
- const pendingDir = path6.join(directory, config2.paths.pending);
17881
- const unrecognizedDir = path6.join(directory, config2.paths.unrecognized);
18122
+ const importsDir = path7.join(directory, config2.paths.import);
18123
+ const pendingDir = path7.join(directory, config2.paths.pending);
18124
+ const unrecognizedDir = path7.join(directory, config2.paths.unrecognized);
17882
18125
  const csvFiles = findCSVFiles(importsDir);
17883
18126
  if (csvFiles.length === 0) {
17884
18127
  return buildSuccessResult2([], [], `No CSV files found in ${config2.paths.import}`);
@@ -17899,8 +18142,8 @@ var classify_statements_default = tool({
17899
18142
  }
17900
18143
  });
17901
18144
  // src/tools/import-statements.ts
17902
- import * as fs9 from "fs";
17903
- import * as path9 from "path";
18145
+ import * as fs10 from "fs";
18146
+ import * as path10 from "path";
17904
18147
 
17905
18148
  // node_modules/minimatch/dist/esm/index.js
17906
18149
  var import_brace_expansion = __toESM(require_brace_expansion(), 1);
@@ -18493,11 +18736,11 @@ var qmarksTestNoExtDot = ([$0]) => {
18493
18736
  return (f) => f.length === len && f !== "." && f !== "..";
18494
18737
  };
18495
18738
  var defaultPlatform = typeof process === "object" && process ? typeof process.env === "object" && process.env && process.env.__MINIMATCH_TESTING_PLATFORM__ || process.platform : "posix";
18496
- var path7 = {
18739
+ var path8 = {
18497
18740
  win32: { sep: "\\" },
18498
18741
  posix: { sep: "/" }
18499
18742
  };
18500
- var sep = defaultPlatform === "win32" ? path7.win32.sep : path7.posix.sep;
18743
+ var sep = defaultPlatform === "win32" ? path8.win32.sep : path8.posix.sep;
18501
18744
  minimatch.sep = sep;
18502
18745
  var GLOBSTAR = Symbol("globstar **");
18503
18746
  minimatch.GLOBSTAR = GLOBSTAR;
@@ -20231,7 +20474,7 @@ class LRUCache {
20231
20474
  // node_modules/path-scurry/dist/esm/index.js
20232
20475
  import { posix, win32 } from "path";
20233
20476
  import { fileURLToPath } from "url";
20234
- import { lstatSync, readdir as readdirCB, readdirSync as readdirSync3, readlinkSync, realpathSync as rps } from "fs";
20477
+ import { lstatSync, readdir as readdirCB, readdirSync as readdirSync4, readlinkSync, realpathSync as rps } from "fs";
20235
20478
  import * as actualFS from "fs";
20236
20479
  import { lstat, readdir, readlink, realpath } from "fs/promises";
20237
20480
 
@@ -20903,7 +21146,7 @@ var realpathSync = rps.native;
20903
21146
  var defaultFS = {
20904
21147
  lstatSync,
20905
21148
  readdir: readdirCB,
20906
- readdirSync: readdirSync3,
21149
+ readdirSync: readdirSync4,
20907
21150
  readlinkSync,
20908
21151
  realpathSync,
20909
21152
  promises: {
@@ -21102,12 +21345,12 @@ class PathBase {
21102
21345
  childrenCache() {
21103
21346
  return this.#children;
21104
21347
  }
21105
- resolve(path8) {
21106
- if (!path8) {
21348
+ resolve(path9) {
21349
+ if (!path9) {
21107
21350
  return this;
21108
21351
  }
21109
- const rootPath = this.getRootString(path8);
21110
- const dir = path8.substring(rootPath.length);
21352
+ const rootPath = this.getRootString(path9);
21353
+ const dir = path9.substring(rootPath.length);
21111
21354
  const dirParts = dir.split(this.splitSep);
21112
21355
  const result = rootPath ? this.getRoot(rootPath).#resolveParts(dirParts) : this.#resolveParts(dirParts);
21113
21356
  return result;
@@ -21636,8 +21879,8 @@ class PathWin32 extends PathBase {
21636
21879
  newChild(name, type2 = UNKNOWN, opts = {}) {
21637
21880
  return new PathWin32(name, type2, this.root, this.roots, this.nocase, this.childrenCache(), opts);
21638
21881
  }
21639
- getRootString(path8) {
21640
- return win32.parse(path8).root;
21882
+ getRootString(path9) {
21883
+ return win32.parse(path9).root;
21641
21884
  }
21642
21885
  getRoot(rootPath) {
21643
21886
  rootPath = uncToDrive(rootPath.toUpperCase());
@@ -21663,8 +21906,8 @@ class PathPosix extends PathBase {
21663
21906
  constructor(name, type2 = UNKNOWN, root, roots, nocase, children, opts) {
21664
21907
  super(name, type2, root, roots, nocase, children, opts);
21665
21908
  }
21666
- getRootString(path8) {
21667
- return path8.startsWith("/") ? "/" : "";
21909
+ getRootString(path9) {
21910
+ return path9.startsWith("/") ? "/" : "";
21668
21911
  }
21669
21912
  getRoot(_rootPath) {
21670
21913
  return this.root;
@@ -21684,8 +21927,8 @@ class PathScurryBase {
21684
21927
  #children;
21685
21928
  nocase;
21686
21929
  #fs;
21687
- constructor(cwd = process.cwd(), pathImpl, sep2, { nocase, childrenCacheSize = 16 * 1024, fs: fs6 = defaultFS } = {}) {
21688
- this.#fs = fsFromOption(fs6);
21930
+ constructor(cwd = process.cwd(), pathImpl, sep2, { nocase, childrenCacheSize = 16 * 1024, fs: fs7 = defaultFS } = {}) {
21931
+ this.#fs = fsFromOption(fs7);
21689
21932
  if (cwd instanceof URL || cwd.startsWith("file://")) {
21690
21933
  cwd = fileURLToPath(cwd);
21691
21934
  }
@@ -21721,11 +21964,11 @@ class PathScurryBase {
21721
21964
  }
21722
21965
  this.cwd = prev;
21723
21966
  }
21724
- depth(path8 = this.cwd) {
21725
- if (typeof path8 === "string") {
21726
- path8 = this.cwd.resolve(path8);
21967
+ depth(path9 = this.cwd) {
21968
+ if (typeof path9 === "string") {
21969
+ path9 = this.cwd.resolve(path9);
21727
21970
  }
21728
- return path8.depth();
21971
+ return path9.depth();
21729
21972
  }
21730
21973
  childrenCache() {
21731
21974
  return this.#children;
@@ -22141,9 +22384,9 @@ class PathScurryBase {
22141
22384
  process2();
22142
22385
  return results;
22143
22386
  }
22144
- chdir(path8 = this.cwd) {
22387
+ chdir(path9 = this.cwd) {
22145
22388
  const oldCwd = this.cwd;
22146
- this.cwd = typeof path8 === "string" ? this.cwd.resolve(path8) : path8;
22389
+ this.cwd = typeof path9 === "string" ? this.cwd.resolve(path9) : path9;
22147
22390
  this.cwd[setAsCwd](oldCwd);
22148
22391
  }
22149
22392
  }
@@ -22161,8 +22404,8 @@ class PathScurryWin32 extends PathScurryBase {
22161
22404
  parseRootPath(dir) {
22162
22405
  return win32.parse(dir).root.toUpperCase();
22163
22406
  }
22164
- newRoot(fs6) {
22165
- return new PathWin32(this.rootPath, IFDIR, undefined, this.roots, this.nocase, this.childrenCache(), { fs: fs6 });
22407
+ newRoot(fs7) {
22408
+ return new PathWin32(this.rootPath, IFDIR, undefined, this.roots, this.nocase, this.childrenCache(), { fs: fs7 });
22166
22409
  }
22167
22410
  isAbsolute(p) {
22168
22411
  return p.startsWith("/") || p.startsWith("\\") || /^[a-z]:(\/|\\)/i.test(p);
@@ -22179,8 +22422,8 @@ class PathScurryPosix extends PathScurryBase {
22179
22422
  parseRootPath(_dir) {
22180
22423
  return "/";
22181
22424
  }
22182
- newRoot(fs6) {
22183
- return new PathPosix(this.rootPath, IFDIR, undefined, this.roots, this.nocase, this.childrenCache(), { fs: fs6 });
22425
+ newRoot(fs7) {
22426
+ return new PathPosix(this.rootPath, IFDIR, undefined, this.roots, this.nocase, this.childrenCache(), { fs: fs7 });
22184
22427
  }
22185
22428
  isAbsolute(p) {
22186
22429
  return p.startsWith("/");
@@ -22432,8 +22675,8 @@ class MatchRecord {
22432
22675
  this.store.set(target, current === undefined ? n : n & current);
22433
22676
  }
22434
22677
  entries() {
22435
- return [...this.store.entries()].map(([path8, n]) => [
22436
- path8,
22678
+ return [...this.store.entries()].map(([path9, n]) => [
22679
+ path9,
22437
22680
  !!(n & 2),
22438
22681
  !!(n & 1)
22439
22682
  ]);
@@ -22636,9 +22879,9 @@ class GlobUtil {
22636
22879
  signal;
22637
22880
  maxDepth;
22638
22881
  includeChildMatches;
22639
- constructor(patterns, path8, opts) {
22882
+ constructor(patterns, path9, opts) {
22640
22883
  this.patterns = patterns;
22641
- this.path = path8;
22884
+ this.path = path9;
22642
22885
  this.opts = opts;
22643
22886
  this.#sep = !opts.posix && opts.platform === "win32" ? "\\" : "/";
22644
22887
  this.includeChildMatches = opts.includeChildMatches !== false;
@@ -22657,11 +22900,11 @@ class GlobUtil {
22657
22900
  });
22658
22901
  }
22659
22902
  }
22660
- #ignored(path8) {
22661
- return this.seen.has(path8) || !!this.#ignore?.ignored?.(path8);
22903
+ #ignored(path9) {
22904
+ return this.seen.has(path9) || !!this.#ignore?.ignored?.(path9);
22662
22905
  }
22663
- #childrenIgnored(path8) {
22664
- return !!this.#ignore?.childrenIgnored?.(path8);
22906
+ #childrenIgnored(path9) {
22907
+ return !!this.#ignore?.childrenIgnored?.(path9);
22665
22908
  }
22666
22909
  pause() {
22667
22910
  this.paused = true;
@@ -22874,8 +23117,8 @@ class GlobUtil {
22874
23117
 
22875
23118
  class GlobWalker extends GlobUtil {
22876
23119
  matches = new Set;
22877
- constructor(patterns, path8, opts) {
22878
- super(patterns, path8, opts);
23120
+ constructor(patterns, path9, opts) {
23121
+ super(patterns, path9, opts);
22879
23122
  }
22880
23123
  matchEmit(e) {
22881
23124
  this.matches.add(e);
@@ -22913,8 +23156,8 @@ class GlobWalker extends GlobUtil {
22913
23156
 
22914
23157
  class GlobStream extends GlobUtil {
22915
23158
  results;
22916
- constructor(patterns, path8, opts) {
22917
- super(patterns, path8, opts);
23159
+ constructor(patterns, path9, opts) {
23160
+ super(patterns, path9, opts);
22918
23161
  this.results = new Minipass({
22919
23162
  signal: this.signal,
22920
23163
  objectMode: true
@@ -23182,8 +23425,8 @@ var glob = Object.assign(glob_, {
23182
23425
  glob.glob = glob;
23183
23426
 
23184
23427
  // src/utils/rulesMatcher.ts
23185
- import * as fs6 from "fs";
23186
- import * as path8 from "path";
23428
+ import * as fs7 from "fs";
23429
+ import * as path9 from "path";
23187
23430
  function parseSourceDirective(content) {
23188
23431
  const match2 = content.match(/^source\s+([^\n#]+)/m);
23189
23432
  if (!match2) {
@@ -23192,28 +23435,28 @@ function parseSourceDirective(content) {
23192
23435
  return match2[1].trim();
23193
23436
  }
23194
23437
  function resolveSourcePath(sourcePath, rulesFilePath) {
23195
- if (path8.isAbsolute(sourcePath)) {
23438
+ if (path9.isAbsolute(sourcePath)) {
23196
23439
  return sourcePath;
23197
23440
  }
23198
- const rulesDir = path8.dirname(rulesFilePath);
23199
- return path8.resolve(rulesDir, sourcePath);
23441
+ const rulesDir = path9.dirname(rulesFilePath);
23442
+ return path9.resolve(rulesDir, sourcePath);
23200
23443
  }
23201
23444
  function loadRulesMapping(rulesDir) {
23202
23445
  const mapping = {};
23203
- if (!fs6.existsSync(rulesDir)) {
23446
+ if (!fs7.existsSync(rulesDir)) {
23204
23447
  return mapping;
23205
23448
  }
23206
- const files = fs6.readdirSync(rulesDir);
23449
+ const files = fs7.readdirSync(rulesDir);
23207
23450
  for (const file2 of files) {
23208
23451
  if (!file2.endsWith(".rules")) {
23209
23452
  continue;
23210
23453
  }
23211
- const rulesFilePath = path8.join(rulesDir, file2);
23212
- const stat = fs6.statSync(rulesFilePath);
23454
+ const rulesFilePath = path9.join(rulesDir, file2);
23455
+ const stat = fs7.statSync(rulesFilePath);
23213
23456
  if (!stat.isFile()) {
23214
23457
  continue;
23215
23458
  }
23216
- const content = fs6.readFileSync(rulesFilePath, "utf-8");
23459
+ const content = fs7.readFileSync(rulesFilePath, "utf-8");
23217
23460
  const sourcePath = parseSourceDirective(content);
23218
23461
  if (!sourcePath) {
23219
23462
  continue;
@@ -23227,22 +23470,22 @@ function findRulesForCsv(csvPath, mapping) {
23227
23470
  if (mapping[csvPath]) {
23228
23471
  return mapping[csvPath];
23229
23472
  }
23230
- const normalizedCsvPath = path8.normalize(csvPath);
23473
+ const normalizedCsvPath = path9.normalize(csvPath);
23231
23474
  for (const [mappedCsv, rulesFile] of Object.entries(mapping)) {
23232
- const normalizedMappedCsv = path8.normalize(mappedCsv);
23475
+ const normalizedMappedCsv = path9.normalize(mappedCsv);
23233
23476
  if (normalizedMappedCsv === normalizedCsvPath) {
23234
23477
  return rulesFile;
23235
23478
  }
23236
23479
  }
23237
23480
  for (const [pattern, rulesFile] of Object.entries(mapping)) {
23238
- if (minimatch(csvPath, pattern) || minimatch(normalizedCsvPath, path8.normalize(pattern))) {
23481
+ if (minimatch(csvPath, pattern) || minimatch(normalizedCsvPath, path9.normalize(pattern))) {
23239
23482
  return rulesFile;
23240
23483
  }
23241
23484
  }
23242
- const csvBasename = path8.basename(csvPath);
23485
+ const csvBasename = path9.basename(csvPath);
23243
23486
  const matches = [];
23244
23487
  for (const rulesFile of Object.values(mapping)) {
23245
- const rulesBasename = path8.basename(rulesFile, ".rules");
23488
+ const rulesBasename = path9.basename(rulesFile, ".rules");
23246
23489
  if (csvBasename.startsWith(rulesBasename)) {
23247
23490
  matches.push({ rulesFile, prefixLength: rulesBasename.length });
23248
23491
  }
@@ -23373,7 +23616,7 @@ async function getAccountBalance(mainJournalPath, account, asOfDate, executor =
23373
23616
  }
23374
23617
 
23375
23618
  // src/utils/rulesParser.ts
23376
- import * as fs7 from "fs";
23619
+ import * as fs8 from "fs";
23377
23620
  function parseSkipRows(rulesContent) {
23378
23621
  const match2 = rulesContent.match(/^skip\s+(\d+)/m);
23379
23622
  return match2 ? parseInt(match2[1], 10) : 0;
@@ -23439,7 +23682,7 @@ function parseAccount1(rulesContent) {
23439
23682
  }
23440
23683
  function getAccountFromRulesFile(rulesFilePath) {
23441
23684
  try {
23442
- const content = fs7.readFileSync(rulesFilePath, "utf-8");
23685
+ const content = fs8.readFileSync(rulesFilePath, "utf-8");
23443
23686
  return parseAccount1(content);
23444
23687
  } catch {
23445
23688
  return null;
@@ -23459,7 +23702,7 @@ function parseRulesFile(rulesContent) {
23459
23702
 
23460
23703
  // src/utils/csvParser.ts
23461
23704
  var import_convert_csv_to_json = __toESM(require_convert_csv_to_json(), 1);
23462
- import * as fs8 from "fs";
23705
+ import * as fs9 from "fs";
23463
23706
 
23464
23707
  // src/utils/balanceUtils.ts
23465
23708
  function parseAmountValue(amountStr) {
@@ -23508,7 +23751,7 @@ function balancesMatch(balance1, balance2) {
23508
23751
 
23509
23752
  // src/utils/csvParser.ts
23510
23753
  function parseCsvFile(csvPath, config2) {
23511
- const csvContent = fs8.readFileSync(csvPath, "utf-8");
23754
+ const csvContent = fs9.readFileSync(csvPath, "utf-8");
23512
23755
  const lines = csvContent.split(`
23513
23756
  `);
23514
23757
  const headerIndex = config2.skipRows;
@@ -23666,21 +23909,21 @@ function buildSuccessResult3(files, summary, message) {
23666
23909
  });
23667
23910
  }
23668
23911
  function findCsvFromRulesFile(rulesFile) {
23669
- const content = fs9.readFileSync(rulesFile, "utf-8");
23912
+ const content = fs10.readFileSync(rulesFile, "utf-8");
23670
23913
  const match2 = content.match(/^source\s+([^\n#]+)/m);
23671
23914
  if (!match2) {
23672
23915
  return null;
23673
23916
  }
23674
23917
  const sourcePath = match2[1].trim();
23675
- const rulesDir = path9.dirname(rulesFile);
23676
- const absolutePattern = path9.resolve(rulesDir, sourcePath);
23918
+ const rulesDir = path10.dirname(rulesFile);
23919
+ const absolutePattern = path10.resolve(rulesDir, sourcePath);
23677
23920
  const matches = glob.sync(absolutePattern);
23678
23921
  if (matches.length === 0) {
23679
23922
  return null;
23680
23923
  }
23681
23924
  matches.sort((a, b) => {
23682
- const aStat = fs9.statSync(a);
23683
- const bStat = fs9.statSync(b);
23925
+ const aStat = fs10.statSync(a);
23926
+ const bStat = fs10.statSync(b);
23684
23927
  return bStat.mtime.getTime() - aStat.mtime.getTime();
23685
23928
  });
23686
23929
  return matches[0];
@@ -23688,7 +23931,7 @@ function findCsvFromRulesFile(rulesFile) {
23688
23931
  async function executeImports(fileResults, directory, pendingDir, doneDir, hledgerExecutor) {
23689
23932
  const importedFiles = [];
23690
23933
  for (const fileResult of fileResults) {
23691
- const rulesFile = fileResult.rulesFile ? path9.join(directory, fileResult.rulesFile) : null;
23934
+ const rulesFile = fileResult.rulesFile ? path10.join(directory, fileResult.rulesFile) : null;
23692
23935
  if (!rulesFile)
23693
23936
  continue;
23694
23937
  const year = fileResult.transactionYear;
@@ -23720,7 +23963,7 @@ async function executeImports(fileResults, directory, pendingDir, doneDir, hledg
23720
23963
  importedFiles.push(importedCsv);
23721
23964
  }
23722
23965
  }
23723
- const mainJournalPath = path9.join(directory, ".hledger.journal");
23966
+ const mainJournalPath = path10.join(directory, ".hledger.journal");
23724
23967
  const validationResult = await validateLedger(mainJournalPath, hledgerExecutor);
23725
23968
  if (!validationResult.valid) {
23726
23969
  return {
@@ -23730,13 +23973,13 @@ async function executeImports(fileResults, directory, pendingDir, doneDir, hledg
23730
23973
  };
23731
23974
  }
23732
23975
  for (const csvFile of importedFiles) {
23733
- const relativePath = path9.relative(pendingDir, csvFile);
23734
- const destPath = path9.join(doneDir, relativePath);
23735
- const destDir = path9.dirname(destPath);
23736
- if (!fs9.existsSync(destDir)) {
23737
- fs9.mkdirSync(destDir, { recursive: true });
23976
+ const relativePath = path10.relative(pendingDir, csvFile);
23977
+ const destPath = path10.join(doneDir, relativePath);
23978
+ const destDir = path10.dirname(destPath);
23979
+ if (!fs10.existsSync(destDir)) {
23980
+ fs10.mkdirSync(destDir, { recursive: true });
23738
23981
  }
23739
- fs9.renameSync(csvFile, destPath);
23982
+ fs10.renameSync(csvFile, destPath);
23740
23983
  }
23741
23984
  return {
23742
23985
  success: true,
@@ -23747,7 +23990,7 @@ async function processCsvFile(csvFile, rulesMapping, directory, hledgerExecutor)
23747
23990
  const rulesFile = findRulesForCsv(csvFile, rulesMapping);
23748
23991
  if (!rulesFile) {
23749
23992
  return {
23750
- csv: path9.relative(directory, csvFile),
23993
+ csv: path10.relative(directory, csvFile),
23751
23994
  rulesFile: null,
23752
23995
  totalTransactions: 0,
23753
23996
  matchedTransactions: 0,
@@ -23758,8 +24001,8 @@ async function processCsvFile(csvFile, rulesMapping, directory, hledgerExecutor)
23758
24001
  const result = await hledgerExecutor(["print", "-f", rulesFile]);
23759
24002
  if (result.exitCode !== 0) {
23760
24003
  return {
23761
- csv: path9.relative(directory, csvFile),
23762
- rulesFile: path9.relative(directory, rulesFile),
24004
+ csv: path10.relative(directory, csvFile),
24005
+ rulesFile: path10.relative(directory, rulesFile),
23763
24006
  totalTransactions: 0,
23764
24007
  matchedTransactions: 0,
23765
24008
  unknownPostings: [],
@@ -23773,8 +24016,8 @@ async function processCsvFile(csvFile, rulesMapping, directory, hledgerExecutor)
23773
24016
  if (years.size > 1) {
23774
24017
  const yearList = Array.from(years).sort().join(", ");
23775
24018
  return {
23776
- csv: path9.relative(directory, csvFile),
23777
- rulesFile: path9.relative(directory, rulesFile),
24019
+ csv: path10.relative(directory, csvFile),
24020
+ rulesFile: path10.relative(directory, rulesFile),
23778
24021
  totalTransactions: transactionCount,
23779
24022
  matchedTransactions: matchedCount,
23780
24023
  unknownPostings: [],
@@ -23784,7 +24027,7 @@ async function processCsvFile(csvFile, rulesMapping, directory, hledgerExecutor)
23784
24027
  const transactionYear = years.size === 1 ? Array.from(years)[0] : undefined;
23785
24028
  if (unknownPostings.length > 0) {
23786
24029
  try {
23787
- const rulesContent = fs9.readFileSync(rulesFile, "utf-8");
24030
+ const rulesContent = fs10.readFileSync(rulesFile, "utf-8");
23788
24031
  const rulesConfig = parseRulesFile(rulesContent);
23789
24032
  const csvRows = parseCsvFile(csvFile, rulesConfig);
23790
24033
  for (const posting of unknownPostings) {
@@ -23801,19 +24044,22 @@ async function processCsvFile(csvFile, rulesMapping, directory, hledgerExecutor)
23801
24044
  }
23802
24045
  }
23803
24046
  return {
23804
- csv: path9.relative(directory, csvFile),
23805
- rulesFile: path9.relative(directory, rulesFile),
24047
+ csv: path10.relative(directory, csvFile),
24048
+ rulesFile: path10.relative(directory, rulesFile),
23806
24049
  totalTransactions: transactionCount,
23807
24050
  matchedTransactions: matchedCount,
23808
24051
  unknownPostings,
23809
24052
  transactionYear
23810
24053
  };
23811
24054
  }
23812
- async function importStatements(directory, agent, options, configLoader = loadImportConfig, hledgerExecutor = defaultHledgerExecutor) {
24055
+ async function importStatements(directory, agent, options, configLoader = loadImportConfig, hledgerExecutor = defaultHledgerExecutor, worktreeChecker = isInWorktree, _logger) {
23813
24056
  const restrictionError = checkAccountantAgent(agent, "import statements");
23814
24057
  if (restrictionError) {
23815
24058
  return restrictionError;
23816
24059
  }
24060
+ if (!worktreeChecker(directory)) {
24061
+ return buildErrorResult3("import-statements must be run inside an import worktree", "Use import-pipeline tool to orchestrate the full workflow");
24062
+ }
23817
24063
  let config2;
23818
24064
  try {
23819
24065
  config2 = configLoader(directory);
@@ -23821,9 +24067,9 @@ async function importStatements(directory, agent, options, configLoader = loadIm
23821
24067
  const errorMessage = `Failed to load configuration: ${error45 instanceof Error ? error45.message : String(error45)}`;
23822
24068
  return buildErrorResult3(errorMessage, 'Ensure config/import/providers.yaml exists with required paths including "rules"');
23823
24069
  }
23824
- const pendingDir = path9.join(directory, config2.paths.pending);
23825
- const rulesDir = path9.join(directory, config2.paths.rules);
23826
- const doneDir = path9.join(directory, config2.paths.done);
24070
+ const pendingDir = path10.join(directory, config2.paths.pending);
24071
+ const rulesDir = path10.join(directory, config2.paths.rules);
24072
+ const doneDir = path10.join(directory, config2.paths.done);
23827
24073
  const rulesMapping = loadRulesMapping(rulesDir);
23828
24074
  const csvFiles = findCsvFiles(pendingDir, options.provider, options.currency);
23829
24075
  if (csvFiles.length === 0) {
@@ -23867,8 +24113,8 @@ async function importStatements(directory, agent, options, configLoader = loadIm
23867
24113
  }
23868
24114
  for (const [_rulesFile, matchingCSVs] of rulesFileToCSVs.entries()) {
23869
24115
  matchingCSVs.sort((a, b) => {
23870
- const aStat = fs9.statSync(a);
23871
- const bStat = fs9.statSync(b);
24116
+ const aStat = fs10.statSync(a);
24117
+ const bStat = fs10.statSync(b);
23872
24118
  return bStat.mtime.getTime() - aStat.mtime.getTime();
23873
24119
  });
23874
24120
  const newestCSV = matchingCSVs[0];
@@ -23957,9 +24203,7 @@ This tool processes CSV files in the pending import directory and uses hledger's
23957
24203
  1. Run with checkOnly: true (or no args)
23958
24204
  2. If unknowns found, add rules to the appropriate .rules file
23959
24205
  3. Repeat until no unknowns
23960
- 4. Run with checkOnly: false to import
23961
-
23962
- Note: This tool is typically called via import-pipeline for the full workflow.`,
24206
+ 4. Run with checkOnly: false to import`,
23963
24207
  args: {
23964
24208
  provider: tool.schema.string().optional().describe('Filter by provider (e.g., "revolut", "ubs"). If omitted, process all providers.'),
23965
24209
  currency: tool.schema.string().optional().describe('Filter by currency (e.g., "chf", "eur"). If omitted, process all currencies for the provider.'),
@@ -23975,14 +24219,23 @@ Note: This tool is typically called via import-pipeline for the full workflow.`,
23975
24219
  }
23976
24220
  });
23977
24221
  // src/tools/reconcile-statement.ts
23978
- import * as fs10 from "fs";
23979
- import * as path10 from "path";
24222
+ import * as fs11 from "fs";
24223
+ import * as path11 from "path";
23980
24224
  function buildErrorResult4(params) {
23981
24225
  return JSON.stringify({
23982
24226
  success: false,
23983
24227
  ...params
23984
24228
  });
23985
24229
  }
24230
+ function validateWorktree(directory, worktreeChecker) {
24231
+ if (!worktreeChecker(directory)) {
24232
+ return buildErrorResult4({
24233
+ error: "reconcile-statement must be run inside an import worktree",
24234
+ hint: "Use import-pipeline tool to orchestrate the full workflow"
24235
+ });
24236
+ }
24237
+ return null;
24238
+ }
23986
24239
  function loadConfiguration(directory, configLoader) {
23987
24240
  try {
23988
24241
  const config2 = configLoader(directory);
@@ -24009,14 +24262,14 @@ function findCsvToReconcile(doneDir, options) {
24009
24262
  };
24010
24263
  }
24011
24264
  const csvFile = csvFiles[csvFiles.length - 1];
24012
- const relativePath = path10.relative(path10.dirname(path10.dirname(doneDir)), csvFile);
24265
+ const relativePath = path11.relative(path11.dirname(path11.dirname(doneDir)), csvFile);
24013
24266
  return { csvFile, relativePath };
24014
24267
  }
24015
24268
  function determineClosingBalance(csvFile, config2, options, relativeCsvPath, rulesDir) {
24016
24269
  let metadata;
24017
24270
  try {
24018
- const content = fs10.readFileSync(csvFile, "utf-8");
24019
- const filename = path10.basename(csvFile);
24271
+ const content = fs11.readFileSync(csvFile, "utf-8");
24272
+ const filename = path11.basename(csvFile);
24020
24273
  const detectionResult = detectProvider(filename, content, config2);
24021
24274
  metadata = detectionResult?.metadata;
24022
24275
  } catch {
@@ -24098,7 +24351,7 @@ function tryExtractClosingBalanceFromCSV(csvFile, rulesDir) {
24098
24351
  if (!rulesFile) {
24099
24352
  return null;
24100
24353
  }
24101
- const rulesContent = fs10.readFileSync(rulesFile, "utf-8");
24354
+ const rulesContent = fs11.readFileSync(rulesFile, "utf-8");
24102
24355
  const rulesConfig = parseRulesFile(rulesContent);
24103
24356
  const csvRows = parseCsvFile(csvFile, rulesConfig);
24104
24357
  if (csvRows.length === 0) {
@@ -24155,19 +24408,23 @@ function tryExtractClosingBalanceFromCSV(csvFile, rulesDir) {
24155
24408
  return null;
24156
24409
  }
24157
24410
  }
24158
- async function reconcileStatement(directory, agent, options, configLoader = loadImportConfig, hledgerExecutor = defaultHledgerExecutor) {
24411
+ async function reconcileStatement(directory, agent, options, configLoader = loadImportConfig, hledgerExecutor = defaultHledgerExecutor, worktreeChecker = isInWorktree) {
24159
24412
  const restrictionError = checkAccountantAgent(agent, "reconcile statement");
24160
24413
  if (restrictionError) {
24161
24414
  return restrictionError;
24162
24415
  }
24416
+ const worktreeError = validateWorktree(directory, worktreeChecker);
24417
+ if (worktreeError) {
24418
+ return worktreeError;
24419
+ }
24163
24420
  const configResult = loadConfiguration(directory, configLoader);
24164
24421
  if ("error" in configResult) {
24165
24422
  return configResult.error;
24166
24423
  }
24167
24424
  const { config: config2 } = configResult;
24168
- const doneDir = path10.join(directory, config2.paths.done);
24169
- const rulesDir = path10.join(directory, config2.paths.rules);
24170
- const mainJournalPath = path10.join(directory, ".hledger.journal");
24425
+ const doneDir = path11.join(directory, config2.paths.done);
24426
+ const rulesDir = path11.join(directory, config2.paths.rules);
24427
+ const mainJournalPath = path11.join(directory, ".hledger.journal");
24171
24428
  const csvResult = findCsvToReconcile(doneDir, options);
24172
24429
  if ("error" in csvResult) {
24173
24430
  return csvResult.error;
@@ -24263,6 +24520,7 @@ var reconcile_statement_default = tool({
24263
24520
  description: `ACCOUNTANT AGENT ONLY: Reconcile imported bank statement against closing balance.
24264
24521
 
24265
24522
  This tool validates that the imported transactions result in the correct closing balance.
24523
+ It must be run inside an import worktree (use import-pipeline for the full workflow).
24266
24524
 
24267
24525
  **Workflow:**
24268
24526
  1. Finds the most recently imported CSV in the done directory
@@ -24295,16 +24553,17 @@ This tool validates that the imported transactions result in the correct closing
24295
24553
  }
24296
24554
  });
24297
24555
  // src/tools/import-pipeline.ts
24298
- import * as path12 from "path";
24556
+ import * as fs14 from "fs";
24557
+ import * as path13 from "path";
24299
24558
 
24300
24559
  // src/utils/accountDeclarations.ts
24301
- import * as fs11 from "fs";
24560
+ import * as fs12 from "fs";
24302
24561
  function extractAccountsFromRulesFile(rulesPath) {
24303
24562
  const accounts = new Set;
24304
- if (!fs11.existsSync(rulesPath)) {
24563
+ if (!fs12.existsSync(rulesPath)) {
24305
24564
  return accounts;
24306
24565
  }
24307
- const content = fs11.readFileSync(rulesPath, "utf-8");
24566
+ const content = fs12.readFileSync(rulesPath, "utf-8");
24308
24567
  const lines = content.split(`
24309
24568
  `);
24310
24569
  for (const line of lines) {
@@ -24339,10 +24598,10 @@ function sortAccountDeclarations(accounts) {
24339
24598
  return Array.from(accounts).sort((a, b) => a.localeCompare(b));
24340
24599
  }
24341
24600
  function ensureAccountDeclarations(yearJournalPath, accounts) {
24342
- if (!fs11.existsSync(yearJournalPath)) {
24601
+ if (!fs12.existsSync(yearJournalPath)) {
24343
24602
  throw new Error(`Year journal not found: ${yearJournalPath}`);
24344
24603
  }
24345
- const content = fs11.readFileSync(yearJournalPath, "utf-8");
24604
+ const content = fs12.readFileSync(yearJournalPath, "utf-8");
24346
24605
  const lines = content.split(`
24347
24606
  `);
24348
24607
  const existingAccounts = new Set;
@@ -24404,7 +24663,7 @@ function ensureAccountDeclarations(yearJournalPath, accounts) {
24404
24663
  newContent.push("");
24405
24664
  }
24406
24665
  newContent.push(...otherLines);
24407
- fs11.writeFileSync(yearJournalPath, newContent.join(`
24666
+ fs12.writeFileSync(yearJournalPath, newContent.join(`
24408
24667
  `));
24409
24668
  return {
24410
24669
  added: Array.from(missingAccounts).sort(),
@@ -24413,8 +24672,8 @@ function ensureAccountDeclarations(yearJournalPath, accounts) {
24413
24672
  }
24414
24673
 
24415
24674
  // src/utils/logger.ts
24416
- import fs12 from "fs/promises";
24417
- import path11 from "path";
24675
+ import fs13 from "fs/promises";
24676
+ import path12 from "path";
24418
24677
 
24419
24678
  class MarkdownLogger {
24420
24679
  buffer = [];
@@ -24426,7 +24685,7 @@ class MarkdownLogger {
24426
24685
  this.autoFlush = config2.autoFlush ?? true;
24427
24686
  this.context = config2.context || {};
24428
24687
  const filename = config2.filename || `import-${this.getTimestamp()}.md`;
24429
- this.logPath = path11.join(config2.logDir, filename);
24688
+ this.logPath = path12.join(config2.logDir, filename);
24430
24689
  this.buffer.push(`# Import Pipeline Log`);
24431
24690
  this.buffer.push(`**Started**: ${new Date().toLocaleString()}`);
24432
24691
  this.buffer.push("");
@@ -24523,8 +24782,8 @@ class MarkdownLogger {
24523
24782
  if (this.buffer.length === 0)
24524
24783
  return;
24525
24784
  try {
24526
- await fs12.mkdir(path11.dirname(this.logPath), { recursive: true });
24527
- await fs12.writeFile(this.logPath, this.buffer.join(`
24785
+ await fs13.mkdir(path12.dirname(this.logPath), { recursive: true });
24786
+ await fs13.writeFile(this.logPath, this.buffer.join(`
24528
24787
  `), "utf-8");
24529
24788
  } catch {}
24530
24789
  }
@@ -24551,7 +24810,7 @@ function createImportLogger(directory, worktreeId, provider) {
24551
24810
  if (provider)
24552
24811
  context.provider = provider;
24553
24812
  const logger = createLogger({
24554
- logDir: path11.join(directory, ".memory"),
24813
+ logDir: path12.join(directory, ".memory"),
24555
24814
  autoFlush: true,
24556
24815
  context
24557
24816
  });
@@ -24591,8 +24850,51 @@ function buildErrorResult5(result, error45, hint) {
24591
24850
  }
24592
24851
  return JSON.stringify(result);
24593
24852
  }
24594
- async function executeClassifyStep(context, logger) {
24595
- logger?.startSection("Step 1: Classify Transactions");
24853
+ function buildCommitMessage(provider, currency, fromDate, untilDate, transactionCount) {
24854
+ const providerStr = provider?.toUpperCase() || "statements";
24855
+ const currencyStr = currency?.toUpperCase();
24856
+ const dateRange = fromDate && untilDate ? ` ${fromDate} to ${untilDate}` : "";
24857
+ const txStr = transactionCount > 0 ? ` (${transactionCount} transactions)` : "";
24858
+ const parts = ["Import:", providerStr];
24859
+ if (currencyStr) {
24860
+ parts.push(currencyStr);
24861
+ }
24862
+ return `${parts.join(" ")}${dateRange}${txStr}`;
24863
+ }
24864
+ function cleanupIncomingFiles(worktree, context) {
24865
+ const incomingDir = path13.join(worktree.mainRepoPath, "import/incoming");
24866
+ if (!fs14.existsSync(incomingDir)) {
24867
+ return;
24868
+ }
24869
+ const importStep = context.result.steps.import;
24870
+ if (!importStep?.success || !importStep.details) {
24871
+ return;
24872
+ }
24873
+ const importResult = importStep.details;
24874
+ if (!importResult.files || !Array.isArray(importResult.files)) {
24875
+ return;
24876
+ }
24877
+ let deletedCount = 0;
24878
+ for (const fileResult of importResult.files) {
24879
+ if (!fileResult.csv)
24880
+ continue;
24881
+ const filename = path13.basename(fileResult.csv);
24882
+ const filePath = path13.join(incomingDir, filename);
24883
+ if (fs14.existsSync(filePath)) {
24884
+ try {
24885
+ fs14.unlinkSync(filePath);
24886
+ deletedCount++;
24887
+ } catch (error45) {
24888
+ console.error(`[ERROR] Failed to delete ${filename}: ${error45 instanceof Error ? error45.message : String(error45)}`);
24889
+ }
24890
+ }
24891
+ }
24892
+ if (deletedCount > 0) {
24893
+ console.log(`[INFO] Cleaned up ${deletedCount} file(s) from import/incoming/`);
24894
+ }
24895
+ }
24896
+ async function executeClassifyStep(context, worktree, logger) {
24897
+ logger?.startSection("Step 2: Classify Transactions");
24596
24898
  logger?.logStep("Classify", "start");
24597
24899
  if (context.options.skipClassify) {
24598
24900
  logger?.info("Classification skipped (skipClassify: true)");
@@ -24600,7 +24902,8 @@ async function executeClassifyStep(context, logger) {
24600
24902
  logger?.endSection();
24601
24903
  return;
24602
24904
  }
24603
- const classifyResult = await classifyStatements(context.directory, context.agent, context.configLoader);
24905
+ const inWorktree = () => true;
24906
+ const classifyResult = await classifyStatements(worktree.path, context.agent, context.configLoader, inWorktree);
24604
24907
  const classifyParsed = JSON.parse(classifyResult);
24605
24908
  const success2 = classifyParsed.success !== false;
24606
24909
  let message = success2 ? "Classification complete" : "Classification had issues";
@@ -24617,12 +24920,12 @@ async function executeClassifyStep(context, logger) {
24617
24920
  context.result.steps.classify = buildStepResult(success2, message, details);
24618
24921
  logger?.endSection();
24619
24922
  }
24620
- async function executeAccountDeclarationsStep(context, logger) {
24621
- logger?.startSection("Step 2: Check Account Declarations");
24923
+ async function executeAccountDeclarationsStep(context, worktree, logger) {
24924
+ logger?.startSection("Step 3: Check Account Declarations");
24622
24925
  logger?.logStep("Check Accounts", "start");
24623
- const config2 = context.configLoader(context.directory);
24624
- const pendingDir = path12.join(context.directory, config2.paths.pending);
24625
- const rulesDir = path12.join(context.directory, config2.paths.rules);
24926
+ 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);
24626
24929
  const csvFiles = findCsvFiles(pendingDir, context.options.provider, context.options.currency);
24627
24930
  if (csvFiles.length === 0) {
24628
24931
  context.result.steps.accountDeclarations = buildStepResult(true, "No CSV files to process", {
@@ -24653,7 +24956,7 @@ async function executeAccountDeclarationsStep(context, logger) {
24653
24956
  context.result.steps.accountDeclarations = buildStepResult(true, "No accounts found in rules files", {
24654
24957
  accountsAdded: [],
24655
24958
  journalUpdated: "",
24656
- rulesScanned: Array.from(matchedRulesFiles).map((f) => path12.relative(context.directory, f))
24959
+ rulesScanned: Array.from(matchedRulesFiles).map((f) => path13.relative(worktree.path, f))
24657
24960
  });
24658
24961
  return;
24659
24962
  }
@@ -24676,23 +24979,23 @@ async function executeAccountDeclarationsStep(context, logger) {
24676
24979
  context.result.steps.accountDeclarations = buildStepResult(false, "Could not determine transaction year from CSV files", {
24677
24980
  accountsAdded: [],
24678
24981
  journalUpdated: "",
24679
- rulesScanned: Array.from(matchedRulesFiles).map((f) => path12.relative(context.directory, f))
24982
+ rulesScanned: Array.from(matchedRulesFiles).map((f) => path13.relative(worktree.path, f))
24680
24983
  });
24681
24984
  return;
24682
24985
  }
24683
24986
  let yearJournalPath;
24684
24987
  try {
24685
- yearJournalPath = ensureYearJournalExists(context.directory, transactionYear);
24988
+ yearJournalPath = ensureYearJournalExists(worktree.path, transactionYear);
24686
24989
  } catch (error45) {
24687
24990
  context.result.steps.accountDeclarations = buildStepResult(false, `Failed to create year journal: ${error45 instanceof Error ? error45.message : String(error45)}`, {
24688
24991
  accountsAdded: [],
24689
24992
  journalUpdated: "",
24690
- rulesScanned: Array.from(matchedRulesFiles).map((f) => path12.relative(context.directory, f))
24993
+ rulesScanned: Array.from(matchedRulesFiles).map((f) => path13.relative(worktree.path, f))
24691
24994
  });
24692
24995
  return;
24693
24996
  }
24694
24997
  const result = ensureAccountDeclarations(yearJournalPath, allAccounts);
24695
- const message = result.added.length > 0 ? `Added ${result.added.length} account declaration(s) to ${path12.relative(context.directory, yearJournalPath)}` : "All required accounts already declared";
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";
24696
24999
  logger?.logStep("Check Accounts", "success", message);
24697
25000
  if (result.added.length > 0) {
24698
25001
  for (const account of result.added) {
@@ -24701,19 +25004,20 @@ async function executeAccountDeclarationsStep(context, logger) {
24701
25004
  }
24702
25005
  context.result.steps.accountDeclarations = buildStepResult(true, message, {
24703
25006
  accountsAdded: result.added,
24704
- journalUpdated: path12.relative(context.directory, yearJournalPath),
24705
- rulesScanned: Array.from(matchedRulesFiles).map((f) => path12.relative(context.directory, f))
25007
+ journalUpdated: path13.relative(worktree.path, yearJournalPath),
25008
+ rulesScanned: Array.from(matchedRulesFiles).map((f) => path13.relative(worktree.path, f))
24706
25009
  });
24707
25010
  logger?.endSection();
24708
25011
  }
24709
- async function executeDryRunStep(context, logger) {
24710
- logger?.startSection("Step 3: Dry Run Import");
25012
+ async function executeDryRunStep(context, worktree, logger) {
25013
+ logger?.startSection("Step 4: Dry Run Import");
24711
25014
  logger?.logStep("Dry Run", "start");
24712
- const dryRunResult = await importStatements(context.directory, context.agent, {
25015
+ const inWorktree = () => true;
25016
+ const dryRunResult = await importStatements(worktree.path, context.agent, {
24713
25017
  provider: context.options.provider,
24714
25018
  currency: context.options.currency,
24715
25019
  checkOnly: true
24716
- }, context.configLoader, context.hledgerExecutor);
25020
+ }, context.configLoader, context.hledgerExecutor, inWorktree);
24717
25021
  const dryRunParsed = JSON.parse(dryRunResult);
24718
25022
  const message = dryRunParsed.success ? `Dry run passed: ${dryRunParsed.summary?.totalTransactions || 0} transactions ready` : `Dry run failed: ${dryRunParsed.summary?.unknown || 0} unknown account(s)`;
24719
25023
  logger?.logStep("Dry Run", dryRunParsed.success ? "success" : "error", message);
@@ -24737,14 +25041,15 @@ async function executeDryRunStep(context, logger) {
24737
25041
  }
24738
25042
  logger?.endSection();
24739
25043
  }
24740
- async function executeImportStep(context, logger) {
24741
- logger?.startSection("Step 4: Import Transactions");
25044
+ async function executeImportStep(context, worktree, logger) {
25045
+ logger?.startSection("Step 5: Import Transactions");
24742
25046
  logger?.logStep("Import", "start");
24743
- const importResult = await importStatements(context.directory, context.agent, {
25047
+ const inWorktree = () => true;
25048
+ const importResult = await importStatements(worktree.path, context.agent, {
24744
25049
  provider: context.options.provider,
24745
25050
  currency: context.options.currency,
24746
25051
  checkOnly: false
24747
- }, context.configLoader, context.hledgerExecutor);
25052
+ }, context.configLoader, context.hledgerExecutor, inWorktree);
24748
25053
  const importParsed = JSON.parse(importResult);
24749
25054
  const message = importParsed.success ? `Imported ${importParsed.summary?.totalTransactions || 0} transactions` : `Import failed: ${importParsed.error || "Unknown error"}`;
24750
25055
  logger?.logStep("Import", importParsed.success ? "success" : "error", message);
@@ -24761,15 +25066,16 @@ async function executeImportStep(context, logger) {
24761
25066
  }
24762
25067
  logger?.endSection();
24763
25068
  }
24764
- async function executeReconcileStep(context, logger) {
24765
- logger?.startSection("Step 5: Reconcile Balance");
25069
+ async function executeReconcileStep(context, worktree, logger) {
25070
+ logger?.startSection("Step 6: Reconcile Balance");
24766
25071
  logger?.logStep("Reconcile", "start");
24767
- const reconcileResult = await reconcileStatement(context.directory, context.agent, {
25072
+ const inWorktree = () => true;
25073
+ const reconcileResult = await reconcileStatement(worktree.path, context.agent, {
24768
25074
  provider: context.options.provider,
24769
25075
  currency: context.options.currency,
24770
25076
  closingBalance: context.options.closingBalance,
24771
25077
  account: context.options.account
24772
- }, context.configLoader, context.hledgerExecutor);
25078
+ }, context.configLoader, context.hledgerExecutor, inWorktree);
24773
25079
  const reconcileParsed = JSON.parse(reconcileResult);
24774
25080
  const message = reconcileParsed.success ? `Balance reconciled: ${reconcileParsed.actualBalance}` : `Balance mismatch: expected ${reconcileParsed.expectedBalance}, got ${reconcileParsed.actualBalance}`;
24775
25081
  logger?.logStep("Reconcile", reconcileParsed.success ? "success" : "error", message);
@@ -24793,9 +25099,42 @@ async function executeReconcileStep(context, logger) {
24793
25099
  }
24794
25100
  logger?.endSection();
24795
25101
  }
25102
+ async function executeMergeStep(context, worktree, logger) {
25103
+ logger?.startSection("Step 7: Merge to Main");
25104
+ logger?.logStep("Merge", "start");
25105
+ const importDetails = context.result.steps.import?.details;
25106
+ const reconcileDetails = context.result.steps.reconcile?.details;
25107
+ if (!importDetails || !reconcileDetails) {
25108
+ throw new Error("Import or reconcile step not completed before merge");
25109
+ }
25110
+ const commitInfo = {
25111
+ fromDate: reconcileDetails.metadata?.["from-date"],
25112
+ untilDate: reconcileDetails.metadata?.["until-date"]
25113
+ };
25114
+ const transactionCount = importDetails.summary?.totalTransactions || 0;
25115
+ const commitMessage = buildCommitMessage(context.options.provider, context.options.currency, commitInfo.fromDate, commitInfo.untilDate, transactionCount);
25116
+ try {
25117
+ logger?.info(`Commit message: "${commitMessage}"`);
25118
+ mergeWorktree(worktree, commitMessage);
25119
+ logger?.logStep("Merge", "success", "Merged to main branch");
25120
+ const mergeDetails = { commitMessage };
25121
+ context.result.steps.merge = buildStepResult(true, `Merged to main: "${commitMessage}"`, mergeDetails);
25122
+ cleanupIncomingFiles(worktree, context);
25123
+ logger?.endSection();
25124
+ } catch (error45) {
25125
+ logger?.logStep("Merge", "error");
25126
+ logger?.error("Merge to main branch failed", error45);
25127
+ logger?.endSection();
25128
+ const message = `Merge failed: ${error45 instanceof Error ? error45.message : String(error45)}`;
25129
+ context.result.steps.merge = buildStepResult(false, message);
25130
+ context.result.error = "Merge to main branch failed";
25131
+ throw new Error("Merge failed");
25132
+ }
25133
+ }
24796
25134
  function handleNoTransactions(result) {
24797
25135
  result.steps.import = buildStepResult(true, "No transactions to import");
24798
25136
  result.steps.reconcile = buildStepResult(true, "Reconciliation skipped (no transactions)");
25137
+ result.steps.merge = buildStepResult(true, "Merge skipped (no changes)");
24799
25138
  return buildSuccessResult4(result, "No transactions found to import");
24800
25139
  }
24801
25140
  async function importPipeline(directory, agent, options, configLoader = loadImportConfig, hledgerExecutor = defaultHledgerExecutor) {
@@ -24808,6 +25147,7 @@ async function importPipeline(directory, agent, options, configLoader = loadImpo
24808
25147
  logger.info(`Provider filter: ${options.provider || "all"}`);
24809
25148
  logger.info(`Currency filter: ${options.currency || "all"}`);
24810
25149
  logger.info(`Skip classify: ${options.skipClassify || false}`);
25150
+ logger.info(`Keep worktree on error: ${options.keepWorktreeOnError ?? true}`);
24811
25151
  logger.info("");
24812
25152
  const result = {
24813
25153
  success: false,
@@ -24822,68 +25162,176 @@ async function importPipeline(directory, agent, options, configLoader = loadImpo
24822
25162
  result
24823
25163
  };
24824
25164
  try {
24825
- await executeClassifyStep(context, logger);
24826
- await executeAccountDeclarationsStep(context, logger);
24827
- await executeDryRunStep(context, logger);
24828
- await executeImportStep(context, logger);
24829
- await executeReconcileStep(context, logger);
24830
- const transactionCount = context.result.steps.import?.details?.summary?.totalTransactions || 0;
24831
- logger.startSection("Summary");
24832
- logger.info(`Import completed successfully`);
24833
- logger.info(`Total transactions imported: ${transactionCount}`);
24834
- if (context.result.steps.reconcile?.details?.actualBalance) {
24835
- logger.info(`Balance: ${context.result.steps.reconcile.details.actualBalance}`);
24836
- }
24837
- logger.info(`Log file: ${logger.getLogPath()}`);
24838
- logger.endSection();
24839
- return buildSuccessResult4(result, `Successfully imported ${transactionCount} transaction(s)`);
25165
+ return await withWorktree(directory, async (worktree) => {
25166
+ logger.setContext("worktreeId", worktree.uuid);
25167
+ logger.setContext("worktreePath", worktree.path);
25168
+ result.worktreeId = worktree.uuid;
25169
+ result.steps.worktree = buildStepResult(true, `Created worktree at ${worktree.path}`, {
25170
+ path: worktree.path,
25171
+ branch: worktree.branch
25172
+ });
25173
+ logger.startSection("Step 1: Sync Files");
25174
+ logger.logStep("Sync Files", "start");
25175
+ try {
25176
+ const config2 = configLoader(directory);
25177
+ const syncResult = syncCSVFilesToWorktree(directory, worktree.path, config2.paths.import);
25178
+ if (syncResult.synced.length === 0 && syncResult.errors.length === 0) {
25179
+ logger.logStep("Sync Files", "success", "No CSV files to sync");
25180
+ result.steps.sync = buildStepResult(true, "No CSV files to sync", {
25181
+ synced: []
25182
+ });
25183
+ } else if (syncResult.errors.length > 0) {
25184
+ logger.warn(`Synced ${syncResult.synced.length} file(s) with ${syncResult.errors.length} error(s)`);
25185
+ result.steps.sync = buildStepResult(true, `Synced ${syncResult.synced.length} file(s) with ${syncResult.errors.length} error(s)`, { synced: syncResult.synced, errors: syncResult.errors });
25186
+ } 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
+ result.steps.sync = buildStepResult(true, `Synced ${syncResult.synced.length} CSV file(s) to worktree`, { synced: syncResult.synced });
25192
+ }
25193
+ logger.endSection();
25194
+ } catch (error45) {
25195
+ logger.logStep("Sync Files", "error");
25196
+ logger.error("Failed to sync CSV files", error45);
25197
+ logger.endSection();
25198
+ const errorMsg = error45 instanceof Error ? error45.message : String(error45);
25199
+ result.steps.sync = buildStepResult(false, `Failed to sync CSV files: ${errorMsg}`, { synced: [], errors: [{ file: "unknown", error: errorMsg }] });
25200
+ }
25201
+ 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);
25207
+ try {
25208
+ const config2 = configLoader(directory);
25209
+ const cleanupResult = cleanupProcessedCSVFiles(directory, config2.paths.import);
25210
+ if (cleanupResult.deleted.length === 0 && cleanupResult.errors.length === 0) {
25211
+ result.steps.cleanup = buildStepResult(true, "No CSV files to cleanup", { csvCleanup: { deleted: [] } });
25212
+ } else if (cleanupResult.errors.length > 0) {
25213
+ result.steps.cleanup = buildStepResult(true, `Deleted ${cleanupResult.deleted.length} CSV file(s) with ${cleanupResult.errors.length} error(s)`, {
25214
+ csvCleanup: {
25215
+ deleted: cleanupResult.deleted,
25216
+ errors: cleanupResult.errors
25217
+ }
25218
+ });
25219
+ } else {
25220
+ result.steps.cleanup = buildStepResult(true, `Deleted ${cleanupResult.deleted.length} CSV file(s) from main repo`, { csvCleanup: { deleted: cleanupResult.deleted } });
25221
+ }
25222
+ } catch (error45) {
25223
+ const errorMsg = error45 instanceof Error ? error45.message : String(error45);
25224
+ result.steps.cleanup = buildStepResult(false, `Failed to cleanup CSV files: ${errorMsg}`, {
25225
+ csvCleanup: {
25226
+ deleted: [],
25227
+ errors: [{ file: "unknown", error: errorMsg }]
25228
+ }
25229
+ });
25230
+ }
25231
+ await executeMergeStep(context, worktree, logger);
25232
+ const existingCleanup = result.steps.cleanup;
25233
+ if (existingCleanup) {
25234
+ existingCleanup.message += ", worktree cleaned up";
25235
+ existingCleanup.details = {
25236
+ ...existingCleanup.details,
25237
+ cleanedAfterSuccess: true
25238
+ };
25239
+ }
25240
+ 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
+ return buildSuccessResult4(result, `Successfully imported ${transactionCount} transaction(s)`);
25250
+ } 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,
25265
+ csvCleanup: { deleted: [] }
25266
+ });
25267
+ if (error45 instanceof NoTransactionsError) {
25268
+ return handleNoTransactions(result);
25269
+ }
25270
+ if (!result.error) {
25271
+ result.error = error45 instanceof Error ? error45.message : String(error45);
25272
+ }
25273
+ return buildErrorResult5(result, result.error, result.hint);
25274
+ }
25275
+ }, {
25276
+ keepOnError: options.keepWorktreeOnError ?? true,
25277
+ logger
25278
+ });
24840
25279
  } catch (error45) {
24841
- logger.error("Pipeline step failed", error45);
24842
- logger.info(`Log file: ${logger.getLogPath()}`);
24843
- if (error45 instanceof NoTransactionsError) {
24844
- return handleNoTransactions(result);
24845
- }
24846
- if (!result.error) {
24847
- result.error = error45 instanceof Error ? error45.message : String(error45);
24848
- }
24849
- return buildErrorResult5(result, result.error, result.hint);
25280
+ logger.error("Pipeline failed", error45);
25281
+ result.steps.worktree = buildStepResult(false, `Failed to create worktree: ${error45 instanceof Error ? error45.message : String(error45)}`);
25282
+ result.error = "Failed to create worktree";
25283
+ return buildErrorResult5(result, result.error);
24850
25284
  } finally {
24851
25285
  logger.endSection();
24852
25286
  await logger.flush();
24853
25287
  }
24854
25288
  }
24855
25289
  var import_pipeline_default = tool({
24856
- description: `ACCOUNTANT AGENT ONLY: Complete import pipeline with balance reconciliation.
25290
+ description: `ACCOUNTANT AGENT ONLY: Complete import pipeline with git worktree isolation and balance reconciliation.
24857
25291
 
24858
- This tool orchestrates the full import workflow:
25292
+ This tool orchestrates the full import workflow in an isolated git worktree:
24859
25293
 
24860
25294
  **Pipeline Steps:**
24861
- 1. **Classify**: Moves CSVs from import/incoming to import/pending (optional, skip with skipClassify)
24862
- 2. **Account Declarations**: Ensures all required accounts are declared in year journal
25295
+ 1. **Create Worktree**: Creates an isolated git worktree for safe import
25296
+ 2. **Classify**: Moves CSVs from import to pending directory (optional, skip with skipClassify)
24863
25297
  3. **Dry Run**: Validates all transactions have known accounts
24864
- 4. **Import**: Imports transactions to the journal (moves CSVs to import/done)
25298
+ 4. **Import**: Imports transactions to the journal
24865
25299
  5. **Reconcile**: Validates closing balance matches CSV metadata
25300
+ 6. **Merge**: Merges worktree to main with --no-ff
25301
+ 7. **Cleanup**: Removes worktree (or preserves on error)
24866
25302
 
24867
- **Important:**
24868
- - All changes remain uncommitted in your working directory
24869
- - If any step fails, changes remain in place for inspection
24870
- - CSV files move from incoming/ \u2192 pending/ \u2192 done/ during the process
25303
+ **Safety Features:**
25304
+ - All changes happen in isolated worktree
25305
+ - If any step fails, worktree is preserved by default for debugging
25306
+ - Balance reconciliation ensures data integrity
25307
+ - Atomic commit with merge --no-ff preserves history
25308
+
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)
24871
25315
 
24872
25316
  **Logging:**
24873
- - All operations logged to .memory/import-<timestamp>.md
24874
- - Log includes command output, timing, and error details
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)
24875
25321
 
24876
25322
  **Usage:**
24877
- - Basic: import-pipeline (processes all CSVs in incoming/)
25323
+ - Basic: import-pipeline (processes all pending CSVs)
24878
25324
  - Filtered: import-pipeline --provider ubs --currency chf
24879
- - Manual balance: import-pipeline --closingBalance "CHF 1234.56"
24880
- - Skip classify: import-pipeline --skipClassify true`,
25325
+ - With manual balance: import-pipeline --closingBalance "CHF 1234.56"
25326
+ - Skip classify: import-pipeline --skipClassify true
25327
+ - Always cleanup: import-pipeline --keepWorktreeOnError false`,
24881
25328
  args: {
24882
25329
  provider: tool.schema.string().optional().describe('Filter by provider (e.g., "ubs", "revolut")'),
24883
25330
  currency: tool.schema.string().optional().describe('Filter by currency (e.g., "chf", "eur")'),
24884
25331
  closingBalance: tool.schema.string().optional().describe("Manual closing balance override (if not in CSV metadata)"),
24885
25332
  account: tool.schema.string().optional().describe("Manual account override (auto-detected from rules file if not provided)"),
24886
- skipClassify: tool.schema.boolean().optional().describe("Skip the classify step (default: false)")
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)")
24887
25335
  },
24888
25336
  async execute(params, context) {
24889
25337
  const { directory, agent } = context;
@@ -24892,21 +25340,22 @@ This tool orchestrates the full import workflow:
24892
25340
  currency: params.currency,
24893
25341
  closingBalance: params.closingBalance,
24894
25342
  account: params.account,
24895
- skipClassify: params.skipClassify
25343
+ skipClassify: params.skipClassify,
25344
+ keepWorktreeOnError: params.keepWorktreeOnError
24896
25345
  });
24897
25346
  }
24898
25347
  });
24899
25348
  // src/tools/init-directories.ts
24900
- import * as fs13 from "fs";
24901
- import * as path13 from "path";
25349
+ import * as fs15 from "fs";
25350
+ import * as path14 from "path";
24902
25351
  async function initDirectories(directory) {
24903
25352
  try {
24904
25353
  const config2 = loadImportConfig(directory);
24905
25354
  const directoriesCreated = [];
24906
25355
  const gitkeepFiles = [];
24907
- const importBase = path13.join(directory, "import");
24908
- if (!fs13.existsSync(importBase)) {
24909
- fs13.mkdirSync(importBase, { recursive: true });
25356
+ const importBase = path14.join(directory, "import");
25357
+ if (!fs15.existsSync(importBase)) {
25358
+ fs15.mkdirSync(importBase, { recursive: true });
24910
25359
  directoriesCreated.push("import");
24911
25360
  }
24912
25361
  const pathsToCreate = [
@@ -24916,20 +25365,20 @@ async function initDirectories(directory) {
24916
25365
  { key: "unrecognized", path: config2.paths.unrecognized }
24917
25366
  ];
24918
25367
  for (const { path: dirPath } of pathsToCreate) {
24919
- const fullPath = path13.join(directory, dirPath);
24920
- if (!fs13.existsSync(fullPath)) {
24921
- fs13.mkdirSync(fullPath, { recursive: true });
25368
+ const fullPath = path14.join(directory, dirPath);
25369
+ if (!fs15.existsSync(fullPath)) {
25370
+ fs15.mkdirSync(fullPath, { recursive: true });
24922
25371
  directoriesCreated.push(dirPath);
24923
25372
  }
24924
- const gitkeepPath = path13.join(fullPath, ".gitkeep");
24925
- if (!fs13.existsSync(gitkeepPath)) {
24926
- fs13.writeFileSync(gitkeepPath, "");
24927
- gitkeepFiles.push(path13.join(dirPath, ".gitkeep"));
25373
+ const gitkeepPath = path14.join(fullPath, ".gitkeep");
25374
+ if (!fs15.existsSync(gitkeepPath)) {
25375
+ fs15.writeFileSync(gitkeepPath, "");
25376
+ gitkeepFiles.push(path14.join(dirPath, ".gitkeep"));
24928
25377
  }
24929
25378
  }
24930
- const gitignorePath = path13.join(importBase, ".gitignore");
25379
+ const gitignorePath = path14.join(importBase, ".gitignore");
24931
25380
  let gitignoreCreated = false;
24932
- if (!fs13.existsSync(gitignorePath)) {
25381
+ if (!fs15.existsSync(gitignorePath)) {
24933
25382
  const gitignoreContent = `# Ignore CSV/PDF files in temporary directories
24934
25383
  /incoming/*.csv
24935
25384
  /incoming/*.pdf
@@ -24947,7 +25396,7 @@ async function initDirectories(directory) {
24947
25396
  .DS_Store
24948
25397
  Thumbs.db
24949
25398
  `;
24950
- fs13.writeFileSync(gitignorePath, gitignoreContent);
25399
+ fs15.writeFileSync(gitignorePath, gitignoreContent);
24951
25400
  gitignoreCreated = true;
24952
25401
  }
24953
25402
  const parts = [];
@@ -25023,8 +25472,8 @@ You can now drop CSV files into import/incoming/ and run import-pipeline.`);
25023
25472
  }
25024
25473
  });
25025
25474
  // src/index.ts
25026
- var __dirname2 = dirname5(fileURLToPath3(import.meta.url));
25027
- var AGENT_FILE = join12(__dirname2, "..", "agent", "accountant.md");
25475
+ var __dirname2 = dirname6(fileURLToPath3(import.meta.url));
25476
+ var AGENT_FILE = join13(__dirname2, "..", "agent", "accountant.md");
25028
25477
  var AccountantPlugin = async () => {
25029
25478
  const agent = loadAgent(AGENT_FILE);
25030
25479
  return {