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