@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.
Files changed (2) hide show
  1. package/dist/index.js +383 -180
  2. 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 join11 } from "path";
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
- closingBalance = metadata.closing_balance;
18639
- if (metadata.currency && !closingBalance.includes(metadata.currency)) {
18640
- closingBalance = `${metadata.currency} ${closingBalance}`;
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 reconcileStatementCore(directory, agent, options, configLoader = loadImportConfig, hledgerExecutor = defaultHledgerExecutor, worktreeChecker = isInWorktree) {
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 reconcileStatementCore(directory, agent, {
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
- function extractCommitInfo(reconcileResult) {
18820
- try {
18821
- const parsed = JSON.parse(reconcileResult);
18822
- return {
18823
- fromDate: parsed.metadata?.from_date,
18824
- untilDate: parsed.metadata?.until_date,
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 extractTransactionCount(importResult) {
18832
- try {
18833
- const parsed = JSON.parse(importResult);
18834
- return parsed.summary?.totalTransactions || 0;
18835
- } catch {
18836
- return 0;
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
- return `Import: ${providerStr} ${currencyStr}${dateRange}${txStr}`.trim();
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 importPipelineCore(directory, agent, options, configLoader = loadImportConfig, hledgerExecutor = defaultHledgerExecutor) {
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
- let worktree = null;
19048
+ const context = {
19049
+ directory,
19050
+ agent,
19051
+ options,
19052
+ configLoader,
19053
+ hledgerExecutor,
19054
+ result
19055
+ };
18856
19056
  try {
18857
- try {
18858
- worktree = createImportWorktree(directory);
19057
+ return await withWorktree(directory, async (worktree) => {
18859
19058
  result.worktreeId = worktree.uuid;
18860
- result.steps.worktree = {
18861
- success: true,
18862
- message: `Created worktree at ${worktree.path}`,
18863
- details: { path: worktree.path, branch: worktree.branch }
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
- removeWorktree(worktree, true);
18997
- result.steps.cleanup = {
18998
- success: true,
18999
- message: "Worktree cleaned up after failure"
19000
- };
19001
- } catch (cleanupError) {
19002
- result.steps.cleanup = {
19003
- success: false,
19004
- message: `Cleanup failed: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`
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
- if (!result.error) {
19009
- result.error = error45 instanceof Error ? error45.message : String(error45);
19010
- }
19011
- return JSON.stringify(result);
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 importPipelineCore(directory, agent, {
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 = join11(__dirname2, "..", "agent", "accountant.md");
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fuzzle/opencode-accountant",
3
- "version": "0.1.1-next.1",
3
+ "version": "0.1.2-next.1",
4
4
  "description": "An OpenCode accounting agent, specialized in double-entry-bookkepping with hledger",
5
5
  "author": {
6
6
  "name": "ali bengali",