@fuzzle/opencode-accountant 0.4.1 → 0.4.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/agent/accountant.md +61 -1
- package/dist/index.js +418 -577
- 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
|
|
1551
|
+
var fs8 = __require("fs");
|
|
1552
1552
|
|
|
1553
1553
|
class FileUtils {
|
|
1554
1554
|
readFile(fileInputName, encoding) {
|
|
1555
|
-
return
|
|
1555
|
+
return fs8.readFileSync(fileInputName, encoding).toString();
|
|
1556
1556
|
}
|
|
1557
1557
|
readFileAsync(fileInputName, encoding = "utf8") {
|
|
1558
|
-
if (
|
|
1559
|
-
return
|
|
1558
|
+
if (fs8.promises && typeof fs8.promises.readFile === "function") {
|
|
1559
|
+
return fs8.promises.readFile(fileInputName, encoding).then((buf) => buf.toString());
|
|
1560
1560
|
}
|
|
1561
1561
|
return new Promise((resolve2, reject) => {
|
|
1562
|
-
|
|
1562
|
+
fs8.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
|
-
|
|
1572
|
+
fs8.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 (
|
|
1582
|
-
return
|
|
1581
|
+
if (fs8.promises && typeof fs8.promises.writeFile === "function") {
|
|
1582
|
+
return fs8.promises.writeFile(fileOutputName, json3);
|
|
1583
1583
|
}
|
|
1584
1584
|
return new Promise((resolve2, reject) => {
|
|
1585
|
-
|
|
1585
|
+
fs8.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
|
|
2150
|
+
import { dirname as dirname5, join as join12 } 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
|
|
17483
|
-
import * as
|
|
17482
|
+
import * as fs5 from "fs";
|
|
17483
|
+
import * as path6 from "path";
|
|
17484
17484
|
|
|
17485
17485
|
// src/utils/importConfig.ts
|
|
17486
17486
|
import * as fs3 from "fs";
|
|
@@ -17509,7 +17509,8 @@ function validatePaths(paths) {
|
|
|
17509
17509
|
pending: pathsObj.pending,
|
|
17510
17510
|
done: pathsObj.done,
|
|
17511
17511
|
unrecognized: pathsObj.unrecognized,
|
|
17512
|
-
rules: pathsObj.rules
|
|
17512
|
+
rules: pathsObj.rules,
|
|
17513
|
+
logs: pathsObj.logs
|
|
17513
17514
|
};
|
|
17514
17515
|
}
|
|
17515
17516
|
function validateDetectionRule(providerName, index, rule) {
|
|
@@ -17639,6 +17640,9 @@ function loadImportConfig(directory) {
|
|
|
17639
17640
|
for (const [name, config2] of Object.entries(providersObj)) {
|
|
17640
17641
|
providers[name] = validateProviderConfig(name, config2);
|
|
17641
17642
|
}
|
|
17643
|
+
if (!paths.logs) {
|
|
17644
|
+
paths.logs = ".memory";
|
|
17645
|
+
}
|
|
17642
17646
|
return { paths, providers };
|
|
17643
17647
|
}
|
|
17644
17648
|
|
|
@@ -17747,242 +17751,22 @@ function detectProvider(filename, content, config2) {
|
|
|
17747
17751
|
return null;
|
|
17748
17752
|
}
|
|
17749
17753
|
|
|
17750
|
-
// src/utils/
|
|
17751
|
-
import { spawnSync } from "child_process";
|
|
17752
|
-
|
|
17753
|
-
// node_modules/uuid/dist-node/stringify.js
|
|
17754
|
-
var byteToHex = [];
|
|
17755
|
-
for (let i2 = 0;i2 < 256; ++i2) {
|
|
17756
|
-
byteToHex.push((i2 + 256).toString(16).slice(1));
|
|
17757
|
-
}
|
|
17758
|
-
function unsafeStringify(arr, offset = 0) {
|
|
17759
|
-
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();
|
|
17760
|
-
}
|
|
17761
|
-
|
|
17762
|
-
// node_modules/uuid/dist-node/rng.js
|
|
17763
|
-
import { randomFillSync } from "crypto";
|
|
17764
|
-
var rnds8Pool = new Uint8Array(256);
|
|
17765
|
-
var poolPtr = rnds8Pool.length;
|
|
17766
|
-
function rng() {
|
|
17767
|
-
if (poolPtr > rnds8Pool.length - 16) {
|
|
17768
|
-
randomFillSync(rnds8Pool);
|
|
17769
|
-
poolPtr = 0;
|
|
17770
|
-
}
|
|
17771
|
-
return rnds8Pool.slice(poolPtr, poolPtr += 16);
|
|
17772
|
-
}
|
|
17773
|
-
|
|
17774
|
-
// node_modules/uuid/dist-node/native.js
|
|
17775
|
-
import { randomUUID } from "crypto";
|
|
17776
|
-
var native_default = { randomUUID };
|
|
17777
|
-
|
|
17778
|
-
// node_modules/uuid/dist-node/v4.js
|
|
17779
|
-
function _v4(options, buf, offset) {
|
|
17780
|
-
options = options || {};
|
|
17781
|
-
const rnds = options.random ?? options.rng?.() ?? rng();
|
|
17782
|
-
if (rnds.length < 16) {
|
|
17783
|
-
throw new Error("Random bytes length must be >= 16");
|
|
17784
|
-
}
|
|
17785
|
-
rnds[6] = rnds[6] & 15 | 64;
|
|
17786
|
-
rnds[8] = rnds[8] & 63 | 128;
|
|
17787
|
-
if (buf) {
|
|
17788
|
-
offset = offset || 0;
|
|
17789
|
-
if (offset < 0 || offset + 16 > buf.length) {
|
|
17790
|
-
throw new RangeError(`UUID byte range ${offset}:${offset + 15} is out of buffer bounds`);
|
|
17791
|
-
}
|
|
17792
|
-
for (let i2 = 0;i2 < 16; ++i2) {
|
|
17793
|
-
buf[offset + i2] = rnds[i2];
|
|
17794
|
-
}
|
|
17795
|
-
return buf;
|
|
17796
|
-
}
|
|
17797
|
-
return unsafeStringify(rnds);
|
|
17798
|
-
}
|
|
17799
|
-
function v4(options, buf, offset) {
|
|
17800
|
-
if (native_default.randomUUID && !buf && !options) {
|
|
17801
|
-
return native_default.randomUUID();
|
|
17802
|
-
}
|
|
17803
|
-
return _v4(options, buf, offset);
|
|
17804
|
-
}
|
|
17805
|
-
var v4_default = v4;
|
|
17806
|
-
// src/utils/worktreeManager.ts
|
|
17754
|
+
// src/utils/fileUtils.ts
|
|
17807
17755
|
import * as fs4 from "fs";
|
|
17808
17756
|
import * as path5 from "path";
|
|
17809
|
-
function execGit(args, cwd) {
|
|
17810
|
-
const result = spawnSync("git", args, { cwd, encoding: "utf-8" });
|
|
17811
|
-
if (result.status !== 0) {
|
|
17812
|
-
throw new Error(result.stderr || result.stdout || `git ${args[0]} failed`);
|
|
17813
|
-
}
|
|
17814
|
-
return (result.stdout || "").trim();
|
|
17815
|
-
}
|
|
17816
|
-
function execGitSafe(args, cwd) {
|
|
17817
|
-
const result = spawnSync("git", args, { cwd, encoding: "utf-8" });
|
|
17818
|
-
if (result.status !== 0) {
|
|
17819
|
-
return { success: false, output: result.stderr || result.stdout || `git ${args[0]} failed` };
|
|
17820
|
-
}
|
|
17821
|
-
return { success: true, output: (result.stdout || "").trim() };
|
|
17822
|
-
}
|
|
17823
|
-
function copyIncomingFiles(mainRepoPath, worktreePath) {
|
|
17824
|
-
const sourceDir = path5.join(mainRepoPath, "import/incoming");
|
|
17825
|
-
const targetDir = path5.join(worktreePath, "import/incoming");
|
|
17826
|
-
if (!fs4.existsSync(sourceDir)) {
|
|
17827
|
-
return;
|
|
17828
|
-
}
|
|
17829
|
-
fs4.mkdirSync(targetDir, { recursive: true });
|
|
17830
|
-
const entries = fs4.readdirSync(sourceDir, { withFileTypes: true });
|
|
17831
|
-
let copiedCount = 0;
|
|
17832
|
-
for (const entry of entries) {
|
|
17833
|
-
if (entry.isFile() && !entry.name.startsWith(".")) {
|
|
17834
|
-
const srcPath = path5.join(sourceDir, entry.name);
|
|
17835
|
-
const destPath = path5.join(targetDir, entry.name);
|
|
17836
|
-
fs4.copyFileSync(srcPath, destPath);
|
|
17837
|
-
copiedCount++;
|
|
17838
|
-
}
|
|
17839
|
-
}
|
|
17840
|
-
if (copiedCount > 0) {
|
|
17841
|
-
console.log(`[INFO] Copied ${copiedCount} file(s) from import/incoming/ to worktree`);
|
|
17842
|
-
}
|
|
17843
|
-
}
|
|
17844
|
-
function createImportWorktree(mainRepoPath, options = {}) {
|
|
17845
|
-
const baseDir = options.baseDir ?? "/tmp";
|
|
17846
|
-
const uuid3 = v4_default();
|
|
17847
|
-
const branch = `import-${uuid3}`;
|
|
17848
|
-
const worktreePath = path5.join(baseDir, `import-worktree-${uuid3}`);
|
|
17849
|
-
try {
|
|
17850
|
-
execGit(["rev-parse", "--git-dir"], mainRepoPath);
|
|
17851
|
-
} catch {
|
|
17852
|
-
throw new Error(`Not a git repository: ${mainRepoPath}`);
|
|
17853
|
-
}
|
|
17854
|
-
execGit(["branch", branch], mainRepoPath);
|
|
17855
|
-
try {
|
|
17856
|
-
execGit(["worktree", "add", worktreePath, branch], mainRepoPath);
|
|
17857
|
-
} catch (error45) {
|
|
17858
|
-
execGitSafe(["branch", "-D", branch], mainRepoPath);
|
|
17859
|
-
throw error45;
|
|
17860
|
-
}
|
|
17861
|
-
copyIncomingFiles(mainRepoPath, worktreePath);
|
|
17862
|
-
return {
|
|
17863
|
-
path: worktreePath,
|
|
17864
|
-
branch,
|
|
17865
|
-
uuid: uuid3,
|
|
17866
|
-
mainRepoPath
|
|
17867
|
-
};
|
|
17868
|
-
}
|
|
17869
|
-
function mergeWorktree(context, commitMessage) {
|
|
17870
|
-
const status = execGit(["status", "--porcelain"], context.path);
|
|
17871
|
-
if (status.length > 0) {
|
|
17872
|
-
execGit(["add", "-A"], context.path);
|
|
17873
|
-
execGit(["commit", "-m", commitMessage], context.path);
|
|
17874
|
-
}
|
|
17875
|
-
const currentBranch = execGit(["rev-parse", "--abbrev-ref", "HEAD"], context.mainRepoPath);
|
|
17876
|
-
execGit(["merge", "--no-ff", context.branch, "-m", commitMessage], context.mainRepoPath);
|
|
17877
|
-
if (currentBranch !== "main" && currentBranch !== "master") {
|
|
17878
|
-
execGit(["checkout", currentBranch], context.mainRepoPath);
|
|
17879
|
-
}
|
|
17880
|
-
}
|
|
17881
|
-
function removeWorktree(context, force = false) {
|
|
17882
|
-
const forceFlag = force ? "--force" : "";
|
|
17883
|
-
const args = ["worktree", "remove", context.path];
|
|
17884
|
-
if (forceFlag) {
|
|
17885
|
-
args.push(forceFlag);
|
|
17886
|
-
}
|
|
17887
|
-
const removeResult = execGitSafe(args, context.mainRepoPath);
|
|
17888
|
-
if (!removeResult.success) {
|
|
17889
|
-
if (!fs4.existsSync(context.path)) {} else {
|
|
17890
|
-
return { success: false, error: `Failed to remove worktree: ${removeResult.output}` };
|
|
17891
|
-
}
|
|
17892
|
-
}
|
|
17893
|
-
execGitSafe(["worktree", "prune"], context.mainRepoPath);
|
|
17894
|
-
const branchResult = execGitSafe(["branch", "-D", context.branch], context.mainRepoPath);
|
|
17895
|
-
if (!branchResult.success) {
|
|
17896
|
-
if (!branchResult.output.includes("not found")) {
|
|
17897
|
-
return { success: false, error: `Failed to delete branch: ${branchResult.output}` };
|
|
17898
|
-
}
|
|
17899
|
-
}
|
|
17900
|
-
return { success: true };
|
|
17901
|
-
}
|
|
17902
|
-
function isInWorktree(directory) {
|
|
17903
|
-
try {
|
|
17904
|
-
const gitDir = execGit(["rev-parse", "--git-dir"], directory);
|
|
17905
|
-
return gitDir.includes(".git/worktrees/");
|
|
17906
|
-
} catch {
|
|
17907
|
-
return false;
|
|
17908
|
-
}
|
|
17909
|
-
}
|
|
17910
|
-
async function withWorktree(directory, operation) {
|
|
17911
|
-
let createdWorktree = null;
|
|
17912
|
-
try {
|
|
17913
|
-
createdWorktree = createImportWorktree(directory);
|
|
17914
|
-
const result = await operation(createdWorktree);
|
|
17915
|
-
return result;
|
|
17916
|
-
} finally {
|
|
17917
|
-
if (createdWorktree) {
|
|
17918
|
-
removeWorktree(createdWorktree, true);
|
|
17919
|
-
}
|
|
17920
|
-
}
|
|
17921
|
-
}
|
|
17922
|
-
|
|
17923
|
-
// src/utils/fileUtils.ts
|
|
17924
|
-
import * as fs5 from "fs";
|
|
17925
|
-
import * as path6 from "path";
|
|
17926
17757
|
function findCSVFiles(importsDir) {
|
|
17927
|
-
if (!
|
|
17758
|
+
if (!fs4.existsSync(importsDir)) {
|
|
17928
17759
|
return [];
|
|
17929
17760
|
}
|
|
17930
|
-
return
|
|
17931
|
-
const fullPath =
|
|
17932
|
-
return
|
|
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();
|
|
17933
17764
|
});
|
|
17934
17765
|
}
|
|
17935
17766
|
function ensureDirectory(dirPath) {
|
|
17936
|
-
if (!
|
|
17937
|
-
|
|
17938
|
-
}
|
|
17939
|
-
}
|
|
17940
|
-
function syncCSVFilesToWorktree(mainRepoPath, worktreePath, importDir) {
|
|
17941
|
-
const result = {
|
|
17942
|
-
synced: [],
|
|
17943
|
-
errors: []
|
|
17944
|
-
};
|
|
17945
|
-
const mainImportDir = path6.join(mainRepoPath, importDir);
|
|
17946
|
-
const worktreeImportDir = path6.join(worktreePath, importDir);
|
|
17947
|
-
const csvFiles = findCSVFiles(mainImportDir);
|
|
17948
|
-
if (csvFiles.length === 0) {
|
|
17949
|
-
return result;
|
|
17950
|
-
}
|
|
17951
|
-
ensureDirectory(worktreeImportDir);
|
|
17952
|
-
for (const file2 of csvFiles) {
|
|
17953
|
-
try {
|
|
17954
|
-
const sourcePath = path6.join(mainImportDir, file2);
|
|
17955
|
-
const destPath = path6.join(worktreeImportDir, file2);
|
|
17956
|
-
fs5.copyFileSync(sourcePath, destPath);
|
|
17957
|
-
result.synced.push(file2);
|
|
17958
|
-
} catch (error45) {
|
|
17959
|
-
const errorMsg = error45 instanceof Error ? error45.message : String(error45);
|
|
17960
|
-
result.errors.push({ file: file2, error: errorMsg });
|
|
17961
|
-
}
|
|
17962
|
-
}
|
|
17963
|
-
return result;
|
|
17964
|
-
}
|
|
17965
|
-
function cleanupProcessedCSVFiles(mainRepoPath, importDir) {
|
|
17966
|
-
const result = {
|
|
17967
|
-
deleted: [],
|
|
17968
|
-
errors: []
|
|
17969
|
-
};
|
|
17970
|
-
const mainImportDir = path6.join(mainRepoPath, importDir);
|
|
17971
|
-
const csvFiles = findCSVFiles(mainImportDir);
|
|
17972
|
-
if (csvFiles.length === 0) {
|
|
17973
|
-
return result;
|
|
17974
|
-
}
|
|
17975
|
-
for (const file2 of csvFiles) {
|
|
17976
|
-
try {
|
|
17977
|
-
const filePath = path6.join(mainImportDir, file2);
|
|
17978
|
-
fs5.unlinkSync(filePath);
|
|
17979
|
-
result.deleted.push(file2);
|
|
17980
|
-
} catch (error45) {
|
|
17981
|
-
const errorMsg = error45 instanceof Error ? error45.message : String(error45);
|
|
17982
|
-
result.errors.push({ file: file2, error: errorMsg });
|
|
17983
|
-
}
|
|
17767
|
+
if (!fs4.existsSync(dirPath)) {
|
|
17768
|
+
fs4.mkdirSync(dirPath, { recursive: true });
|
|
17984
17769
|
}
|
|
17985
|
-
return result;
|
|
17986
17770
|
}
|
|
17987
17771
|
|
|
17988
17772
|
// src/tools/classify-statements.ts
|
|
@@ -18022,20 +17806,20 @@ function planMoves(csvFiles, importsDir, pendingDir, unrecognizedDir, config2) {
|
|
|
18022
17806
|
const plannedMoves = [];
|
|
18023
17807
|
const collisions = [];
|
|
18024
17808
|
for (const filename of csvFiles) {
|
|
18025
|
-
const sourcePath =
|
|
18026
|
-
const content =
|
|
17809
|
+
const sourcePath = path6.join(importsDir, filename);
|
|
17810
|
+
const content = fs5.readFileSync(sourcePath, "utf-8");
|
|
18027
17811
|
const detection = detectProvider(filename, content, config2);
|
|
18028
17812
|
let targetPath;
|
|
18029
17813
|
let targetFilename;
|
|
18030
17814
|
if (detection) {
|
|
18031
17815
|
targetFilename = detection.outputFilename || filename;
|
|
18032
|
-
const targetDir =
|
|
18033
|
-
targetPath =
|
|
17816
|
+
const targetDir = path6.join(pendingDir, detection.provider, detection.currency);
|
|
17817
|
+
targetPath = path6.join(targetDir, targetFilename);
|
|
18034
17818
|
} else {
|
|
18035
17819
|
targetFilename = filename;
|
|
18036
|
-
targetPath =
|
|
17820
|
+
targetPath = path6.join(unrecognizedDir, filename);
|
|
18037
17821
|
}
|
|
18038
|
-
if (
|
|
17822
|
+
if (fs5.existsSync(targetPath)) {
|
|
18039
17823
|
collisions.push({
|
|
18040
17824
|
filename,
|
|
18041
17825
|
existingPath: targetPath
|
|
@@ -18056,28 +17840,28 @@ function executeMoves(plannedMoves, config2, unrecognizedDir) {
|
|
|
18056
17840
|
const unrecognized = [];
|
|
18057
17841
|
for (const move of plannedMoves) {
|
|
18058
17842
|
if (move.detection) {
|
|
18059
|
-
const targetDir =
|
|
17843
|
+
const targetDir = path6.dirname(move.targetPath);
|
|
18060
17844
|
ensureDirectory(targetDir);
|
|
18061
|
-
|
|
17845
|
+
fs5.renameSync(move.sourcePath, move.targetPath);
|
|
18062
17846
|
classified.push({
|
|
18063
17847
|
filename: move.targetFilename,
|
|
18064
17848
|
originalFilename: move.detection.outputFilename ? move.filename : undefined,
|
|
18065
17849
|
provider: move.detection.provider,
|
|
18066
17850
|
currency: move.detection.currency,
|
|
18067
|
-
targetPath:
|
|
17851
|
+
targetPath: path6.join(config2.paths.pending, move.detection.provider, move.detection.currency, move.targetFilename)
|
|
18068
17852
|
});
|
|
18069
17853
|
} else {
|
|
18070
17854
|
ensureDirectory(unrecognizedDir);
|
|
18071
|
-
|
|
17855
|
+
fs5.renameSync(move.sourcePath, move.targetPath);
|
|
18072
17856
|
unrecognized.push({
|
|
18073
17857
|
filename: move.filename,
|
|
18074
|
-
targetPath:
|
|
17858
|
+
targetPath: path6.join(config2.paths.unrecognized, move.filename)
|
|
18075
17859
|
});
|
|
18076
17860
|
}
|
|
18077
17861
|
}
|
|
18078
17862
|
return { classified, unrecognized };
|
|
18079
17863
|
}
|
|
18080
|
-
async function classifyStatements(directory, agent, configLoader = loadImportConfig
|
|
17864
|
+
async function classifyStatements(directory, agent, configLoader = loadImportConfig) {
|
|
18081
17865
|
const restrictionError = checkAccountantAgent(agent, "classify statements", {
|
|
18082
17866
|
classified: [],
|
|
18083
17867
|
unrecognized: []
|
|
@@ -18085,9 +17869,6 @@ async function classifyStatements(directory, agent, configLoader = loadImportCon
|
|
|
18085
17869
|
if (restrictionError) {
|
|
18086
17870
|
return restrictionError;
|
|
18087
17871
|
}
|
|
18088
|
-
if (!worktreeChecker(directory)) {
|
|
18089
|
-
return buildErrorResult2("classify-statements must be run inside an import worktree", "Use import-pipeline tool to orchestrate the full workflow");
|
|
18090
|
-
}
|
|
18091
17872
|
let config2;
|
|
18092
17873
|
try {
|
|
18093
17874
|
config2 = configLoader(directory);
|
|
@@ -18095,9 +17876,9 @@ async function classifyStatements(directory, agent, configLoader = loadImportCon
|
|
|
18095
17876
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
18096
17877
|
return buildErrorResult2(errorMessage);
|
|
18097
17878
|
}
|
|
18098
|
-
const importsDir =
|
|
18099
|
-
const pendingDir =
|
|
18100
|
-
const unrecognizedDir =
|
|
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);
|
|
18101
17882
|
const csvFiles = findCSVFiles(importsDir);
|
|
18102
17883
|
if (csvFiles.length === 0) {
|
|
18103
17884
|
return buildSuccessResult2([], [], `No CSV files found in ${config2.paths.import}`);
|
|
@@ -18118,8 +17899,8 @@ var classify_statements_default = tool({
|
|
|
18118
17899
|
}
|
|
18119
17900
|
});
|
|
18120
17901
|
// src/tools/import-statements.ts
|
|
18121
|
-
import * as
|
|
18122
|
-
import * as
|
|
17902
|
+
import * as fs9 from "fs";
|
|
17903
|
+
import * as path9 from "path";
|
|
18123
17904
|
|
|
18124
17905
|
// node_modules/minimatch/dist/esm/index.js
|
|
18125
17906
|
var import_brace_expansion = __toESM(require_brace_expansion(), 1);
|
|
@@ -18712,11 +18493,11 @@ var qmarksTestNoExtDot = ([$0]) => {
|
|
|
18712
18493
|
return (f) => f.length === len && f !== "." && f !== "..";
|
|
18713
18494
|
};
|
|
18714
18495
|
var defaultPlatform = typeof process === "object" && process ? typeof process.env === "object" && process.env && process.env.__MINIMATCH_TESTING_PLATFORM__ || process.platform : "posix";
|
|
18715
|
-
var
|
|
18496
|
+
var path7 = {
|
|
18716
18497
|
win32: { sep: "\\" },
|
|
18717
18498
|
posix: { sep: "/" }
|
|
18718
18499
|
};
|
|
18719
|
-
var sep = defaultPlatform === "win32" ?
|
|
18500
|
+
var sep = defaultPlatform === "win32" ? path7.win32.sep : path7.posix.sep;
|
|
18720
18501
|
minimatch.sep = sep;
|
|
18721
18502
|
var GLOBSTAR = Symbol("globstar **");
|
|
18722
18503
|
minimatch.GLOBSTAR = GLOBSTAR;
|
|
@@ -20450,7 +20231,7 @@ class LRUCache {
|
|
|
20450
20231
|
// node_modules/path-scurry/dist/esm/index.js
|
|
20451
20232
|
import { posix, win32 } from "path";
|
|
20452
20233
|
import { fileURLToPath } from "url";
|
|
20453
|
-
import { lstatSync, readdir as readdirCB, readdirSync as
|
|
20234
|
+
import { lstatSync, readdir as readdirCB, readdirSync as readdirSync3, readlinkSync, realpathSync as rps } from "fs";
|
|
20454
20235
|
import * as actualFS from "fs";
|
|
20455
20236
|
import { lstat, readdir, readlink, realpath } from "fs/promises";
|
|
20456
20237
|
|
|
@@ -21122,7 +20903,7 @@ var realpathSync = rps.native;
|
|
|
21122
20903
|
var defaultFS = {
|
|
21123
20904
|
lstatSync,
|
|
21124
20905
|
readdir: readdirCB,
|
|
21125
|
-
readdirSync:
|
|
20906
|
+
readdirSync: readdirSync3,
|
|
21126
20907
|
readlinkSync,
|
|
21127
20908
|
realpathSync,
|
|
21128
20909
|
promises: {
|
|
@@ -21321,12 +21102,12 @@ class PathBase {
|
|
|
21321
21102
|
childrenCache() {
|
|
21322
21103
|
return this.#children;
|
|
21323
21104
|
}
|
|
21324
|
-
resolve(
|
|
21325
|
-
if (!
|
|
21105
|
+
resolve(path8) {
|
|
21106
|
+
if (!path8) {
|
|
21326
21107
|
return this;
|
|
21327
21108
|
}
|
|
21328
|
-
const rootPath = this.getRootString(
|
|
21329
|
-
const dir =
|
|
21109
|
+
const rootPath = this.getRootString(path8);
|
|
21110
|
+
const dir = path8.substring(rootPath.length);
|
|
21330
21111
|
const dirParts = dir.split(this.splitSep);
|
|
21331
21112
|
const result = rootPath ? this.getRoot(rootPath).#resolveParts(dirParts) : this.#resolveParts(dirParts);
|
|
21332
21113
|
return result;
|
|
@@ -21855,8 +21636,8 @@ class PathWin32 extends PathBase {
|
|
|
21855
21636
|
newChild(name, type2 = UNKNOWN, opts = {}) {
|
|
21856
21637
|
return new PathWin32(name, type2, this.root, this.roots, this.nocase, this.childrenCache(), opts);
|
|
21857
21638
|
}
|
|
21858
|
-
getRootString(
|
|
21859
|
-
return win32.parse(
|
|
21639
|
+
getRootString(path8) {
|
|
21640
|
+
return win32.parse(path8).root;
|
|
21860
21641
|
}
|
|
21861
21642
|
getRoot(rootPath) {
|
|
21862
21643
|
rootPath = uncToDrive(rootPath.toUpperCase());
|
|
@@ -21882,8 +21663,8 @@ class PathPosix extends PathBase {
|
|
|
21882
21663
|
constructor(name, type2 = UNKNOWN, root, roots, nocase, children, opts) {
|
|
21883
21664
|
super(name, type2, root, roots, nocase, children, opts);
|
|
21884
21665
|
}
|
|
21885
|
-
getRootString(
|
|
21886
|
-
return
|
|
21666
|
+
getRootString(path8) {
|
|
21667
|
+
return path8.startsWith("/") ? "/" : "";
|
|
21887
21668
|
}
|
|
21888
21669
|
getRoot(_rootPath) {
|
|
21889
21670
|
return this.root;
|
|
@@ -21903,8 +21684,8 @@ class PathScurryBase {
|
|
|
21903
21684
|
#children;
|
|
21904
21685
|
nocase;
|
|
21905
21686
|
#fs;
|
|
21906
|
-
constructor(cwd = process.cwd(), pathImpl, sep2, { nocase, childrenCacheSize = 16 * 1024, fs:
|
|
21907
|
-
this.#fs = fsFromOption(
|
|
21687
|
+
constructor(cwd = process.cwd(), pathImpl, sep2, { nocase, childrenCacheSize = 16 * 1024, fs: fs6 = defaultFS } = {}) {
|
|
21688
|
+
this.#fs = fsFromOption(fs6);
|
|
21908
21689
|
if (cwd instanceof URL || cwd.startsWith("file://")) {
|
|
21909
21690
|
cwd = fileURLToPath(cwd);
|
|
21910
21691
|
}
|
|
@@ -21940,11 +21721,11 @@ class PathScurryBase {
|
|
|
21940
21721
|
}
|
|
21941
21722
|
this.cwd = prev;
|
|
21942
21723
|
}
|
|
21943
|
-
depth(
|
|
21944
|
-
if (typeof
|
|
21945
|
-
|
|
21724
|
+
depth(path8 = this.cwd) {
|
|
21725
|
+
if (typeof path8 === "string") {
|
|
21726
|
+
path8 = this.cwd.resolve(path8);
|
|
21946
21727
|
}
|
|
21947
|
-
return
|
|
21728
|
+
return path8.depth();
|
|
21948
21729
|
}
|
|
21949
21730
|
childrenCache() {
|
|
21950
21731
|
return this.#children;
|
|
@@ -22360,9 +22141,9 @@ class PathScurryBase {
|
|
|
22360
22141
|
process2();
|
|
22361
22142
|
return results;
|
|
22362
22143
|
}
|
|
22363
|
-
chdir(
|
|
22144
|
+
chdir(path8 = this.cwd) {
|
|
22364
22145
|
const oldCwd = this.cwd;
|
|
22365
|
-
this.cwd = typeof
|
|
22146
|
+
this.cwd = typeof path8 === "string" ? this.cwd.resolve(path8) : path8;
|
|
22366
22147
|
this.cwd[setAsCwd](oldCwd);
|
|
22367
22148
|
}
|
|
22368
22149
|
}
|
|
@@ -22380,8 +22161,8 @@ class PathScurryWin32 extends PathScurryBase {
|
|
|
22380
22161
|
parseRootPath(dir) {
|
|
22381
22162
|
return win32.parse(dir).root.toUpperCase();
|
|
22382
22163
|
}
|
|
22383
|
-
newRoot(
|
|
22384
|
-
return new PathWin32(this.rootPath, IFDIR, undefined, this.roots, this.nocase, this.childrenCache(), { fs:
|
|
22164
|
+
newRoot(fs6) {
|
|
22165
|
+
return new PathWin32(this.rootPath, IFDIR, undefined, this.roots, this.nocase, this.childrenCache(), { fs: fs6 });
|
|
22385
22166
|
}
|
|
22386
22167
|
isAbsolute(p) {
|
|
22387
22168
|
return p.startsWith("/") || p.startsWith("\\") || /^[a-z]:(\/|\\)/i.test(p);
|
|
@@ -22398,8 +22179,8 @@ class PathScurryPosix extends PathScurryBase {
|
|
|
22398
22179
|
parseRootPath(_dir) {
|
|
22399
22180
|
return "/";
|
|
22400
22181
|
}
|
|
22401
|
-
newRoot(
|
|
22402
|
-
return new PathPosix(this.rootPath, IFDIR, undefined, this.roots, this.nocase, this.childrenCache(), { fs:
|
|
22182
|
+
newRoot(fs6) {
|
|
22183
|
+
return new PathPosix(this.rootPath, IFDIR, undefined, this.roots, this.nocase, this.childrenCache(), { fs: fs6 });
|
|
22403
22184
|
}
|
|
22404
22185
|
isAbsolute(p) {
|
|
22405
22186
|
return p.startsWith("/");
|
|
@@ -22651,8 +22432,8 @@ class MatchRecord {
|
|
|
22651
22432
|
this.store.set(target, current === undefined ? n : n & current);
|
|
22652
22433
|
}
|
|
22653
22434
|
entries() {
|
|
22654
|
-
return [...this.store.entries()].map(([
|
|
22655
|
-
|
|
22435
|
+
return [...this.store.entries()].map(([path8, n]) => [
|
|
22436
|
+
path8,
|
|
22656
22437
|
!!(n & 2),
|
|
22657
22438
|
!!(n & 1)
|
|
22658
22439
|
]);
|
|
@@ -22855,9 +22636,9 @@ class GlobUtil {
|
|
|
22855
22636
|
signal;
|
|
22856
22637
|
maxDepth;
|
|
22857
22638
|
includeChildMatches;
|
|
22858
|
-
constructor(patterns,
|
|
22639
|
+
constructor(patterns, path8, opts) {
|
|
22859
22640
|
this.patterns = patterns;
|
|
22860
|
-
this.path =
|
|
22641
|
+
this.path = path8;
|
|
22861
22642
|
this.opts = opts;
|
|
22862
22643
|
this.#sep = !opts.posix && opts.platform === "win32" ? "\\" : "/";
|
|
22863
22644
|
this.includeChildMatches = opts.includeChildMatches !== false;
|
|
@@ -22876,11 +22657,11 @@ class GlobUtil {
|
|
|
22876
22657
|
});
|
|
22877
22658
|
}
|
|
22878
22659
|
}
|
|
22879
|
-
#ignored(
|
|
22880
|
-
return this.seen.has(
|
|
22660
|
+
#ignored(path8) {
|
|
22661
|
+
return this.seen.has(path8) || !!this.#ignore?.ignored?.(path8);
|
|
22881
22662
|
}
|
|
22882
|
-
#childrenIgnored(
|
|
22883
|
-
return !!this.#ignore?.childrenIgnored?.(
|
|
22663
|
+
#childrenIgnored(path8) {
|
|
22664
|
+
return !!this.#ignore?.childrenIgnored?.(path8);
|
|
22884
22665
|
}
|
|
22885
22666
|
pause() {
|
|
22886
22667
|
this.paused = true;
|
|
@@ -23093,8 +22874,8 @@ class GlobUtil {
|
|
|
23093
22874
|
|
|
23094
22875
|
class GlobWalker extends GlobUtil {
|
|
23095
22876
|
matches = new Set;
|
|
23096
|
-
constructor(patterns,
|
|
23097
|
-
super(patterns,
|
|
22877
|
+
constructor(patterns, path8, opts) {
|
|
22878
|
+
super(patterns, path8, opts);
|
|
23098
22879
|
}
|
|
23099
22880
|
matchEmit(e) {
|
|
23100
22881
|
this.matches.add(e);
|
|
@@ -23132,8 +22913,8 @@ class GlobWalker extends GlobUtil {
|
|
|
23132
22913
|
|
|
23133
22914
|
class GlobStream extends GlobUtil {
|
|
23134
22915
|
results;
|
|
23135
|
-
constructor(patterns,
|
|
23136
|
-
super(patterns,
|
|
22916
|
+
constructor(patterns, path8, opts) {
|
|
22917
|
+
super(patterns, path8, opts);
|
|
23137
22918
|
this.results = new Minipass({
|
|
23138
22919
|
signal: this.signal,
|
|
23139
22920
|
objectMode: true
|
|
@@ -23401,8 +23182,8 @@ var glob = Object.assign(glob_, {
|
|
|
23401
23182
|
glob.glob = glob;
|
|
23402
23183
|
|
|
23403
23184
|
// src/utils/rulesMatcher.ts
|
|
23404
|
-
import * as
|
|
23405
|
-
import * as
|
|
23185
|
+
import * as fs6 from "fs";
|
|
23186
|
+
import * as path8 from "path";
|
|
23406
23187
|
function parseSourceDirective(content) {
|
|
23407
23188
|
const match2 = content.match(/^source\s+([^\n#]+)/m);
|
|
23408
23189
|
if (!match2) {
|
|
@@ -23411,28 +23192,28 @@ function parseSourceDirective(content) {
|
|
|
23411
23192
|
return match2[1].trim();
|
|
23412
23193
|
}
|
|
23413
23194
|
function resolveSourcePath(sourcePath, rulesFilePath) {
|
|
23414
|
-
if (
|
|
23195
|
+
if (path8.isAbsolute(sourcePath)) {
|
|
23415
23196
|
return sourcePath;
|
|
23416
23197
|
}
|
|
23417
|
-
const rulesDir =
|
|
23418
|
-
return
|
|
23198
|
+
const rulesDir = path8.dirname(rulesFilePath);
|
|
23199
|
+
return path8.resolve(rulesDir, sourcePath);
|
|
23419
23200
|
}
|
|
23420
23201
|
function loadRulesMapping(rulesDir) {
|
|
23421
23202
|
const mapping = {};
|
|
23422
|
-
if (!
|
|
23203
|
+
if (!fs6.existsSync(rulesDir)) {
|
|
23423
23204
|
return mapping;
|
|
23424
23205
|
}
|
|
23425
|
-
const files =
|
|
23206
|
+
const files = fs6.readdirSync(rulesDir);
|
|
23426
23207
|
for (const file2 of files) {
|
|
23427
23208
|
if (!file2.endsWith(".rules")) {
|
|
23428
23209
|
continue;
|
|
23429
23210
|
}
|
|
23430
|
-
const rulesFilePath =
|
|
23431
|
-
const stat =
|
|
23211
|
+
const rulesFilePath = path8.join(rulesDir, file2);
|
|
23212
|
+
const stat = fs6.statSync(rulesFilePath);
|
|
23432
23213
|
if (!stat.isFile()) {
|
|
23433
23214
|
continue;
|
|
23434
23215
|
}
|
|
23435
|
-
const content =
|
|
23216
|
+
const content = fs6.readFileSync(rulesFilePath, "utf-8");
|
|
23436
23217
|
const sourcePath = parseSourceDirective(content);
|
|
23437
23218
|
if (!sourcePath) {
|
|
23438
23219
|
continue;
|
|
@@ -23446,22 +23227,22 @@ function findRulesForCsv(csvPath, mapping) {
|
|
|
23446
23227
|
if (mapping[csvPath]) {
|
|
23447
23228
|
return mapping[csvPath];
|
|
23448
23229
|
}
|
|
23449
|
-
const normalizedCsvPath =
|
|
23230
|
+
const normalizedCsvPath = path8.normalize(csvPath);
|
|
23450
23231
|
for (const [mappedCsv, rulesFile] of Object.entries(mapping)) {
|
|
23451
|
-
const normalizedMappedCsv =
|
|
23232
|
+
const normalizedMappedCsv = path8.normalize(mappedCsv);
|
|
23452
23233
|
if (normalizedMappedCsv === normalizedCsvPath) {
|
|
23453
23234
|
return rulesFile;
|
|
23454
23235
|
}
|
|
23455
23236
|
}
|
|
23456
23237
|
for (const [pattern, rulesFile] of Object.entries(mapping)) {
|
|
23457
|
-
if (minimatch(csvPath, pattern) || minimatch(normalizedCsvPath,
|
|
23238
|
+
if (minimatch(csvPath, pattern) || minimatch(normalizedCsvPath, path8.normalize(pattern))) {
|
|
23458
23239
|
return rulesFile;
|
|
23459
23240
|
}
|
|
23460
23241
|
}
|
|
23461
|
-
const csvBasename =
|
|
23242
|
+
const csvBasename = path8.basename(csvPath);
|
|
23462
23243
|
const matches = [];
|
|
23463
23244
|
for (const rulesFile of Object.values(mapping)) {
|
|
23464
|
-
const rulesBasename =
|
|
23245
|
+
const rulesBasename = path8.basename(rulesFile, ".rules");
|
|
23465
23246
|
if (csvBasename.startsWith(rulesBasename)) {
|
|
23466
23247
|
matches.push({ rulesFile, prefixLength: rulesBasename.length });
|
|
23467
23248
|
}
|
|
@@ -23592,7 +23373,7 @@ async function getAccountBalance(mainJournalPath, account, asOfDate, executor =
|
|
|
23592
23373
|
}
|
|
23593
23374
|
|
|
23594
23375
|
// src/utils/rulesParser.ts
|
|
23595
|
-
import * as
|
|
23376
|
+
import * as fs7 from "fs";
|
|
23596
23377
|
function parseSkipRows(rulesContent) {
|
|
23597
23378
|
const match2 = rulesContent.match(/^skip\s+(\d+)/m);
|
|
23598
23379
|
return match2 ? parseInt(match2[1], 10) : 0;
|
|
@@ -23658,7 +23439,7 @@ function parseAccount1(rulesContent) {
|
|
|
23658
23439
|
}
|
|
23659
23440
|
function getAccountFromRulesFile(rulesFilePath) {
|
|
23660
23441
|
try {
|
|
23661
|
-
const content =
|
|
23442
|
+
const content = fs7.readFileSync(rulesFilePath, "utf-8");
|
|
23662
23443
|
return parseAccount1(content);
|
|
23663
23444
|
} catch {
|
|
23664
23445
|
return null;
|
|
@@ -23678,7 +23459,7 @@ function parseRulesFile(rulesContent) {
|
|
|
23678
23459
|
|
|
23679
23460
|
// src/utils/csvParser.ts
|
|
23680
23461
|
var import_convert_csv_to_json = __toESM(require_convert_csv_to_json(), 1);
|
|
23681
|
-
import * as
|
|
23462
|
+
import * as fs8 from "fs";
|
|
23682
23463
|
|
|
23683
23464
|
// src/utils/balanceUtils.ts
|
|
23684
23465
|
function parseAmountValue(amountStr) {
|
|
@@ -23727,7 +23508,7 @@ function balancesMatch(balance1, balance2) {
|
|
|
23727
23508
|
|
|
23728
23509
|
// src/utils/csvParser.ts
|
|
23729
23510
|
function parseCsvFile(csvPath, config2) {
|
|
23730
|
-
const csvContent =
|
|
23511
|
+
const csvContent = fs8.readFileSync(csvPath, "utf-8");
|
|
23731
23512
|
const lines = csvContent.split(`
|
|
23732
23513
|
`);
|
|
23733
23514
|
const headerIndex = config2.skipRows;
|
|
@@ -23885,21 +23666,21 @@ function buildSuccessResult3(files, summary, message) {
|
|
|
23885
23666
|
});
|
|
23886
23667
|
}
|
|
23887
23668
|
function findCsvFromRulesFile(rulesFile) {
|
|
23888
|
-
const content =
|
|
23669
|
+
const content = fs9.readFileSync(rulesFile, "utf-8");
|
|
23889
23670
|
const match2 = content.match(/^source\s+([^\n#]+)/m);
|
|
23890
23671
|
if (!match2) {
|
|
23891
23672
|
return null;
|
|
23892
23673
|
}
|
|
23893
23674
|
const sourcePath = match2[1].trim();
|
|
23894
|
-
const rulesDir =
|
|
23895
|
-
const absolutePattern =
|
|
23675
|
+
const rulesDir = path9.dirname(rulesFile);
|
|
23676
|
+
const absolutePattern = path9.resolve(rulesDir, sourcePath);
|
|
23896
23677
|
const matches = glob.sync(absolutePattern);
|
|
23897
23678
|
if (matches.length === 0) {
|
|
23898
23679
|
return null;
|
|
23899
23680
|
}
|
|
23900
23681
|
matches.sort((a, b) => {
|
|
23901
|
-
const aStat =
|
|
23902
|
-
const bStat =
|
|
23682
|
+
const aStat = fs9.statSync(a);
|
|
23683
|
+
const bStat = fs9.statSync(b);
|
|
23903
23684
|
return bStat.mtime.getTime() - aStat.mtime.getTime();
|
|
23904
23685
|
});
|
|
23905
23686
|
return matches[0];
|
|
@@ -23907,7 +23688,7 @@ function findCsvFromRulesFile(rulesFile) {
|
|
|
23907
23688
|
async function executeImports(fileResults, directory, pendingDir, doneDir, hledgerExecutor) {
|
|
23908
23689
|
const importedFiles = [];
|
|
23909
23690
|
for (const fileResult of fileResults) {
|
|
23910
|
-
const rulesFile = fileResult.rulesFile ?
|
|
23691
|
+
const rulesFile = fileResult.rulesFile ? path9.join(directory, fileResult.rulesFile) : null;
|
|
23911
23692
|
if (!rulesFile)
|
|
23912
23693
|
continue;
|
|
23913
23694
|
const year = fileResult.transactionYear;
|
|
@@ -23939,7 +23720,7 @@ async function executeImports(fileResults, directory, pendingDir, doneDir, hledg
|
|
|
23939
23720
|
importedFiles.push(importedCsv);
|
|
23940
23721
|
}
|
|
23941
23722
|
}
|
|
23942
|
-
const mainJournalPath =
|
|
23723
|
+
const mainJournalPath = path9.join(directory, ".hledger.journal");
|
|
23943
23724
|
const validationResult = await validateLedger(mainJournalPath, hledgerExecutor);
|
|
23944
23725
|
if (!validationResult.valid) {
|
|
23945
23726
|
return {
|
|
@@ -23949,13 +23730,13 @@ async function executeImports(fileResults, directory, pendingDir, doneDir, hledg
|
|
|
23949
23730
|
};
|
|
23950
23731
|
}
|
|
23951
23732
|
for (const csvFile of importedFiles) {
|
|
23952
|
-
const relativePath =
|
|
23953
|
-
const destPath =
|
|
23954
|
-
const destDir =
|
|
23955
|
-
if (!
|
|
23956
|
-
|
|
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 });
|
|
23957
23738
|
}
|
|
23958
|
-
|
|
23739
|
+
fs9.renameSync(csvFile, destPath);
|
|
23959
23740
|
}
|
|
23960
23741
|
return {
|
|
23961
23742
|
success: true,
|
|
@@ -23966,7 +23747,7 @@ async function processCsvFile(csvFile, rulesMapping, directory, hledgerExecutor)
|
|
|
23966
23747
|
const rulesFile = findRulesForCsv(csvFile, rulesMapping);
|
|
23967
23748
|
if (!rulesFile) {
|
|
23968
23749
|
return {
|
|
23969
|
-
csv:
|
|
23750
|
+
csv: path9.relative(directory, csvFile),
|
|
23970
23751
|
rulesFile: null,
|
|
23971
23752
|
totalTransactions: 0,
|
|
23972
23753
|
matchedTransactions: 0,
|
|
@@ -23977,8 +23758,8 @@ async function processCsvFile(csvFile, rulesMapping, directory, hledgerExecutor)
|
|
|
23977
23758
|
const result = await hledgerExecutor(["print", "-f", rulesFile]);
|
|
23978
23759
|
if (result.exitCode !== 0) {
|
|
23979
23760
|
return {
|
|
23980
|
-
csv:
|
|
23981
|
-
rulesFile:
|
|
23761
|
+
csv: path9.relative(directory, csvFile),
|
|
23762
|
+
rulesFile: path9.relative(directory, rulesFile),
|
|
23982
23763
|
totalTransactions: 0,
|
|
23983
23764
|
matchedTransactions: 0,
|
|
23984
23765
|
unknownPostings: [],
|
|
@@ -23992,8 +23773,8 @@ async function processCsvFile(csvFile, rulesMapping, directory, hledgerExecutor)
|
|
|
23992
23773
|
if (years.size > 1) {
|
|
23993
23774
|
const yearList = Array.from(years).sort().join(", ");
|
|
23994
23775
|
return {
|
|
23995
|
-
csv:
|
|
23996
|
-
rulesFile:
|
|
23776
|
+
csv: path9.relative(directory, csvFile),
|
|
23777
|
+
rulesFile: path9.relative(directory, rulesFile),
|
|
23997
23778
|
totalTransactions: transactionCount,
|
|
23998
23779
|
matchedTransactions: matchedCount,
|
|
23999
23780
|
unknownPostings: [],
|
|
@@ -24003,7 +23784,7 @@ async function processCsvFile(csvFile, rulesMapping, directory, hledgerExecutor)
|
|
|
24003
23784
|
const transactionYear = years.size === 1 ? Array.from(years)[0] : undefined;
|
|
24004
23785
|
if (unknownPostings.length > 0) {
|
|
24005
23786
|
try {
|
|
24006
|
-
const rulesContent =
|
|
23787
|
+
const rulesContent = fs9.readFileSync(rulesFile, "utf-8");
|
|
24007
23788
|
const rulesConfig = parseRulesFile(rulesContent);
|
|
24008
23789
|
const csvRows = parseCsvFile(csvFile, rulesConfig);
|
|
24009
23790
|
for (const posting of unknownPostings) {
|
|
@@ -24020,22 +23801,19 @@ async function processCsvFile(csvFile, rulesMapping, directory, hledgerExecutor)
|
|
|
24020
23801
|
}
|
|
24021
23802
|
}
|
|
24022
23803
|
return {
|
|
24023
|
-
csv:
|
|
24024
|
-
rulesFile:
|
|
23804
|
+
csv: path9.relative(directory, csvFile),
|
|
23805
|
+
rulesFile: path9.relative(directory, rulesFile),
|
|
24025
23806
|
totalTransactions: transactionCount,
|
|
24026
23807
|
matchedTransactions: matchedCount,
|
|
24027
23808
|
unknownPostings,
|
|
24028
23809
|
transactionYear
|
|
24029
23810
|
};
|
|
24030
23811
|
}
|
|
24031
|
-
async function importStatements(directory, agent, options, configLoader = loadImportConfig, hledgerExecutor = defaultHledgerExecutor
|
|
23812
|
+
async function importStatements(directory, agent, options, configLoader = loadImportConfig, hledgerExecutor = defaultHledgerExecutor) {
|
|
24032
23813
|
const restrictionError = checkAccountantAgent(agent, "import statements");
|
|
24033
23814
|
if (restrictionError) {
|
|
24034
23815
|
return restrictionError;
|
|
24035
23816
|
}
|
|
24036
|
-
if (!worktreeChecker(directory)) {
|
|
24037
|
-
return buildErrorResult3("import-statements must be run inside an import worktree", "Use import-pipeline tool to orchestrate the full workflow");
|
|
24038
|
-
}
|
|
24039
23817
|
let config2;
|
|
24040
23818
|
try {
|
|
24041
23819
|
config2 = configLoader(directory);
|
|
@@ -24043,9 +23821,9 @@ async function importStatements(directory, agent, options, configLoader = loadIm
|
|
|
24043
23821
|
const errorMessage = `Failed to load configuration: ${error45 instanceof Error ? error45.message : String(error45)}`;
|
|
24044
23822
|
return buildErrorResult3(errorMessage, 'Ensure config/import/providers.yaml exists with required paths including "rules"');
|
|
24045
23823
|
}
|
|
24046
|
-
const pendingDir =
|
|
24047
|
-
const rulesDir =
|
|
24048
|
-
const doneDir =
|
|
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);
|
|
24049
23827
|
const rulesMapping = loadRulesMapping(rulesDir);
|
|
24050
23828
|
const csvFiles = findCsvFiles(pendingDir, options.provider, options.currency);
|
|
24051
23829
|
if (csvFiles.length === 0) {
|
|
@@ -24089,8 +23867,8 @@ async function importStatements(directory, agent, options, configLoader = loadIm
|
|
|
24089
23867
|
}
|
|
24090
23868
|
for (const [_rulesFile, matchingCSVs] of rulesFileToCSVs.entries()) {
|
|
24091
23869
|
matchingCSVs.sort((a, b) => {
|
|
24092
|
-
const aStat =
|
|
24093
|
-
const bStat =
|
|
23870
|
+
const aStat = fs9.statSync(a);
|
|
23871
|
+
const bStat = fs9.statSync(b);
|
|
24094
23872
|
return bStat.mtime.getTime() - aStat.mtime.getTime();
|
|
24095
23873
|
});
|
|
24096
23874
|
const newestCSV = matchingCSVs[0];
|
|
@@ -24179,7 +23957,9 @@ This tool processes CSV files in the pending import directory and uses hledger's
|
|
|
24179
23957
|
1. Run with checkOnly: true (or no args)
|
|
24180
23958
|
2. If unknowns found, add rules to the appropriate .rules file
|
|
24181
23959
|
3. Repeat until no unknowns
|
|
24182
|
-
4. Run with checkOnly: false to import
|
|
23960
|
+
4. Run with checkOnly: false to import
|
|
23961
|
+
|
|
23962
|
+
Note: This tool is typically called via import-pipeline for the full workflow.`,
|
|
24183
23963
|
args: {
|
|
24184
23964
|
provider: tool.schema.string().optional().describe('Filter by provider (e.g., "revolut", "ubs"). If omitted, process all providers.'),
|
|
24185
23965
|
currency: tool.schema.string().optional().describe('Filter by currency (e.g., "chf", "eur"). If omitted, process all currencies for the provider.'),
|
|
@@ -24195,23 +23975,14 @@ This tool processes CSV files in the pending import directory and uses hledger's
|
|
|
24195
23975
|
}
|
|
24196
23976
|
});
|
|
24197
23977
|
// src/tools/reconcile-statement.ts
|
|
24198
|
-
import * as
|
|
24199
|
-
import * as
|
|
23978
|
+
import * as fs10 from "fs";
|
|
23979
|
+
import * as path10 from "path";
|
|
24200
23980
|
function buildErrorResult4(params) {
|
|
24201
23981
|
return JSON.stringify({
|
|
24202
23982
|
success: false,
|
|
24203
23983
|
...params
|
|
24204
23984
|
});
|
|
24205
23985
|
}
|
|
24206
|
-
function validateWorktree(directory, worktreeChecker) {
|
|
24207
|
-
if (!worktreeChecker(directory)) {
|
|
24208
|
-
return buildErrorResult4({
|
|
24209
|
-
error: "reconcile-statement must be run inside an import worktree",
|
|
24210
|
-
hint: "Use import-pipeline tool to orchestrate the full workflow"
|
|
24211
|
-
});
|
|
24212
|
-
}
|
|
24213
|
-
return null;
|
|
24214
|
-
}
|
|
24215
23986
|
function loadConfiguration(directory, configLoader) {
|
|
24216
23987
|
try {
|
|
24217
23988
|
const config2 = configLoader(directory);
|
|
@@ -24238,14 +24009,14 @@ function findCsvToReconcile(doneDir, options) {
|
|
|
24238
24009
|
};
|
|
24239
24010
|
}
|
|
24240
24011
|
const csvFile = csvFiles[csvFiles.length - 1];
|
|
24241
|
-
const relativePath =
|
|
24012
|
+
const relativePath = path10.relative(path10.dirname(path10.dirname(doneDir)), csvFile);
|
|
24242
24013
|
return { csvFile, relativePath };
|
|
24243
24014
|
}
|
|
24244
24015
|
function determineClosingBalance(csvFile, config2, options, relativeCsvPath, rulesDir) {
|
|
24245
24016
|
let metadata;
|
|
24246
24017
|
try {
|
|
24247
|
-
const content =
|
|
24248
|
-
const filename =
|
|
24018
|
+
const content = fs10.readFileSync(csvFile, "utf-8");
|
|
24019
|
+
const filename = path10.basename(csvFile);
|
|
24249
24020
|
const detectionResult = detectProvider(filename, content, config2);
|
|
24250
24021
|
metadata = detectionResult?.metadata;
|
|
24251
24022
|
} catch {
|
|
@@ -24327,7 +24098,7 @@ function tryExtractClosingBalanceFromCSV(csvFile, rulesDir) {
|
|
|
24327
24098
|
if (!rulesFile) {
|
|
24328
24099
|
return null;
|
|
24329
24100
|
}
|
|
24330
|
-
const rulesContent =
|
|
24101
|
+
const rulesContent = fs10.readFileSync(rulesFile, "utf-8");
|
|
24331
24102
|
const rulesConfig = parseRulesFile(rulesContent);
|
|
24332
24103
|
const csvRows = parseCsvFile(csvFile, rulesConfig);
|
|
24333
24104
|
if (csvRows.length === 0) {
|
|
@@ -24384,23 +24155,19 @@ function tryExtractClosingBalanceFromCSV(csvFile, rulesDir) {
|
|
|
24384
24155
|
return null;
|
|
24385
24156
|
}
|
|
24386
24157
|
}
|
|
24387
|
-
async function reconcileStatement(directory, agent, options, configLoader = loadImportConfig, hledgerExecutor = defaultHledgerExecutor
|
|
24158
|
+
async function reconcileStatement(directory, agent, options, configLoader = loadImportConfig, hledgerExecutor = defaultHledgerExecutor) {
|
|
24388
24159
|
const restrictionError = checkAccountantAgent(agent, "reconcile statement");
|
|
24389
24160
|
if (restrictionError) {
|
|
24390
24161
|
return restrictionError;
|
|
24391
24162
|
}
|
|
24392
|
-
const worktreeError = validateWorktree(directory, worktreeChecker);
|
|
24393
|
-
if (worktreeError) {
|
|
24394
|
-
return worktreeError;
|
|
24395
|
-
}
|
|
24396
24163
|
const configResult = loadConfiguration(directory, configLoader);
|
|
24397
24164
|
if ("error" in configResult) {
|
|
24398
24165
|
return configResult.error;
|
|
24399
24166
|
}
|
|
24400
24167
|
const { config: config2 } = configResult;
|
|
24401
|
-
const doneDir =
|
|
24402
|
-
const rulesDir =
|
|
24403
|
-
const mainJournalPath =
|
|
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");
|
|
24404
24171
|
const csvResult = findCsvToReconcile(doneDir, options);
|
|
24405
24172
|
if ("error" in csvResult) {
|
|
24406
24173
|
return csvResult.error;
|
|
@@ -24496,7 +24263,6 @@ var reconcile_statement_default = tool({
|
|
|
24496
24263
|
description: `ACCOUNTANT AGENT ONLY: Reconcile imported bank statement against closing balance.
|
|
24497
24264
|
|
|
24498
24265
|
This tool validates that the imported transactions result in the correct closing balance.
|
|
24499
|
-
It must be run inside an import worktree (use import-pipeline for the full workflow).
|
|
24500
24266
|
|
|
24501
24267
|
**Workflow:**
|
|
24502
24268
|
1. Finds the most recently imported CSV in the done directory
|
|
@@ -24529,17 +24295,16 @@ It must be run inside an import worktree (use import-pipeline for the full workf
|
|
|
24529
24295
|
}
|
|
24530
24296
|
});
|
|
24531
24297
|
// src/tools/import-pipeline.ts
|
|
24532
|
-
import * as fs13 from "fs";
|
|
24533
24298
|
import * as path12 from "path";
|
|
24534
24299
|
|
|
24535
24300
|
// src/utils/accountDeclarations.ts
|
|
24536
|
-
import * as
|
|
24301
|
+
import * as fs11 from "fs";
|
|
24537
24302
|
function extractAccountsFromRulesFile(rulesPath) {
|
|
24538
24303
|
const accounts = new Set;
|
|
24539
|
-
if (!
|
|
24304
|
+
if (!fs11.existsSync(rulesPath)) {
|
|
24540
24305
|
return accounts;
|
|
24541
24306
|
}
|
|
24542
|
-
const content =
|
|
24307
|
+
const content = fs11.readFileSync(rulesPath, "utf-8");
|
|
24543
24308
|
const lines = content.split(`
|
|
24544
24309
|
`);
|
|
24545
24310
|
for (const line of lines) {
|
|
@@ -24574,10 +24339,10 @@ function sortAccountDeclarations(accounts) {
|
|
|
24574
24339
|
return Array.from(accounts).sort((a, b) => a.localeCompare(b));
|
|
24575
24340
|
}
|
|
24576
24341
|
function ensureAccountDeclarations(yearJournalPath, accounts) {
|
|
24577
|
-
if (!
|
|
24342
|
+
if (!fs11.existsSync(yearJournalPath)) {
|
|
24578
24343
|
throw new Error(`Year journal not found: ${yearJournalPath}`);
|
|
24579
24344
|
}
|
|
24580
|
-
const content =
|
|
24345
|
+
const content = fs11.readFileSync(yearJournalPath, "utf-8");
|
|
24581
24346
|
const lines = content.split(`
|
|
24582
24347
|
`);
|
|
24583
24348
|
const existingAccounts = new Set;
|
|
@@ -24639,7 +24404,7 @@ function ensureAccountDeclarations(yearJournalPath, accounts) {
|
|
|
24639
24404
|
newContent.push("");
|
|
24640
24405
|
}
|
|
24641
24406
|
newContent.push(...otherLines);
|
|
24642
|
-
|
|
24407
|
+
fs11.writeFileSync(yearJournalPath, newContent.join(`
|
|
24643
24408
|
`));
|
|
24644
24409
|
return {
|
|
24645
24410
|
added: Array.from(missingAccounts).sort(),
|
|
@@ -24647,6 +24412,158 @@ function ensureAccountDeclarations(yearJournalPath, accounts) {
|
|
|
24647
24412
|
};
|
|
24648
24413
|
}
|
|
24649
24414
|
|
|
24415
|
+
// src/utils/logger.ts
|
|
24416
|
+
import fs12 from "fs/promises";
|
|
24417
|
+
import path11 from "path";
|
|
24418
|
+
|
|
24419
|
+
class MarkdownLogger {
|
|
24420
|
+
buffer = [];
|
|
24421
|
+
logPath;
|
|
24422
|
+
context = {};
|
|
24423
|
+
autoFlush;
|
|
24424
|
+
sectionDepth = 0;
|
|
24425
|
+
constructor(config2) {
|
|
24426
|
+
this.autoFlush = config2.autoFlush ?? true;
|
|
24427
|
+
this.context = config2.context || {};
|
|
24428
|
+
const filename = config2.filename || `import-${this.getTimestamp()}.md`;
|
|
24429
|
+
this.logPath = path11.join(config2.logDir, filename);
|
|
24430
|
+
this.buffer.push(`# Import Pipeline Log`);
|
|
24431
|
+
this.buffer.push(`**Started**: ${new Date().toLocaleString()}`);
|
|
24432
|
+
this.buffer.push("");
|
|
24433
|
+
}
|
|
24434
|
+
startSection(title, level = 2) {
|
|
24435
|
+
this.buffer.push("");
|
|
24436
|
+
this.buffer.push(`${"#".repeat(level + 1)} ${title}`);
|
|
24437
|
+
this.buffer.push(`**Started**: ${this.getTime()}`);
|
|
24438
|
+
this.buffer.push("");
|
|
24439
|
+
this.sectionDepth++;
|
|
24440
|
+
}
|
|
24441
|
+
endSection() {
|
|
24442
|
+
if (this.sectionDepth > 0) {
|
|
24443
|
+
this.buffer.push("");
|
|
24444
|
+
this.buffer.push("---");
|
|
24445
|
+
this.buffer.push("");
|
|
24446
|
+
this.sectionDepth--;
|
|
24447
|
+
}
|
|
24448
|
+
}
|
|
24449
|
+
info(message) {
|
|
24450
|
+
this.buffer.push(message);
|
|
24451
|
+
if (this.autoFlush)
|
|
24452
|
+
this.flushAsync();
|
|
24453
|
+
}
|
|
24454
|
+
warn(message) {
|
|
24455
|
+
this.buffer.push(`\u26A0\uFE0F **WARNING**: ${message}`);
|
|
24456
|
+
if (this.autoFlush)
|
|
24457
|
+
this.flushAsync();
|
|
24458
|
+
}
|
|
24459
|
+
error(message, error45) {
|
|
24460
|
+
this.buffer.push(`\u274C **ERROR**: ${message}`);
|
|
24461
|
+
if (error45) {
|
|
24462
|
+
const errorStr = error45 instanceof Error ? error45.message : String(error45);
|
|
24463
|
+
this.buffer.push("");
|
|
24464
|
+
this.buffer.push("```");
|
|
24465
|
+
this.buffer.push(errorStr);
|
|
24466
|
+
if (error45 instanceof Error && error45.stack) {
|
|
24467
|
+
this.buffer.push("");
|
|
24468
|
+
this.buffer.push(error45.stack);
|
|
24469
|
+
}
|
|
24470
|
+
this.buffer.push("```");
|
|
24471
|
+
this.buffer.push("");
|
|
24472
|
+
}
|
|
24473
|
+
if (this.autoFlush)
|
|
24474
|
+
this.flushAsync();
|
|
24475
|
+
}
|
|
24476
|
+
debug(message) {
|
|
24477
|
+
this.buffer.push(`\uD83D\uDD0D ${message}`);
|
|
24478
|
+
if (this.autoFlush)
|
|
24479
|
+
this.flushAsync();
|
|
24480
|
+
}
|
|
24481
|
+
logStep(stepName, status, details) {
|
|
24482
|
+
const icon = status === "success" ? "\u2705" : status === "error" ? "\u274C" : "\u25B6\uFE0F";
|
|
24483
|
+
const statusText = status.charAt(0).toUpperCase() + status.slice(1);
|
|
24484
|
+
this.buffer.push(`**${stepName}**: ${icon} ${statusText}`);
|
|
24485
|
+
if (details) {
|
|
24486
|
+
this.buffer.push(` ${details}`);
|
|
24487
|
+
}
|
|
24488
|
+
this.buffer.push("");
|
|
24489
|
+
if (this.autoFlush)
|
|
24490
|
+
this.flushAsync();
|
|
24491
|
+
}
|
|
24492
|
+
logCommand(command, output) {
|
|
24493
|
+
this.buffer.push("```bash");
|
|
24494
|
+
this.buffer.push(`$ ${command}`);
|
|
24495
|
+
if (output) {
|
|
24496
|
+
this.buffer.push("");
|
|
24497
|
+
const lines = output.trim().split(`
|
|
24498
|
+
`);
|
|
24499
|
+
if (lines.length > 50) {
|
|
24500
|
+
this.buffer.push(...lines.slice(0, 50));
|
|
24501
|
+
this.buffer.push(`... (${lines.length - 50} more lines omitted)`);
|
|
24502
|
+
} else {
|
|
24503
|
+
this.buffer.push(output.trim());
|
|
24504
|
+
}
|
|
24505
|
+
}
|
|
24506
|
+
this.buffer.push("```");
|
|
24507
|
+
this.buffer.push("");
|
|
24508
|
+
if (this.autoFlush)
|
|
24509
|
+
this.flushAsync();
|
|
24510
|
+
}
|
|
24511
|
+
logResult(data) {
|
|
24512
|
+
this.buffer.push("```json");
|
|
24513
|
+
this.buffer.push(JSON.stringify(data, null, 2));
|
|
24514
|
+
this.buffer.push("```");
|
|
24515
|
+
this.buffer.push("");
|
|
24516
|
+
if (this.autoFlush)
|
|
24517
|
+
this.flushAsync();
|
|
24518
|
+
}
|
|
24519
|
+
setContext(key, value) {
|
|
24520
|
+
this.context[key] = value;
|
|
24521
|
+
}
|
|
24522
|
+
async flush() {
|
|
24523
|
+
if (this.buffer.length === 0)
|
|
24524
|
+
return;
|
|
24525
|
+
try {
|
|
24526
|
+
await fs12.mkdir(path11.dirname(this.logPath), { recursive: true });
|
|
24527
|
+
await fs12.writeFile(this.logPath, this.buffer.join(`
|
|
24528
|
+
`), "utf-8");
|
|
24529
|
+
} catch {}
|
|
24530
|
+
}
|
|
24531
|
+
getLogPath() {
|
|
24532
|
+
return this.logPath;
|
|
24533
|
+
}
|
|
24534
|
+
flushAsync() {
|
|
24535
|
+
this.flush().catch(() => {});
|
|
24536
|
+
}
|
|
24537
|
+
getTimestamp() {
|
|
24538
|
+
return new Date().toISOString().replace(/:/g, "-").split(".")[0];
|
|
24539
|
+
}
|
|
24540
|
+
getTime() {
|
|
24541
|
+
return new Date().toLocaleTimeString();
|
|
24542
|
+
}
|
|
24543
|
+
}
|
|
24544
|
+
function createLogger(config2) {
|
|
24545
|
+
return new MarkdownLogger(config2);
|
|
24546
|
+
}
|
|
24547
|
+
function createImportLogger(directory, worktreeId, provider) {
|
|
24548
|
+
const context = {};
|
|
24549
|
+
if (worktreeId)
|
|
24550
|
+
context.worktreeId = worktreeId;
|
|
24551
|
+
if (provider)
|
|
24552
|
+
context.provider = provider;
|
|
24553
|
+
const logger = createLogger({
|
|
24554
|
+
logDir: path11.join(directory, ".memory"),
|
|
24555
|
+
autoFlush: true,
|
|
24556
|
+
context
|
|
24557
|
+
});
|
|
24558
|
+
if (worktreeId)
|
|
24559
|
+
logger.info(`**Worktree ID**: ${worktreeId}`);
|
|
24560
|
+
if (provider)
|
|
24561
|
+
logger.info(`**Provider**: ${provider}`);
|
|
24562
|
+
logger.info(`**Repository**: ${directory}`);
|
|
24563
|
+
logger.info("");
|
|
24564
|
+
return logger;
|
|
24565
|
+
}
|
|
24566
|
+
|
|
24650
24567
|
// src/tools/import-pipeline.ts
|
|
24651
24568
|
class NoTransactionsError extends Error {
|
|
24652
24569
|
constructor() {
|
|
@@ -24674,73 +24591,38 @@ function buildErrorResult5(result, error45, hint) {
|
|
|
24674
24591
|
}
|
|
24675
24592
|
return JSON.stringify(result);
|
|
24676
24593
|
}
|
|
24677
|
-
function
|
|
24678
|
-
|
|
24679
|
-
|
|
24680
|
-
const dateRange = fromDate && untilDate ? ` ${fromDate} to ${untilDate}` : "";
|
|
24681
|
-
const txStr = transactionCount > 0 ? ` (${transactionCount} transactions)` : "";
|
|
24682
|
-
const parts = ["Import:", providerStr];
|
|
24683
|
-
if (currencyStr) {
|
|
24684
|
-
parts.push(currencyStr);
|
|
24685
|
-
}
|
|
24686
|
-
return `${parts.join(" ")}${dateRange}${txStr}`;
|
|
24687
|
-
}
|
|
24688
|
-
function cleanupIncomingFiles(worktree, context) {
|
|
24689
|
-
const incomingDir = path12.join(worktree.mainRepoPath, "import/incoming");
|
|
24690
|
-
if (!fs13.existsSync(incomingDir)) {
|
|
24691
|
-
return;
|
|
24692
|
-
}
|
|
24693
|
-
const importStep = context.result.steps.import;
|
|
24694
|
-
if (!importStep?.success || !importStep.details) {
|
|
24695
|
-
return;
|
|
24696
|
-
}
|
|
24697
|
-
const importResult = importStep.details;
|
|
24698
|
-
if (!importResult.files || !Array.isArray(importResult.files)) {
|
|
24699
|
-
return;
|
|
24700
|
-
}
|
|
24701
|
-
let deletedCount = 0;
|
|
24702
|
-
for (const fileResult of importResult.files) {
|
|
24703
|
-
if (!fileResult.csv)
|
|
24704
|
-
continue;
|
|
24705
|
-
const filename = path12.basename(fileResult.csv);
|
|
24706
|
-
const filePath = path12.join(incomingDir, filename);
|
|
24707
|
-
if (fs13.existsSync(filePath)) {
|
|
24708
|
-
try {
|
|
24709
|
-
fs13.unlinkSync(filePath);
|
|
24710
|
-
deletedCount++;
|
|
24711
|
-
} catch (error45) {
|
|
24712
|
-
console.error(`[ERROR] Failed to delete ${filename}: ${error45 instanceof Error ? error45.message : String(error45)}`);
|
|
24713
|
-
}
|
|
24714
|
-
}
|
|
24715
|
-
}
|
|
24716
|
-
if (deletedCount > 0) {
|
|
24717
|
-
console.log(`[INFO] Cleaned up ${deletedCount} file(s) from import/incoming/`);
|
|
24718
|
-
}
|
|
24719
|
-
}
|
|
24720
|
-
async function executeClassifyStep(context, worktree) {
|
|
24594
|
+
async function executeClassifyStep(context, logger) {
|
|
24595
|
+
logger?.startSection("Step 1: Classify Transactions");
|
|
24596
|
+
logger?.logStep("Classify", "start");
|
|
24721
24597
|
if (context.options.skipClassify) {
|
|
24598
|
+
logger?.info("Classification skipped (skipClassify: true)");
|
|
24722
24599
|
context.result.steps.classify = buildStepResult(true, "Classification skipped (skipClassify: true)");
|
|
24600
|
+
logger?.endSection();
|
|
24723
24601
|
return;
|
|
24724
24602
|
}
|
|
24725
|
-
const
|
|
24726
|
-
const classifyResult = await classifyStatements(worktree.path, context.agent, context.configLoader, inWorktree);
|
|
24603
|
+
const classifyResult = await classifyStatements(context.directory, context.agent, context.configLoader);
|
|
24727
24604
|
const classifyParsed = JSON.parse(classifyResult);
|
|
24728
24605
|
const success2 = classifyParsed.success !== false;
|
|
24729
24606
|
let message = success2 ? "Classification complete" : "Classification had issues";
|
|
24730
24607
|
if (classifyParsed.unrecognized?.length > 0) {
|
|
24731
24608
|
message = `Classification complete with ${classifyParsed.unrecognized.length} unrecognized file(s)`;
|
|
24609
|
+
logger?.warn(`${classifyParsed.unrecognized.length} unrecognized file(s)`);
|
|
24732
24610
|
}
|
|
24611
|
+
logger?.logStep("Classify", success2 ? "success" : "error", message);
|
|
24733
24612
|
const details = {
|
|
24734
24613
|
success: success2,
|
|
24735
24614
|
unrecognized: classifyParsed.unrecognized,
|
|
24736
24615
|
classified: classifyParsed
|
|
24737
24616
|
};
|
|
24738
24617
|
context.result.steps.classify = buildStepResult(success2, message, details);
|
|
24739
|
-
|
|
24740
|
-
|
|
24741
|
-
|
|
24742
|
-
|
|
24743
|
-
|
|
24618
|
+
logger?.endSection();
|
|
24619
|
+
}
|
|
24620
|
+
async function executeAccountDeclarationsStep(context, logger) {
|
|
24621
|
+
logger?.startSection("Step 2: Check Account Declarations");
|
|
24622
|
+
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);
|
|
24744
24626
|
const csvFiles = findCsvFiles(pendingDir, context.options.provider, context.options.currency);
|
|
24745
24627
|
if (csvFiles.length === 0) {
|
|
24746
24628
|
context.result.steps.accountDeclarations = buildStepResult(true, "No CSV files to process", {
|
|
@@ -24771,7 +24653,7 @@ async function executeAccountDeclarationsStep(context, worktree) {
|
|
|
24771
24653
|
context.result.steps.accountDeclarations = buildStepResult(true, "No accounts found in rules files", {
|
|
24772
24654
|
accountsAdded: [],
|
|
24773
24655
|
journalUpdated: "",
|
|
24774
|
-
rulesScanned: Array.from(matchedRulesFiles).map((f) => path12.relative(
|
|
24656
|
+
rulesScanned: Array.from(matchedRulesFiles).map((f) => path12.relative(context.directory, f))
|
|
24775
24657
|
});
|
|
24776
24658
|
return;
|
|
24777
24659
|
}
|
|
@@ -24794,80 +24676,107 @@ async function executeAccountDeclarationsStep(context, worktree) {
|
|
|
24794
24676
|
context.result.steps.accountDeclarations = buildStepResult(false, "Could not determine transaction year from CSV files", {
|
|
24795
24677
|
accountsAdded: [],
|
|
24796
24678
|
journalUpdated: "",
|
|
24797
|
-
rulesScanned: Array.from(matchedRulesFiles).map((f) => path12.relative(
|
|
24679
|
+
rulesScanned: Array.from(matchedRulesFiles).map((f) => path12.relative(context.directory, f))
|
|
24798
24680
|
});
|
|
24799
24681
|
return;
|
|
24800
24682
|
}
|
|
24801
24683
|
let yearJournalPath;
|
|
24802
24684
|
try {
|
|
24803
|
-
yearJournalPath = ensureYearJournalExists(
|
|
24685
|
+
yearJournalPath = ensureYearJournalExists(context.directory, transactionYear);
|
|
24804
24686
|
} catch (error45) {
|
|
24805
24687
|
context.result.steps.accountDeclarations = buildStepResult(false, `Failed to create year journal: ${error45 instanceof Error ? error45.message : String(error45)}`, {
|
|
24806
24688
|
accountsAdded: [],
|
|
24807
24689
|
journalUpdated: "",
|
|
24808
|
-
rulesScanned: Array.from(matchedRulesFiles).map((f) => path12.relative(
|
|
24690
|
+
rulesScanned: Array.from(matchedRulesFiles).map((f) => path12.relative(context.directory, f))
|
|
24809
24691
|
});
|
|
24810
24692
|
return;
|
|
24811
24693
|
}
|
|
24812
24694
|
const result = ensureAccountDeclarations(yearJournalPath, allAccounts);
|
|
24813
|
-
const message = result.added.length > 0 ? `Added ${result.added.length} account declaration(s) to ${path12.relative(
|
|
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";
|
|
24696
|
+
logger?.logStep("Check Accounts", "success", message);
|
|
24697
|
+
if (result.added.length > 0) {
|
|
24698
|
+
for (const account of result.added) {
|
|
24699
|
+
logger?.info(` - ${account}`);
|
|
24700
|
+
}
|
|
24701
|
+
}
|
|
24814
24702
|
context.result.steps.accountDeclarations = buildStepResult(true, message, {
|
|
24815
24703
|
accountsAdded: result.added,
|
|
24816
|
-
journalUpdated: path12.relative(
|
|
24817
|
-
rulesScanned: Array.from(matchedRulesFiles).map((f) => path12.relative(
|
|
24704
|
+
journalUpdated: path12.relative(context.directory, yearJournalPath),
|
|
24705
|
+
rulesScanned: Array.from(matchedRulesFiles).map((f) => path12.relative(context.directory, f))
|
|
24818
24706
|
});
|
|
24707
|
+
logger?.endSection();
|
|
24819
24708
|
}
|
|
24820
|
-
async function executeDryRunStep(context,
|
|
24821
|
-
|
|
24822
|
-
|
|
24709
|
+
async function executeDryRunStep(context, logger) {
|
|
24710
|
+
logger?.startSection("Step 3: Dry Run Import");
|
|
24711
|
+
logger?.logStep("Dry Run", "start");
|
|
24712
|
+
const dryRunResult = await importStatements(context.directory, context.agent, {
|
|
24823
24713
|
provider: context.options.provider,
|
|
24824
24714
|
currency: context.options.currency,
|
|
24825
24715
|
checkOnly: true
|
|
24826
|
-
}, context.configLoader, context.hledgerExecutor
|
|
24716
|
+
}, context.configLoader, context.hledgerExecutor);
|
|
24827
24717
|
const dryRunParsed = JSON.parse(dryRunResult);
|
|
24828
24718
|
const message = dryRunParsed.success ? `Dry run passed: ${dryRunParsed.summary?.totalTransactions || 0} transactions ready` : `Dry run failed: ${dryRunParsed.summary?.unknown || 0} unknown account(s)`;
|
|
24719
|
+
logger?.logStep("Dry Run", dryRunParsed.success ? "success" : "error", message);
|
|
24720
|
+
if (dryRunParsed.summary?.totalTransactions) {
|
|
24721
|
+
logger?.info(`Found ${dryRunParsed.summary.totalTransactions} transactions`);
|
|
24722
|
+
}
|
|
24829
24723
|
context.result.steps.dryRun = buildStepResult(dryRunParsed.success, message, {
|
|
24830
24724
|
success: dryRunParsed.success,
|
|
24831
24725
|
summary: dryRunParsed.summary
|
|
24832
24726
|
});
|
|
24833
24727
|
if (!dryRunParsed.success) {
|
|
24728
|
+
logger?.error("Dry run found unknown accounts or errors");
|
|
24729
|
+
logger?.endSection();
|
|
24834
24730
|
context.result.error = "Dry run found unknown accounts or errors";
|
|
24835
24731
|
context.result.hint = "Add rules to categorize unknown transactions, then retry";
|
|
24836
24732
|
throw new Error("Dry run failed");
|
|
24837
24733
|
}
|
|
24838
24734
|
if (dryRunParsed.summary?.totalTransactions === 0) {
|
|
24735
|
+
logger?.endSection();
|
|
24839
24736
|
throw new NoTransactionsError;
|
|
24840
24737
|
}
|
|
24738
|
+
logger?.endSection();
|
|
24841
24739
|
}
|
|
24842
|
-
async function executeImportStep(context,
|
|
24843
|
-
|
|
24844
|
-
|
|
24740
|
+
async function executeImportStep(context, logger) {
|
|
24741
|
+
logger?.startSection("Step 4: Import Transactions");
|
|
24742
|
+
logger?.logStep("Import", "start");
|
|
24743
|
+
const importResult = await importStatements(context.directory, context.agent, {
|
|
24845
24744
|
provider: context.options.provider,
|
|
24846
24745
|
currency: context.options.currency,
|
|
24847
24746
|
checkOnly: false
|
|
24848
|
-
}, context.configLoader, context.hledgerExecutor
|
|
24747
|
+
}, context.configLoader, context.hledgerExecutor);
|
|
24849
24748
|
const importParsed = JSON.parse(importResult);
|
|
24850
24749
|
const message = importParsed.success ? `Imported ${importParsed.summary?.totalTransactions || 0} transactions` : `Import failed: ${importParsed.error || "Unknown error"}`;
|
|
24750
|
+
logger?.logStep("Import", importParsed.success ? "success" : "error", message);
|
|
24851
24751
|
context.result.steps.import = buildStepResult(importParsed.success, message, {
|
|
24852
24752
|
success: importParsed.success,
|
|
24853
24753
|
summary: importParsed.summary,
|
|
24854
24754
|
error: importParsed.error
|
|
24855
24755
|
});
|
|
24856
24756
|
if (!importParsed.success) {
|
|
24757
|
+
logger?.error("Import failed", new Error(importParsed.error || "Unknown error"));
|
|
24758
|
+
logger?.endSection();
|
|
24857
24759
|
context.result.error = `Import failed: ${importParsed.error || "Unknown error"}`;
|
|
24858
24760
|
throw new Error("Import failed");
|
|
24859
24761
|
}
|
|
24762
|
+
logger?.endSection();
|
|
24860
24763
|
}
|
|
24861
|
-
async function executeReconcileStep(context,
|
|
24862
|
-
|
|
24863
|
-
|
|
24764
|
+
async function executeReconcileStep(context, logger) {
|
|
24765
|
+
logger?.startSection("Step 5: Reconcile Balance");
|
|
24766
|
+
logger?.logStep("Reconcile", "start");
|
|
24767
|
+
const reconcileResult = await reconcileStatement(context.directory, context.agent, {
|
|
24864
24768
|
provider: context.options.provider,
|
|
24865
24769
|
currency: context.options.currency,
|
|
24866
24770
|
closingBalance: context.options.closingBalance,
|
|
24867
24771
|
account: context.options.account
|
|
24868
|
-
}, context.configLoader, context.hledgerExecutor
|
|
24772
|
+
}, context.configLoader, context.hledgerExecutor);
|
|
24869
24773
|
const reconcileParsed = JSON.parse(reconcileResult);
|
|
24870
24774
|
const message = reconcileParsed.success ? `Balance reconciled: ${reconcileParsed.actualBalance}` : `Balance mismatch: expected ${reconcileParsed.expectedBalance}, got ${reconcileParsed.actualBalance}`;
|
|
24775
|
+
logger?.logStep("Reconcile", reconcileParsed.success ? "success" : "error", message);
|
|
24776
|
+
if (reconcileParsed.success) {
|
|
24777
|
+
logger?.info(`Actual: ${reconcileParsed.actualBalance}`);
|
|
24778
|
+
logger?.info(`Expected: ${reconcileParsed.expectedBalance}`);
|
|
24779
|
+
}
|
|
24871
24780
|
context.result.steps.reconcile = buildStepResult(reconcileParsed.success, message, {
|
|
24872
24781
|
success: reconcileParsed.success,
|
|
24873
24782
|
actualBalance: reconcileParsed.actualBalance,
|
|
@@ -24876,39 +24785,17 @@ async function executeReconcileStep(context, worktree) {
|
|
|
24876
24785
|
error: reconcileParsed.error
|
|
24877
24786
|
});
|
|
24878
24787
|
if (!reconcileParsed.success) {
|
|
24788
|
+
logger?.error("Reconciliation failed", new Error(reconcileParsed.error || "Balance mismatch"));
|
|
24789
|
+
logger?.endSection();
|
|
24879
24790
|
context.result.error = `Reconciliation failed: ${reconcileParsed.error || "Balance mismatch"}`;
|
|
24880
24791
|
context.result.hint = "Check for missing transactions or incorrect rules";
|
|
24881
24792
|
throw new Error("Reconciliation failed");
|
|
24882
24793
|
}
|
|
24883
|
-
|
|
24884
|
-
async function executeMergeStep(context, worktree) {
|
|
24885
|
-
const importDetails = context.result.steps.import?.details;
|
|
24886
|
-
const reconcileDetails = context.result.steps.reconcile?.details;
|
|
24887
|
-
if (!importDetails || !reconcileDetails) {
|
|
24888
|
-
throw new Error("Import or reconcile step not completed before merge");
|
|
24889
|
-
}
|
|
24890
|
-
const commitInfo = {
|
|
24891
|
-
fromDate: reconcileDetails.metadata?.["from-date"],
|
|
24892
|
-
untilDate: reconcileDetails.metadata?.["until-date"]
|
|
24893
|
-
};
|
|
24894
|
-
const transactionCount = importDetails.summary?.totalTransactions || 0;
|
|
24895
|
-
const commitMessage = buildCommitMessage(context.options.provider, context.options.currency, commitInfo.fromDate, commitInfo.untilDate, transactionCount);
|
|
24896
|
-
try {
|
|
24897
|
-
mergeWorktree(worktree, commitMessage);
|
|
24898
|
-
const mergeDetails = { commitMessage };
|
|
24899
|
-
context.result.steps.merge = buildStepResult(true, `Merged to main: "${commitMessage}"`, mergeDetails);
|
|
24900
|
-
cleanupIncomingFiles(worktree, context);
|
|
24901
|
-
} catch (error45) {
|
|
24902
|
-
const message = `Merge failed: ${error45 instanceof Error ? error45.message : String(error45)}`;
|
|
24903
|
-
context.result.steps.merge = buildStepResult(false, message);
|
|
24904
|
-
context.result.error = "Merge to main branch failed";
|
|
24905
|
-
throw new Error("Merge failed");
|
|
24906
|
-
}
|
|
24794
|
+
logger?.endSection();
|
|
24907
24795
|
}
|
|
24908
24796
|
function handleNoTransactions(result) {
|
|
24909
24797
|
result.steps.import = buildStepResult(true, "No transactions to import");
|
|
24910
24798
|
result.steps.reconcile = buildStepResult(true, "Reconciliation skipped (no transactions)");
|
|
24911
|
-
result.steps.merge = buildStepResult(true, "Merge skipped (no changes)");
|
|
24912
24799
|
return buildSuccessResult4(result, "No transactions found to import");
|
|
24913
24800
|
}
|
|
24914
24801
|
async function importPipeline(directory, agent, options, configLoader = loadImportConfig, hledgerExecutor = defaultHledgerExecutor) {
|
|
@@ -24916,6 +24803,12 @@ async function importPipeline(directory, agent, options, configLoader = loadImpo
|
|
|
24916
24803
|
if (restrictionError) {
|
|
24917
24804
|
return restrictionError;
|
|
24918
24805
|
}
|
|
24806
|
+
const logger = createImportLogger(directory, undefined, options.provider);
|
|
24807
|
+
logger.startSection("Import Pipeline", 1);
|
|
24808
|
+
logger.info(`Provider filter: ${options.provider || "all"}`);
|
|
24809
|
+
logger.info(`Currency filter: ${options.currency || "all"}`);
|
|
24810
|
+
logger.info(`Skip classify: ${options.skipClassify || false}`);
|
|
24811
|
+
logger.info("");
|
|
24919
24812
|
const result = {
|
|
24920
24813
|
success: false,
|
|
24921
24814
|
steps: {}
|
|
@@ -24929,113 +24822,61 @@ async function importPipeline(directory, agent, options, configLoader = loadImpo
|
|
|
24929
24822
|
result
|
|
24930
24823
|
};
|
|
24931
24824
|
try {
|
|
24932
|
-
|
|
24933
|
-
|
|
24934
|
-
|
|
24935
|
-
|
|
24936
|
-
|
|
24937
|
-
|
|
24938
|
-
|
|
24939
|
-
|
|
24940
|
-
|
|
24941
|
-
|
|
24942
|
-
|
|
24943
|
-
|
|
24944
|
-
|
|
24945
|
-
|
|
24946
|
-
|
|
24947
|
-
} else {
|
|
24948
|
-
result.steps.sync = buildStepResult(true, `Synced ${syncResult.synced.length} CSV file(s) to worktree`, { synced: syncResult.synced });
|
|
24949
|
-
}
|
|
24950
|
-
} catch (error45) {
|
|
24951
|
-
const errorMsg = error45 instanceof Error ? error45.message : String(error45);
|
|
24952
|
-
result.steps.sync = buildStepResult(false, `Failed to sync CSV files: ${errorMsg}`, { synced: [], errors: [{ file: "unknown", error: errorMsg }] });
|
|
24953
|
-
}
|
|
24954
|
-
try {
|
|
24955
|
-
await executeClassifyStep(context, worktree);
|
|
24956
|
-
await executeAccountDeclarationsStep(context, worktree);
|
|
24957
|
-
await executeDryRunStep(context, worktree);
|
|
24958
|
-
await executeImportStep(context, worktree);
|
|
24959
|
-
await executeReconcileStep(context, worktree);
|
|
24960
|
-
try {
|
|
24961
|
-
const config2 = configLoader(directory);
|
|
24962
|
-
const cleanupResult = cleanupProcessedCSVFiles(directory, config2.paths.import);
|
|
24963
|
-
if (cleanupResult.deleted.length === 0 && cleanupResult.errors.length === 0) {
|
|
24964
|
-
result.steps.cleanup = buildStepResult(true, "No CSV files to cleanup", { csvCleanup: { deleted: [] } });
|
|
24965
|
-
} else if (cleanupResult.errors.length > 0) {
|
|
24966
|
-
result.steps.cleanup = buildStepResult(true, `Deleted ${cleanupResult.deleted.length} CSV file(s) with ${cleanupResult.errors.length} error(s)`, {
|
|
24967
|
-
csvCleanup: {
|
|
24968
|
-
deleted: cleanupResult.deleted,
|
|
24969
|
-
errors: cleanupResult.errors
|
|
24970
|
-
}
|
|
24971
|
-
});
|
|
24972
|
-
} else {
|
|
24973
|
-
result.steps.cleanup = buildStepResult(true, `Deleted ${cleanupResult.deleted.length} CSV file(s) from main repo`, { csvCleanup: { deleted: cleanupResult.deleted } });
|
|
24974
|
-
}
|
|
24975
|
-
} catch (error45) {
|
|
24976
|
-
const errorMsg = error45 instanceof Error ? error45.message : String(error45);
|
|
24977
|
-
result.steps.cleanup = buildStepResult(false, `Failed to cleanup CSV files: ${errorMsg}`, {
|
|
24978
|
-
csvCleanup: {
|
|
24979
|
-
deleted: [],
|
|
24980
|
-
errors: [{ file: "unknown", error: errorMsg }]
|
|
24981
|
-
}
|
|
24982
|
-
});
|
|
24983
|
-
}
|
|
24984
|
-
await executeMergeStep(context, worktree);
|
|
24985
|
-
const existingCleanup = result.steps.cleanup;
|
|
24986
|
-
if (existingCleanup) {
|
|
24987
|
-
existingCleanup.message += ", worktree cleaned up";
|
|
24988
|
-
existingCleanup.details = {
|
|
24989
|
-
...existingCleanup.details,
|
|
24990
|
-
cleanedAfterSuccess: true
|
|
24991
|
-
};
|
|
24992
|
-
}
|
|
24993
|
-
const transactionCount = context.result.steps.import?.details?.summary?.totalTransactions || 0;
|
|
24994
|
-
return buildSuccessResult4(result, `Successfully imported ${transactionCount} transaction(s)`);
|
|
24995
|
-
} catch (error45) {
|
|
24996
|
-
result.steps.cleanup = buildStepResult(true, "Worktree cleaned up after failure (CSV files preserved for retry)", {
|
|
24997
|
-
cleanedAfterFailure: true,
|
|
24998
|
-
csvCleanup: { deleted: [] }
|
|
24999
|
-
});
|
|
25000
|
-
if (error45 instanceof NoTransactionsError) {
|
|
25001
|
-
return handleNoTransactions(result);
|
|
25002
|
-
}
|
|
25003
|
-
if (!result.error) {
|
|
25004
|
-
result.error = error45 instanceof Error ? error45.message : String(error45);
|
|
25005
|
-
}
|
|
25006
|
-
return buildErrorResult5(result, result.error, result.hint);
|
|
25007
|
-
}
|
|
25008
|
-
});
|
|
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)`);
|
|
25009
24840
|
} catch (error45) {
|
|
25010
|
-
|
|
25011
|
-
|
|
25012
|
-
|
|
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);
|
|
24850
|
+
} finally {
|
|
24851
|
+
logger.endSection();
|
|
24852
|
+
await logger.flush();
|
|
25013
24853
|
}
|
|
25014
24854
|
}
|
|
25015
24855
|
var import_pipeline_default = tool({
|
|
25016
|
-
description: `ACCOUNTANT AGENT ONLY: Complete import pipeline with
|
|
24856
|
+
description: `ACCOUNTANT AGENT ONLY: Complete import pipeline with balance reconciliation.
|
|
25017
24857
|
|
|
25018
|
-
This tool orchestrates the full import workflow
|
|
24858
|
+
This tool orchestrates the full import workflow:
|
|
25019
24859
|
|
|
25020
24860
|
**Pipeline Steps:**
|
|
25021
|
-
1. **
|
|
25022
|
-
2. **
|
|
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
|
|
25023
24863
|
3. **Dry Run**: Validates all transactions have known accounts
|
|
25024
|
-
4. **Import**: Imports transactions to the journal
|
|
24864
|
+
4. **Import**: Imports transactions to the journal (moves CSVs to import/done)
|
|
25025
24865
|
5. **Reconcile**: Validates closing balance matches CSV metadata
|
|
25026
|
-
6. **Merge**: Merges worktree to main with --no-ff
|
|
25027
|
-
7. **Cleanup**: Removes worktree
|
|
25028
24866
|
|
|
25029
|
-
**
|
|
25030
|
-
- All changes
|
|
25031
|
-
- If any step fails,
|
|
25032
|
-
-
|
|
25033
|
-
|
|
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
|
|
24871
|
+
|
|
24872
|
+
**Logging:**
|
|
24873
|
+
- All operations logged to .memory/import-<timestamp>.md
|
|
24874
|
+
- Log includes command output, timing, and error details
|
|
25034
24875
|
|
|
25035
24876
|
**Usage:**
|
|
25036
|
-
- Basic: import-pipeline (processes all
|
|
24877
|
+
- Basic: import-pipeline (processes all CSVs in incoming/)
|
|
25037
24878
|
- Filtered: import-pipeline --provider ubs --currency chf
|
|
25038
|
-
-
|
|
24879
|
+
- Manual balance: import-pipeline --closingBalance "CHF 1234.56"
|
|
25039
24880
|
- Skip classify: import-pipeline --skipClassify true`,
|
|
25040
24881
|
args: {
|
|
25041
24882
|
provider: tool.schema.string().optional().describe('Filter by provider (e.g., "ubs", "revolut")'),
|
|
@@ -25056,7 +24897,7 @@ This tool orchestrates the full import workflow in an isolated git worktree:
|
|
|
25056
24897
|
}
|
|
25057
24898
|
});
|
|
25058
24899
|
// src/tools/init-directories.ts
|
|
25059
|
-
import * as
|
|
24900
|
+
import * as fs13 from "fs";
|
|
25060
24901
|
import * as path13 from "path";
|
|
25061
24902
|
async function initDirectories(directory) {
|
|
25062
24903
|
try {
|
|
@@ -25064,8 +24905,8 @@ async function initDirectories(directory) {
|
|
|
25064
24905
|
const directoriesCreated = [];
|
|
25065
24906
|
const gitkeepFiles = [];
|
|
25066
24907
|
const importBase = path13.join(directory, "import");
|
|
25067
|
-
if (!
|
|
25068
|
-
|
|
24908
|
+
if (!fs13.existsSync(importBase)) {
|
|
24909
|
+
fs13.mkdirSync(importBase, { recursive: true });
|
|
25069
24910
|
directoriesCreated.push("import");
|
|
25070
24911
|
}
|
|
25071
24912
|
const pathsToCreate = [
|
|
@@ -25076,19 +24917,19 @@ async function initDirectories(directory) {
|
|
|
25076
24917
|
];
|
|
25077
24918
|
for (const { path: dirPath } of pathsToCreate) {
|
|
25078
24919
|
const fullPath = path13.join(directory, dirPath);
|
|
25079
|
-
if (!
|
|
25080
|
-
|
|
24920
|
+
if (!fs13.existsSync(fullPath)) {
|
|
24921
|
+
fs13.mkdirSync(fullPath, { recursive: true });
|
|
25081
24922
|
directoriesCreated.push(dirPath);
|
|
25082
24923
|
}
|
|
25083
24924
|
const gitkeepPath = path13.join(fullPath, ".gitkeep");
|
|
25084
|
-
if (!
|
|
25085
|
-
|
|
24925
|
+
if (!fs13.existsSync(gitkeepPath)) {
|
|
24926
|
+
fs13.writeFileSync(gitkeepPath, "");
|
|
25086
24927
|
gitkeepFiles.push(path13.join(dirPath, ".gitkeep"));
|
|
25087
24928
|
}
|
|
25088
24929
|
}
|
|
25089
24930
|
const gitignorePath = path13.join(importBase, ".gitignore");
|
|
25090
24931
|
let gitignoreCreated = false;
|
|
25091
|
-
if (!
|
|
24932
|
+
if (!fs13.existsSync(gitignorePath)) {
|
|
25092
24933
|
const gitignoreContent = `# Ignore CSV/PDF files in temporary directories
|
|
25093
24934
|
/incoming/*.csv
|
|
25094
24935
|
/incoming/*.pdf
|
|
@@ -25106,7 +24947,7 @@ async function initDirectories(directory) {
|
|
|
25106
24947
|
.DS_Store
|
|
25107
24948
|
Thumbs.db
|
|
25108
24949
|
`;
|
|
25109
|
-
|
|
24950
|
+
fs13.writeFileSync(gitignorePath, gitignoreContent);
|
|
25110
24951
|
gitignoreCreated = true;
|
|
25111
24952
|
}
|
|
25112
24953
|
const parts = [];
|
|
@@ -25182,8 +25023,8 @@ You can now drop CSV files into import/incoming/ and run import-pipeline.`);
|
|
|
25182
25023
|
}
|
|
25183
25024
|
});
|
|
25184
25025
|
// src/index.ts
|
|
25185
|
-
var __dirname2 =
|
|
25186
|
-
var AGENT_FILE =
|
|
25026
|
+
var __dirname2 = dirname5(fileURLToPath3(import.meta.url));
|
|
25027
|
+
var AGENT_FILE = join12(__dirname2, "..", "agent", "accountant.md");
|
|
25187
25028
|
var AccountantPlugin = async () => {
|
|
25188
25029
|
const agent = loadAgent(AGENT_FILE);
|
|
25189
25030
|
return {
|