@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.
- package/dist/index.js +652 -88
- 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
|
|
1345
|
+
var fs9 = __require("fs");
|
|
1346
1346
|
|
|
1347
1347
|
class FileUtils {
|
|
1348
1348
|
readFile(fileInputName, encoding) {
|
|
1349
|
-
return
|
|
1349
|
+
return fs9.readFileSync(fileInputName, encoding).toString();
|
|
1350
1350
|
}
|
|
1351
1351
|
readFileAsync(fileInputName, encoding = "utf8") {
|
|
1352
|
-
if (
|
|
1353
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
1376
|
-
return
|
|
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
|
-
|
|
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
|
|
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
|
|
17277
|
-
import * as
|
|
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
|
|
17564
|
-
import * as
|
|
17718
|
+
import * as fs5 from "fs";
|
|
17719
|
+
import * as path6 from "path";
|
|
17565
17720
|
function findCSVFiles(importsDir) {
|
|
17566
|
-
if (!
|
|
17721
|
+
if (!fs5.existsSync(importsDir)) {
|
|
17567
17722
|
return [];
|
|
17568
17723
|
}
|
|
17569
|
-
return
|
|
17570
|
-
const fullPath =
|
|
17571
|
-
return
|
|
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 (!
|
|
17576
|
-
|
|
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 =
|
|
17618
|
-
const content =
|
|
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 =
|
|
17625
|
-
targetPath =
|
|
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 =
|
|
17783
|
+
targetPath = path7.join(unrecognizedDir, filename);
|
|
17629
17784
|
}
|
|
17630
|
-
if (
|
|
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 =
|
|
17806
|
+
const targetDir = path7.dirname(move.targetPath);
|
|
17652
17807
|
ensureDirectory(targetDir);
|
|
17653
|
-
|
|
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:
|
|
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
|
-
|
|
17818
|
+
fs6.renameSync(move.sourcePath, move.targetPath);
|
|
17664
17819
|
unrecognized.push({
|
|
17665
17820
|
filename: move.filename,
|
|
17666
|
-
targetPath:
|
|
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 =
|
|
17691
|
-
const pendingDir =
|
|
17692
|
-
const unrecognizedDir =
|
|
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
|
|
17714
|
-
import * as
|
|
17868
|
+
import * as fs10 from "fs";
|
|
17869
|
+
import * as path9 from "path";
|
|
17715
17870
|
|
|
17716
17871
|
// src/utils/rulesMatcher.ts
|
|
17717
|
-
import * as
|
|
17718
|
-
import * as
|
|
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 (
|
|
17882
|
+
if (path8.isAbsolute(sourcePath)) {
|
|
17728
17883
|
return sourcePath;
|
|
17729
17884
|
}
|
|
17730
|
-
const rulesDir =
|
|
17731
|
-
return
|
|
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 (!
|
|
17890
|
+
if (!fs7.existsSync(rulesDir)) {
|
|
17736
17891
|
return mapping;
|
|
17737
17892
|
}
|
|
17738
|
-
const files =
|
|
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 =
|
|
17744
|
-
const stat =
|
|
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 =
|
|
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 =
|
|
17917
|
+
const normalizedCsvPath = path8.normalize(csvPath);
|
|
17763
17918
|
for (const [mappedCsv, rulesFile] of Object.entries(mapping)) {
|
|
17764
|
-
if (
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
18186
|
-
const rulesFile = fileResult.rulesFile ?
|
|
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 =
|
|
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 =
|
|
18233
|
-
const destPath =
|
|
18234
|
-
const destDir =
|
|
18235
|
-
if (!
|
|
18236
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
18261
|
-
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:
|
|
18276
|
-
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 =
|
|
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:
|
|
18304
|
-
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 =
|
|
18327
|
-
const rulesDir =
|
|
18328
|
-
const doneDir =
|
|
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
|
|
18454
|
-
import * as
|
|
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 =
|
|
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 =
|
|
18509
|
-
const filename =
|
|
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
|
-
|
|
18518
|
-
|
|
18519
|
-
|
|
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
|
|
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 =
|
|
18573
|
-
const rulesDir =
|
|
18574
|
-
const mainJournalPath =
|
|
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
|
|
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 =
|
|
18699
|
-
var AGENT_FILE =
|
|
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) {
|