@fuzzle/opencode-accountant 0.1.1-next.1 → 0.1.2-next.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +383 -180
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1941,7 +1941,7 @@ var require_convert_csv_to_json = __commonJS((exports) => {
|
|
|
1941
1941
|
});
|
|
1942
1942
|
|
|
1943
1943
|
// src/index.ts
|
|
1944
|
-
import { dirname as dirname6, join as
|
|
1944
|
+
import { dirname as dirname6, join as join13 } from "path";
|
|
1945
1945
|
import { fileURLToPath } from "url";
|
|
1946
1946
|
|
|
1947
1947
|
// src/utils/agentLoader.ts
|
|
@@ -17614,6 +17614,27 @@ function execGitSafe(args, cwd) {
|
|
|
17614
17614
|
}
|
|
17615
17615
|
return { success: true, output: (result.stdout || "").trim() };
|
|
17616
17616
|
}
|
|
17617
|
+
function copyIncomingFiles(mainRepoPath, worktreePath) {
|
|
17618
|
+
const sourceDir = path5.join(mainRepoPath, "import/incoming");
|
|
17619
|
+
const targetDir = path5.join(worktreePath, "import/incoming");
|
|
17620
|
+
if (!fs4.existsSync(sourceDir)) {
|
|
17621
|
+
return;
|
|
17622
|
+
}
|
|
17623
|
+
fs4.mkdirSync(targetDir, { recursive: true });
|
|
17624
|
+
const entries = fs4.readdirSync(sourceDir, { withFileTypes: true });
|
|
17625
|
+
let copiedCount = 0;
|
|
17626
|
+
for (const entry of entries) {
|
|
17627
|
+
if (entry.isFile() && !entry.name.startsWith(".")) {
|
|
17628
|
+
const srcPath = path5.join(sourceDir, entry.name);
|
|
17629
|
+
const destPath = path5.join(targetDir, entry.name);
|
|
17630
|
+
fs4.copyFileSync(srcPath, destPath);
|
|
17631
|
+
copiedCount++;
|
|
17632
|
+
}
|
|
17633
|
+
}
|
|
17634
|
+
if (copiedCount > 0) {
|
|
17635
|
+
console.log(`[INFO] Copied ${copiedCount} file(s) from import/incoming/ to worktree`);
|
|
17636
|
+
}
|
|
17637
|
+
}
|
|
17617
17638
|
function createImportWorktree(mainRepoPath, options = {}) {
|
|
17618
17639
|
const baseDir = options.baseDir ?? "/tmp";
|
|
17619
17640
|
const uuid3 = v4_default();
|
|
@@ -17631,6 +17652,7 @@ function createImportWorktree(mainRepoPath, options = {}) {
|
|
|
17631
17652
|
execGitSafe(["branch", "-D", branch], mainRepoPath);
|
|
17632
17653
|
throw error45;
|
|
17633
17654
|
}
|
|
17655
|
+
copyIncomingFiles(mainRepoPath, worktreePath);
|
|
17634
17656
|
return {
|
|
17635
17657
|
path: worktreePath,
|
|
17636
17658
|
branch,
|
|
@@ -17679,6 +17701,18 @@ function isInWorktree(directory) {
|
|
|
17679
17701
|
return false;
|
|
17680
17702
|
}
|
|
17681
17703
|
}
|
|
17704
|
+
async function withWorktree(directory, operation) {
|
|
17705
|
+
let createdWorktree = null;
|
|
17706
|
+
try {
|
|
17707
|
+
createdWorktree = createImportWorktree(directory);
|
|
17708
|
+
const result = await operation(createdWorktree);
|
|
17709
|
+
return result;
|
|
17710
|
+
} finally {
|
|
17711
|
+
if (createdWorktree) {
|
|
17712
|
+
removeWorktree(createdWorktree, true);
|
|
17713
|
+
}
|
|
17714
|
+
}
|
|
17715
|
+
}
|
|
17682
17716
|
|
|
17683
17717
|
// src/utils/fileUtils.ts
|
|
17684
17718
|
import * as fs5 from "fs";
|
|
@@ -18635,9 +18669,10 @@ function determineClosingBalance(csvFile, config2, options, relativeCsvPath) {
|
|
|
18635
18669
|
}
|
|
18636
18670
|
let closingBalance = options.closingBalance;
|
|
18637
18671
|
if (!closingBalance && metadata?.closing_balance) {
|
|
18638
|
-
|
|
18639
|
-
|
|
18640
|
-
|
|
18672
|
+
const { closing_balance, currency } = metadata;
|
|
18673
|
+
closingBalance = closing_balance;
|
|
18674
|
+
if (currency && !closingBalance.includes(currency)) {
|
|
18675
|
+
closingBalance = `${currency} ${closingBalance}`;
|
|
18641
18676
|
}
|
|
18642
18677
|
}
|
|
18643
18678
|
if (!closingBalance) {
|
|
@@ -18676,7 +18711,7 @@ function determineAccount(csvFile, rulesDir, options, relativeCsvPath, metadata)
|
|
|
18676
18711
|
}
|
|
18677
18712
|
return { account };
|
|
18678
18713
|
}
|
|
18679
|
-
async function
|
|
18714
|
+
async function reconcileStatement(directory, agent, options, configLoader = loadImportConfig, hledgerExecutor = defaultHledgerExecutor, worktreeChecker = isInWorktree) {
|
|
18680
18715
|
const restrictionError = checkAccountantAgent(agent, "reconcile statement");
|
|
18681
18716
|
if (restrictionError) {
|
|
18682
18717
|
return restrictionError;
|
|
@@ -18807,7 +18842,7 @@ It must be run inside an import worktree (use import-pipeline for the full workf
|
|
|
18807
18842
|
},
|
|
18808
18843
|
async execute(params, context) {
|
|
18809
18844
|
const { directory, agent } = context;
|
|
18810
|
-
return
|
|
18845
|
+
return reconcileStatement(directory, agent, {
|
|
18811
18846
|
provider: params.provider,
|
|
18812
18847
|
currency: params.currency,
|
|
18813
18848
|
closingBalance: params.closingBalance,
|
|
@@ -18816,34 +18851,192 @@ It must be run inside an import worktree (use import-pipeline for the full workf
|
|
|
18816
18851
|
}
|
|
18817
18852
|
});
|
|
18818
18853
|
// src/tools/import-pipeline.ts
|
|
18819
|
-
|
|
18820
|
-
|
|
18821
|
-
|
|
18822
|
-
|
|
18823
|
-
|
|
18824
|
-
|
|
18825
|
-
transactionCount: undefined
|
|
18826
|
-
};
|
|
18827
|
-
} catch {
|
|
18828
|
-
return {};
|
|
18854
|
+
import * as fs12 from "fs";
|
|
18855
|
+
import * as path11 from "path";
|
|
18856
|
+
class NoTransactionsError extends Error {
|
|
18857
|
+
constructor() {
|
|
18858
|
+
super("No transactions to import");
|
|
18859
|
+
this.name = "NoTransactionsError";
|
|
18829
18860
|
}
|
|
18830
18861
|
}
|
|
18831
|
-
function
|
|
18832
|
-
|
|
18833
|
-
|
|
18834
|
-
|
|
18835
|
-
}
|
|
18836
|
-
|
|
18862
|
+
function buildStepResult(success2, message, details) {
|
|
18863
|
+
const result = { success: success2, message };
|
|
18864
|
+
if (details !== undefined) {
|
|
18865
|
+
result.details = details;
|
|
18866
|
+
}
|
|
18867
|
+
return result;
|
|
18868
|
+
}
|
|
18869
|
+
function buildSuccessResult5(result, summary) {
|
|
18870
|
+
result.success = true;
|
|
18871
|
+
result.summary = summary;
|
|
18872
|
+
return JSON.stringify(result);
|
|
18873
|
+
}
|
|
18874
|
+
function buildErrorResult5(result, error45, hint) {
|
|
18875
|
+
result.success = false;
|
|
18876
|
+
result.error = error45;
|
|
18877
|
+
if (hint) {
|
|
18878
|
+
result.hint = hint;
|
|
18837
18879
|
}
|
|
18880
|
+
return JSON.stringify(result);
|
|
18838
18881
|
}
|
|
18839
18882
|
function buildCommitMessage(provider, currency, fromDate, untilDate, transactionCount) {
|
|
18840
18883
|
const providerStr = provider?.toUpperCase() || "statements";
|
|
18841
|
-
const currencyStr = currency?.toUpperCase()
|
|
18884
|
+
const currencyStr = currency?.toUpperCase();
|
|
18842
18885
|
const dateRange = fromDate && untilDate ? ` ${fromDate} to ${untilDate}` : "";
|
|
18843
18886
|
const txStr = transactionCount > 0 ? ` (${transactionCount} transactions)` : "";
|
|
18844
|
-
|
|
18887
|
+
const parts = ["Import:", providerStr];
|
|
18888
|
+
if (currencyStr) {
|
|
18889
|
+
parts.push(currencyStr);
|
|
18890
|
+
}
|
|
18891
|
+
return `${parts.join(" ")}${dateRange}${txStr}`;
|
|
18892
|
+
}
|
|
18893
|
+
function cleanupIncomingFiles(worktree, context) {
|
|
18894
|
+
const incomingDir = path11.join(worktree.mainRepoPath, "import/incoming");
|
|
18895
|
+
if (!fs12.existsSync(incomingDir)) {
|
|
18896
|
+
return;
|
|
18897
|
+
}
|
|
18898
|
+
const importStep = context.result.steps.import;
|
|
18899
|
+
if (!importStep?.success || !importStep.details) {
|
|
18900
|
+
return;
|
|
18901
|
+
}
|
|
18902
|
+
const importResult = importStep.details;
|
|
18903
|
+
if (!importResult.files || !Array.isArray(importResult.files)) {
|
|
18904
|
+
return;
|
|
18905
|
+
}
|
|
18906
|
+
let deletedCount = 0;
|
|
18907
|
+
for (const fileResult of importResult.files) {
|
|
18908
|
+
if (!fileResult.csv)
|
|
18909
|
+
continue;
|
|
18910
|
+
const filename = path11.basename(fileResult.csv);
|
|
18911
|
+
const filePath = path11.join(incomingDir, filename);
|
|
18912
|
+
if (fs12.existsSync(filePath)) {
|
|
18913
|
+
try {
|
|
18914
|
+
fs12.unlinkSync(filePath);
|
|
18915
|
+
deletedCount++;
|
|
18916
|
+
} catch (error45) {
|
|
18917
|
+
console.error(`[ERROR] Failed to delete ${filename}: ${error45 instanceof Error ? error45.message : String(error45)}`);
|
|
18918
|
+
}
|
|
18919
|
+
}
|
|
18920
|
+
}
|
|
18921
|
+
if (deletedCount > 0) {
|
|
18922
|
+
console.log(`[INFO] Cleaned up ${deletedCount} file(s) from import/incoming/`);
|
|
18923
|
+
}
|
|
18924
|
+
}
|
|
18925
|
+
async function executeClassifyStep(context, worktree) {
|
|
18926
|
+
if (context.options.skipClassify) {
|
|
18927
|
+
context.result.steps.classify = buildStepResult(true, "Classification skipped (skipClassify: true)");
|
|
18928
|
+
return;
|
|
18929
|
+
}
|
|
18930
|
+
const inWorktree = () => true;
|
|
18931
|
+
const classifyResult = await classifyStatements(worktree.path, context.agent, context.configLoader, inWorktree);
|
|
18932
|
+
const classifyParsed = JSON.parse(classifyResult);
|
|
18933
|
+
const success2 = classifyParsed.success !== false;
|
|
18934
|
+
let message = success2 ? "Classification complete" : "Classification had issues";
|
|
18935
|
+
if (classifyParsed.unrecognized?.length > 0) {
|
|
18936
|
+
message = `Classification complete with ${classifyParsed.unrecognized.length} unrecognized file(s)`;
|
|
18937
|
+
}
|
|
18938
|
+
const details = {
|
|
18939
|
+
success: success2,
|
|
18940
|
+
unrecognized: classifyParsed.unrecognized,
|
|
18941
|
+
classified: classifyParsed
|
|
18942
|
+
};
|
|
18943
|
+
context.result.steps.classify = buildStepResult(success2, message, details);
|
|
18944
|
+
}
|
|
18945
|
+
async function executeDryRunStep(context, worktree) {
|
|
18946
|
+
const inWorktree = () => true;
|
|
18947
|
+
const dryRunResult = await importStatements(worktree.path, context.agent, {
|
|
18948
|
+
provider: context.options.provider,
|
|
18949
|
+
currency: context.options.currency,
|
|
18950
|
+
checkOnly: true
|
|
18951
|
+
}, context.configLoader, context.hledgerExecutor, inWorktree);
|
|
18952
|
+
const dryRunParsed = JSON.parse(dryRunResult);
|
|
18953
|
+
const message = dryRunParsed.success ? `Dry run passed: ${dryRunParsed.summary?.totalTransactions || 0} transactions ready` : `Dry run failed: ${dryRunParsed.summary?.unknown || 0} unknown account(s)`;
|
|
18954
|
+
context.result.steps.dryRun = buildStepResult(dryRunParsed.success, message, {
|
|
18955
|
+
success: dryRunParsed.success,
|
|
18956
|
+
summary: dryRunParsed.summary
|
|
18957
|
+
});
|
|
18958
|
+
if (!dryRunParsed.success) {
|
|
18959
|
+
context.result.error = "Dry run found unknown accounts or errors";
|
|
18960
|
+
context.result.hint = "Add rules to categorize unknown transactions, then retry";
|
|
18961
|
+
throw new Error("Dry run failed");
|
|
18962
|
+
}
|
|
18963
|
+
if (dryRunParsed.summary?.totalTransactions === 0) {
|
|
18964
|
+
throw new NoTransactionsError;
|
|
18965
|
+
}
|
|
18966
|
+
}
|
|
18967
|
+
async function executeImportStep(context, worktree) {
|
|
18968
|
+
const inWorktree = () => true;
|
|
18969
|
+
const importResult = await importStatements(worktree.path, context.agent, {
|
|
18970
|
+
provider: context.options.provider,
|
|
18971
|
+
currency: context.options.currency,
|
|
18972
|
+
checkOnly: false
|
|
18973
|
+
}, context.configLoader, context.hledgerExecutor, inWorktree);
|
|
18974
|
+
const importParsed = JSON.parse(importResult);
|
|
18975
|
+
const message = importParsed.success ? `Imported ${importParsed.summary?.totalTransactions || 0} transactions` : `Import failed: ${importParsed.error || "Unknown error"}`;
|
|
18976
|
+
context.result.steps.import = buildStepResult(importParsed.success, message, {
|
|
18977
|
+
success: importParsed.success,
|
|
18978
|
+
summary: importParsed.summary,
|
|
18979
|
+
error: importParsed.error
|
|
18980
|
+
});
|
|
18981
|
+
if (!importParsed.success) {
|
|
18982
|
+
context.result.error = `Import failed: ${importParsed.error || "Unknown error"}`;
|
|
18983
|
+
throw new Error("Import failed");
|
|
18984
|
+
}
|
|
18985
|
+
}
|
|
18986
|
+
async function executeReconcileStep(context, worktree) {
|
|
18987
|
+
const inWorktree = () => true;
|
|
18988
|
+
const reconcileResult = await reconcileStatement(worktree.path, context.agent, {
|
|
18989
|
+
provider: context.options.provider,
|
|
18990
|
+
currency: context.options.currency,
|
|
18991
|
+
closingBalance: context.options.closingBalance,
|
|
18992
|
+
account: context.options.account
|
|
18993
|
+
}, context.configLoader, context.hledgerExecutor, inWorktree);
|
|
18994
|
+
const reconcileParsed = JSON.parse(reconcileResult);
|
|
18995
|
+
const message = reconcileParsed.success ? `Balance reconciled: ${reconcileParsed.actualBalance}` : `Balance mismatch: expected ${reconcileParsed.expectedBalance}, got ${reconcileParsed.actualBalance}`;
|
|
18996
|
+
context.result.steps.reconcile = buildStepResult(reconcileParsed.success, message, {
|
|
18997
|
+
success: reconcileParsed.success,
|
|
18998
|
+
actualBalance: reconcileParsed.actualBalance,
|
|
18999
|
+
expectedBalance: reconcileParsed.expectedBalance,
|
|
19000
|
+
metadata: reconcileParsed.metadata,
|
|
19001
|
+
error: reconcileParsed.error
|
|
19002
|
+
});
|
|
19003
|
+
if (!reconcileParsed.success) {
|
|
19004
|
+
context.result.error = `Reconciliation failed: ${reconcileParsed.error || "Balance mismatch"}`;
|
|
19005
|
+
context.result.hint = "Check for missing transactions or incorrect rules";
|
|
19006
|
+
throw new Error("Reconciliation failed");
|
|
19007
|
+
}
|
|
18845
19008
|
}
|
|
18846
|
-
async function
|
|
19009
|
+
async function executeMergeStep(context, worktree) {
|
|
19010
|
+
const importDetails = context.result.steps.import?.details;
|
|
19011
|
+
const reconcileDetails = context.result.steps.reconcile?.details;
|
|
19012
|
+
if (!importDetails || !reconcileDetails) {
|
|
19013
|
+
throw new Error("Import or reconcile step not completed before merge");
|
|
19014
|
+
}
|
|
19015
|
+
const commitInfo = {
|
|
19016
|
+
fromDate: reconcileDetails.metadata?.from_date,
|
|
19017
|
+
untilDate: reconcileDetails.metadata?.until_date
|
|
19018
|
+
};
|
|
19019
|
+
const transactionCount = importDetails.summary?.totalTransactions || 0;
|
|
19020
|
+
const commitMessage = buildCommitMessage(context.options.provider, context.options.currency, commitInfo.fromDate, commitInfo.untilDate, transactionCount);
|
|
19021
|
+
try {
|
|
19022
|
+
mergeWorktree(worktree, commitMessage);
|
|
19023
|
+
const mergeDetails = { commitMessage };
|
|
19024
|
+
context.result.steps.merge = buildStepResult(true, `Merged to main: "${commitMessage}"`, mergeDetails);
|
|
19025
|
+
cleanupIncomingFiles(worktree, context);
|
|
19026
|
+
} catch (error45) {
|
|
19027
|
+
const message = `Merge failed: ${error45 instanceof Error ? error45.message : String(error45)}`;
|
|
19028
|
+
context.result.steps.merge = buildStepResult(false, message);
|
|
19029
|
+
context.result.error = "Merge to main branch failed";
|
|
19030
|
+
throw new Error("Merge failed");
|
|
19031
|
+
}
|
|
19032
|
+
}
|
|
19033
|
+
function handleNoTransactions(result) {
|
|
19034
|
+
result.steps.import = buildStepResult(true, "No transactions to import");
|
|
19035
|
+
result.steps.reconcile = buildStepResult(true, "Reconciliation skipped (no transactions)");
|
|
19036
|
+
result.steps.merge = buildStepResult(true, "Merge skipped (no changes)");
|
|
19037
|
+
return buildSuccessResult5(result, "No transactions found to import");
|
|
19038
|
+
}
|
|
19039
|
+
async function importPipeline(directory, agent, options, configLoader = loadImportConfig, hledgerExecutor = defaultHledgerExecutor) {
|
|
18847
19040
|
const restrictionError = checkAccountantAgent(agent, "import pipeline");
|
|
18848
19041
|
if (restrictionError) {
|
|
18849
19042
|
return restrictionError;
|
|
@@ -18852,163 +19045,47 @@ async function importPipelineCore(directory, agent, options, configLoader = load
|
|
|
18852
19045
|
success: false,
|
|
18853
19046
|
steps: {}
|
|
18854
19047
|
};
|
|
18855
|
-
|
|
19048
|
+
const context = {
|
|
19049
|
+
directory,
|
|
19050
|
+
agent,
|
|
19051
|
+
options,
|
|
19052
|
+
configLoader,
|
|
19053
|
+
hledgerExecutor,
|
|
19054
|
+
result
|
|
19055
|
+
};
|
|
18856
19056
|
try {
|
|
18857
|
-
|
|
18858
|
-
worktree = createImportWorktree(directory);
|
|
19057
|
+
return await withWorktree(directory, async (worktree) => {
|
|
18859
19058
|
result.worktreeId = worktree.uuid;
|
|
18860
|
-
result.steps.worktree = {
|
|
18861
|
-
|
|
18862
|
-
|
|
18863
|
-
|
|
18864
|
-
};
|
|
18865
|
-
} catch (error45) {
|
|
18866
|
-
result.steps.worktree = {
|
|
18867
|
-
success: false,
|
|
18868
|
-
message: `Failed to create worktree: ${error45 instanceof Error ? error45.message : String(error45)}`
|
|
18869
|
-
};
|
|
18870
|
-
throw new Error("Failed to create worktree");
|
|
18871
|
-
}
|
|
18872
|
-
const inWorktree = () => true;
|
|
18873
|
-
if (!options.skipClassify) {
|
|
18874
|
-
const classifyResult = await classifyStatements(worktree.path, agent, configLoader, inWorktree);
|
|
18875
|
-
const classifyParsed = JSON.parse(classifyResult);
|
|
18876
|
-
result.steps.classify = {
|
|
18877
|
-
success: classifyParsed.success !== false,
|
|
18878
|
-
message: classifyParsed.success !== false ? "Classification complete" : "Classification had issues",
|
|
18879
|
-
details: classifyParsed
|
|
18880
|
-
};
|
|
18881
|
-
if (classifyParsed.unrecognized?.length > 0) {
|
|
18882
|
-
result.steps.classify.message = `Classification complete with ${classifyParsed.unrecognized.length} unrecognized file(s)`;
|
|
18883
|
-
}
|
|
18884
|
-
} else {
|
|
18885
|
-
result.steps.classify = {
|
|
18886
|
-
success: true,
|
|
18887
|
-
message: "Classification skipped (skipClassify: true)"
|
|
18888
|
-
};
|
|
18889
|
-
}
|
|
18890
|
-
const dryRunResult = await importStatements(worktree.path, agent, {
|
|
18891
|
-
provider: options.provider,
|
|
18892
|
-
currency: options.currency,
|
|
18893
|
-
checkOnly: true
|
|
18894
|
-
}, configLoader, hledgerExecutor, inWorktree);
|
|
18895
|
-
const dryRunParsed = JSON.parse(dryRunResult);
|
|
18896
|
-
result.steps.dryRun = {
|
|
18897
|
-
success: dryRunParsed.success,
|
|
18898
|
-
message: dryRunParsed.success ? `Dry run passed: ${dryRunParsed.summary?.totalTransactions || 0} transactions ready` : `Dry run failed: ${dryRunParsed.summary?.unknown || 0} unknown account(s)`,
|
|
18899
|
-
details: dryRunParsed
|
|
18900
|
-
};
|
|
18901
|
-
if (!dryRunParsed.success) {
|
|
18902
|
-
result.error = "Dry run found unknown accounts or errors";
|
|
18903
|
-
result.hint = "Add rules to categorize unknown transactions, then retry";
|
|
18904
|
-
throw new Error("Dry run failed");
|
|
18905
|
-
}
|
|
18906
|
-
if (dryRunParsed.summary?.totalTransactions === 0) {
|
|
18907
|
-
result.steps.import = {
|
|
18908
|
-
success: true,
|
|
18909
|
-
message: "No transactions to import"
|
|
18910
|
-
};
|
|
18911
|
-
result.steps.reconcile = {
|
|
18912
|
-
success: true,
|
|
18913
|
-
message: "Reconciliation skipped (no transactions)"
|
|
18914
|
-
};
|
|
18915
|
-
result.steps.merge = {
|
|
18916
|
-
success: true,
|
|
18917
|
-
message: "Merge skipped (no changes)"
|
|
18918
|
-
};
|
|
18919
|
-
result.success = true;
|
|
18920
|
-
result.summary = "No transactions found to import";
|
|
18921
|
-
removeWorktree(worktree, true);
|
|
18922
|
-
result.steps.cleanup = {
|
|
18923
|
-
success: true,
|
|
18924
|
-
message: "Worktree cleaned up"
|
|
18925
|
-
};
|
|
18926
|
-
return JSON.stringify(result);
|
|
18927
|
-
}
|
|
18928
|
-
const importResult = await importStatements(worktree.path, agent, {
|
|
18929
|
-
provider: options.provider,
|
|
18930
|
-
currency: options.currency,
|
|
18931
|
-
checkOnly: false
|
|
18932
|
-
}, configLoader, hledgerExecutor, inWorktree);
|
|
18933
|
-
const importParsed = JSON.parse(importResult);
|
|
18934
|
-
result.steps.import = {
|
|
18935
|
-
success: importParsed.success,
|
|
18936
|
-
message: importParsed.success ? `Imported ${importParsed.summary?.totalTransactions || 0} transactions` : `Import failed: ${importParsed.error || "Unknown error"}`,
|
|
18937
|
-
details: importParsed
|
|
18938
|
-
};
|
|
18939
|
-
if (!importParsed.success) {
|
|
18940
|
-
result.error = `Import failed: ${importParsed.error || "Unknown error"}`;
|
|
18941
|
-
throw new Error("Import failed");
|
|
18942
|
-
}
|
|
18943
|
-
const reconcileResult = await reconcileStatementCore(worktree.path, agent, {
|
|
18944
|
-
provider: options.provider,
|
|
18945
|
-
currency: options.currency,
|
|
18946
|
-
closingBalance: options.closingBalance,
|
|
18947
|
-
account: options.account
|
|
18948
|
-
}, configLoader, hledgerExecutor, inWorktree);
|
|
18949
|
-
const reconcileParsed = JSON.parse(reconcileResult);
|
|
18950
|
-
result.steps.reconcile = {
|
|
18951
|
-
success: reconcileParsed.success,
|
|
18952
|
-
message: reconcileParsed.success ? `Balance reconciled: ${reconcileParsed.actualBalance}` : `Balance mismatch: expected ${reconcileParsed.expectedBalance}, got ${reconcileParsed.actualBalance}`,
|
|
18953
|
-
details: reconcileParsed
|
|
18954
|
-
};
|
|
18955
|
-
if (!reconcileParsed.success) {
|
|
18956
|
-
result.error = `Reconciliation failed: ${reconcileParsed.error || "Balance mismatch"}`;
|
|
18957
|
-
result.hint = "Check for missing transactions or incorrect rules";
|
|
18958
|
-
throw new Error("Reconciliation failed");
|
|
18959
|
-
}
|
|
18960
|
-
const commitInfo = extractCommitInfo(reconcileResult);
|
|
18961
|
-
const transactionCount = extractTransactionCount(importResult);
|
|
18962
|
-
const commitMessage = buildCommitMessage(options.provider, options.currency, commitInfo.fromDate, commitInfo.untilDate, transactionCount);
|
|
18963
|
-
try {
|
|
18964
|
-
mergeWorktree(worktree, commitMessage);
|
|
18965
|
-
result.steps.merge = {
|
|
18966
|
-
success: true,
|
|
18967
|
-
message: `Merged to main: "${commitMessage}"`,
|
|
18968
|
-
details: { commitMessage }
|
|
18969
|
-
};
|
|
18970
|
-
} catch (error45) {
|
|
18971
|
-
result.steps.merge = {
|
|
18972
|
-
success: false,
|
|
18973
|
-
message: `Merge failed: ${error45 instanceof Error ? error45.message : String(error45)}`
|
|
18974
|
-
};
|
|
18975
|
-
result.error = "Merge to main branch failed";
|
|
18976
|
-
throw new Error("Merge failed");
|
|
18977
|
-
}
|
|
18978
|
-
try {
|
|
18979
|
-
removeWorktree(worktree, true);
|
|
18980
|
-
result.steps.cleanup = {
|
|
18981
|
-
success: true,
|
|
18982
|
-
message: "Worktree cleaned up"
|
|
18983
|
-
};
|
|
18984
|
-
} catch (error45) {
|
|
18985
|
-
result.steps.cleanup = {
|
|
18986
|
-
success: false,
|
|
18987
|
-
message: `Cleanup warning: ${error45 instanceof Error ? error45.message : String(error45)}`
|
|
18988
|
-
};
|
|
18989
|
-
}
|
|
18990
|
-
result.success = true;
|
|
18991
|
-
result.summary = `Successfully imported ${transactionCount} transaction(s)`;
|
|
18992
|
-
return JSON.stringify(result);
|
|
18993
|
-
} catch (error45) {
|
|
18994
|
-
if (worktree) {
|
|
19059
|
+
result.steps.worktree = buildStepResult(true, `Created worktree at ${worktree.path}`, {
|
|
19060
|
+
path: worktree.path,
|
|
19061
|
+
branch: worktree.branch
|
|
19062
|
+
});
|
|
18995
19063
|
try {
|
|
18996
|
-
|
|
18997
|
-
|
|
18998
|
-
|
|
18999
|
-
|
|
19000
|
-
|
|
19001
|
-
|
|
19002
|
-
|
|
19003
|
-
|
|
19004
|
-
|
|
19005
|
-
};
|
|
19064
|
+
await executeClassifyStep(context, worktree);
|
|
19065
|
+
await executeDryRunStep(context, worktree);
|
|
19066
|
+
await executeImportStep(context, worktree);
|
|
19067
|
+
await executeReconcileStep(context, worktree);
|
|
19068
|
+
await executeMergeStep(context, worktree);
|
|
19069
|
+
result.steps.cleanup = buildStepResult(true, "Worktree cleaned up", {
|
|
19070
|
+
cleanedAfterSuccess: true
|
|
19071
|
+
});
|
|
19072
|
+
const transactionCount = context.result.steps.import?.details?.summary?.totalTransactions || 0;
|
|
19073
|
+
return buildSuccessResult5(result, `Successfully imported ${transactionCount} transaction(s)`);
|
|
19074
|
+
} catch (error45) {
|
|
19075
|
+
result.steps.cleanup = buildStepResult(true, "Worktree cleaned up after failure", { cleanedAfterFailure: true });
|
|
19076
|
+
if (error45 instanceof NoTransactionsError) {
|
|
19077
|
+
return handleNoTransactions(result);
|
|
19078
|
+
}
|
|
19079
|
+
if (!result.error) {
|
|
19080
|
+
result.error = error45 instanceof Error ? error45.message : String(error45);
|
|
19081
|
+
}
|
|
19082
|
+
return buildErrorResult5(result, result.error, result.hint);
|
|
19006
19083
|
}
|
|
19007
|
-
}
|
|
19008
|
-
|
|
19009
|
-
|
|
19010
|
-
|
|
19011
|
-
return
|
|
19084
|
+
});
|
|
19085
|
+
} catch (error45) {
|
|
19086
|
+
result.steps.worktree = buildStepResult(false, `Failed to create worktree: ${error45 instanceof Error ? error45.message : String(error45)}`);
|
|
19087
|
+
result.error = "Failed to create worktree";
|
|
19088
|
+
return buildErrorResult5(result, result.error);
|
|
19012
19089
|
}
|
|
19013
19090
|
}
|
|
19014
19091
|
var import_pipeline_default = tool({
|
|
@@ -19045,7 +19122,7 @@ This tool orchestrates the full import workflow in an isolated git worktree:
|
|
|
19045
19122
|
},
|
|
19046
19123
|
async execute(params, context) {
|
|
19047
19124
|
const { directory, agent } = context;
|
|
19048
|
-
return
|
|
19125
|
+
return importPipeline(directory, agent, {
|
|
19049
19126
|
provider: params.provider,
|
|
19050
19127
|
currency: params.currency,
|
|
19051
19128
|
closingBalance: params.closingBalance,
|
|
@@ -19054,9 +19131,135 @@ This tool orchestrates the full import workflow in an isolated git worktree:
|
|
|
19054
19131
|
});
|
|
19055
19132
|
}
|
|
19056
19133
|
});
|
|
19134
|
+
// src/tools/init-directories.ts
|
|
19135
|
+
import * as fs13 from "fs";
|
|
19136
|
+
import * as path12 from "path";
|
|
19137
|
+
async function initDirectories(directory) {
|
|
19138
|
+
try {
|
|
19139
|
+
const config2 = loadImportConfig(directory);
|
|
19140
|
+
const directoriesCreated = [];
|
|
19141
|
+
const gitkeepFiles = [];
|
|
19142
|
+
const importBase = path12.join(directory, "import");
|
|
19143
|
+
if (!fs13.existsSync(importBase)) {
|
|
19144
|
+
fs13.mkdirSync(importBase, { recursive: true });
|
|
19145
|
+
directoriesCreated.push("import");
|
|
19146
|
+
}
|
|
19147
|
+
const pathsToCreate = [
|
|
19148
|
+
{ key: "import", path: config2.paths.import },
|
|
19149
|
+
{ key: "pending", path: config2.paths.pending },
|
|
19150
|
+
{ key: "done", path: config2.paths.done },
|
|
19151
|
+
{ key: "unrecognized", path: config2.paths.unrecognized }
|
|
19152
|
+
];
|
|
19153
|
+
for (const { path: dirPath } of pathsToCreate) {
|
|
19154
|
+
const fullPath = path12.join(directory, dirPath);
|
|
19155
|
+
if (!fs13.existsSync(fullPath)) {
|
|
19156
|
+
fs13.mkdirSync(fullPath, { recursive: true });
|
|
19157
|
+
directoriesCreated.push(dirPath);
|
|
19158
|
+
}
|
|
19159
|
+
const gitkeepPath = path12.join(fullPath, ".gitkeep");
|
|
19160
|
+
if (!fs13.existsSync(gitkeepPath)) {
|
|
19161
|
+
fs13.writeFileSync(gitkeepPath, "");
|
|
19162
|
+
gitkeepFiles.push(path12.join(dirPath, ".gitkeep"));
|
|
19163
|
+
}
|
|
19164
|
+
}
|
|
19165
|
+
const gitignorePath = path12.join(importBase, ".gitignore");
|
|
19166
|
+
let gitignoreCreated = false;
|
|
19167
|
+
if (!fs13.existsSync(gitignorePath)) {
|
|
19168
|
+
const gitignoreContent = `# Ignore CSV/PDF files in temporary directories
|
|
19169
|
+
/incoming/*.csv
|
|
19170
|
+
/incoming/*.pdf
|
|
19171
|
+
/pending/**/*.csv
|
|
19172
|
+
/pending/**/*.pdf
|
|
19173
|
+
/unrecognized/**/*.csv
|
|
19174
|
+
/unrecognized/**/*.pdf
|
|
19175
|
+
|
|
19176
|
+
# Track processed files in done/ (audit trail)
|
|
19177
|
+
# No ignore rule needed - tracked by default
|
|
19178
|
+
|
|
19179
|
+
# Ignore temporary files
|
|
19180
|
+
*.tmp
|
|
19181
|
+
*.temp
|
|
19182
|
+
.DS_Store
|
|
19183
|
+
Thumbs.db
|
|
19184
|
+
`;
|
|
19185
|
+
fs13.writeFileSync(gitignorePath, gitignoreContent);
|
|
19186
|
+
gitignoreCreated = true;
|
|
19187
|
+
}
|
|
19188
|
+
const parts = [];
|
|
19189
|
+
if (directoriesCreated.length > 0) {
|
|
19190
|
+
parts.push(`Created ${directoriesCreated.length} director${directoriesCreated.length === 1 ? "y" : "ies"}`);
|
|
19191
|
+
}
|
|
19192
|
+
if (gitkeepFiles.length > 0) {
|
|
19193
|
+
parts.push(`added ${gitkeepFiles.length} .gitkeep file${gitkeepFiles.length === 1 ? "" : "s"}`);
|
|
19194
|
+
}
|
|
19195
|
+
if (gitignoreCreated) {
|
|
19196
|
+
parts.push("created .gitignore");
|
|
19197
|
+
}
|
|
19198
|
+
const message = parts.length > 0 ? `Import directory structure initialized: ${parts.join(", ")}` : "Import directory structure already exists (no changes needed)";
|
|
19199
|
+
return {
|
|
19200
|
+
success: true,
|
|
19201
|
+
directoriesCreated,
|
|
19202
|
+
gitkeepFiles,
|
|
19203
|
+
gitignoreCreated,
|
|
19204
|
+
message
|
|
19205
|
+
};
|
|
19206
|
+
} catch (error45) {
|
|
19207
|
+
return {
|
|
19208
|
+
success: false,
|
|
19209
|
+
directoriesCreated: [],
|
|
19210
|
+
gitkeepFiles: [],
|
|
19211
|
+
gitignoreCreated: false,
|
|
19212
|
+
error: error45 instanceof Error ? error45.message : String(error45),
|
|
19213
|
+
message: "Failed to initialize import directory structure"
|
|
19214
|
+
};
|
|
19215
|
+
}
|
|
19216
|
+
}
|
|
19217
|
+
var init_directories_default = tool({
|
|
19218
|
+
description: "ACCOUNTANT AGENT ONLY: Initialize the import directory structure needed for processing bank statements. Creates import/incoming, import/pending, import/done, and import/unrecognized directories with .gitkeep files and appropriate .gitignore rules. Reads directory paths from config/import/providers.yaml. Safe to run multiple times (idempotent).",
|
|
19219
|
+
args: {},
|
|
19220
|
+
async execute(_params, context) {
|
|
19221
|
+
const restrictionError = checkAccountantAgent(context.agent, "init directories");
|
|
19222
|
+
if (restrictionError) {
|
|
19223
|
+
throw new Error(restrictionError);
|
|
19224
|
+
}
|
|
19225
|
+
const { directory } = context;
|
|
19226
|
+
const result = await initDirectories(directory);
|
|
19227
|
+
if (!result.success) {
|
|
19228
|
+
return `Error: ${result.error}
|
|
19229
|
+
|
|
19230
|
+
${result.message}`;
|
|
19231
|
+
}
|
|
19232
|
+
const output = [];
|
|
19233
|
+
output.push(result.message || "");
|
|
19234
|
+
if (result.directoriesCreated.length > 0) {
|
|
19235
|
+
output.push(`
|
|
19236
|
+
Directories created:`);
|
|
19237
|
+
for (const dir of result.directoriesCreated) {
|
|
19238
|
+
output.push(` - ${dir}`);
|
|
19239
|
+
}
|
|
19240
|
+
}
|
|
19241
|
+
if (result.gitkeepFiles.length > 0) {
|
|
19242
|
+
output.push(`
|
|
19243
|
+
.gitkeep files added:`);
|
|
19244
|
+
for (const file2 of result.gitkeepFiles) {
|
|
19245
|
+
output.push(` - ${file2}`);
|
|
19246
|
+
}
|
|
19247
|
+
}
|
|
19248
|
+
if (result.gitignoreCreated) {
|
|
19249
|
+
output.push(`
|
|
19250
|
+
Created import/.gitignore with rules to:`);
|
|
19251
|
+
output.push(" - Ignore CSV/PDF files in incoming/, pending/, unrecognized/");
|
|
19252
|
+
output.push(" - Track processed files in done/ for audit trail");
|
|
19253
|
+
}
|
|
19254
|
+
output.push(`
|
|
19255
|
+
You can now drop CSV files into import/incoming/ and run import-pipeline.`);
|
|
19256
|
+
return output.join(`
|
|
19257
|
+
`);
|
|
19258
|
+
}
|
|
19259
|
+
});
|
|
19057
19260
|
// src/index.ts
|
|
19058
19261
|
var __dirname2 = dirname6(fileURLToPath(import.meta.url));
|
|
19059
|
-
var AGENT_FILE =
|
|
19262
|
+
var AGENT_FILE = join13(__dirname2, "..", "agent", "accountant.md");
|
|
19060
19263
|
var AccountantPlugin = async () => {
|
|
19061
19264
|
const agent = loadAgent(AGENT_FILE);
|
|
19062
19265
|
return {
|
package/package.json
CHANGED