@fuzzle/opencode-accountant 0.1.1-next.1 → 0.1.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.
Files changed (2) hide show
  1. package/dist/index.js +198 -178
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -17679,6 +17679,18 @@ function isInWorktree(directory) {
17679
17679
  return false;
17680
17680
  }
17681
17681
  }
17682
+ async function withWorktree(directory, operation) {
17683
+ let createdWorktree = null;
17684
+ try {
17685
+ createdWorktree = createImportWorktree(directory);
17686
+ const result = await operation(createdWorktree);
17687
+ return result;
17688
+ } finally {
17689
+ if (createdWorktree) {
17690
+ removeWorktree(createdWorktree, true);
17691
+ }
17692
+ }
17693
+ }
17682
17694
 
17683
17695
  // src/utils/fileUtils.ts
17684
17696
  import * as fs5 from "fs";
@@ -18635,9 +18647,10 @@ function determineClosingBalance(csvFile, config2, options, relativeCsvPath) {
18635
18647
  }
18636
18648
  let closingBalance = options.closingBalance;
18637
18649
  if (!closingBalance && metadata?.closing_balance) {
18638
- closingBalance = metadata.closing_balance;
18639
- if (metadata.currency && !closingBalance.includes(metadata.currency)) {
18640
- closingBalance = `${metadata.currency} ${closingBalance}`;
18650
+ const { closing_balance, currency } = metadata;
18651
+ closingBalance = closing_balance;
18652
+ if (currency && !closingBalance.includes(currency)) {
18653
+ closingBalance = `${currency} ${closingBalance}`;
18641
18654
  }
18642
18655
  }
18643
18656
  if (!closingBalance) {
@@ -18676,7 +18689,7 @@ function determineAccount(csvFile, rulesDir, options, relativeCsvPath, metadata)
18676
18689
  }
18677
18690
  return { account };
18678
18691
  }
18679
- async function reconcileStatementCore(directory, agent, options, configLoader = loadImportConfig, hledgerExecutor = defaultHledgerExecutor, worktreeChecker = isInWorktree) {
18692
+ async function reconcileStatement(directory, agent, options, configLoader = loadImportConfig, hledgerExecutor = defaultHledgerExecutor, worktreeChecker = isInWorktree) {
18680
18693
  const restrictionError = checkAccountantAgent(agent, "reconcile statement");
18681
18694
  if (restrictionError) {
18682
18695
  return restrictionError;
@@ -18807,7 +18820,7 @@ It must be run inside an import worktree (use import-pipeline for the full workf
18807
18820
  },
18808
18821
  async execute(params, context) {
18809
18822
  const { directory, agent } = context;
18810
- return reconcileStatementCore(directory, agent, {
18823
+ return reconcileStatement(directory, agent, {
18811
18824
  provider: params.provider,
18812
18825
  currency: params.currency,
18813
18826
  closingBalance: params.closingBalance,
@@ -18816,34 +18829,157 @@ It must be run inside an import worktree (use import-pipeline for the full workf
18816
18829
  }
18817
18830
  });
18818
18831
  // 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 {};
18832
+ class NoTransactionsError extends Error {
18833
+ constructor() {
18834
+ super("No transactions to import");
18835
+ this.name = "NoTransactionsError";
18829
18836
  }
18830
18837
  }
18831
- function extractTransactionCount(importResult) {
18832
- try {
18833
- const parsed = JSON.parse(importResult);
18834
- return parsed.summary?.totalTransactions || 0;
18835
- } catch {
18836
- return 0;
18838
+ function buildStepResult(success2, message, details) {
18839
+ const result = { success: success2, message };
18840
+ if (details !== undefined) {
18841
+ result.details = details;
18842
+ }
18843
+ return result;
18844
+ }
18845
+ function buildSuccessResult5(result, summary) {
18846
+ result.success = true;
18847
+ result.summary = summary;
18848
+ return JSON.stringify(result);
18849
+ }
18850
+ function buildErrorResult5(result, error45, hint) {
18851
+ result.success = false;
18852
+ result.error = error45;
18853
+ if (hint) {
18854
+ result.hint = hint;
18837
18855
  }
18856
+ return JSON.stringify(result);
18838
18857
  }
18839
18858
  function buildCommitMessage(provider, currency, fromDate, untilDate, transactionCount) {
18840
18859
  const providerStr = provider?.toUpperCase() || "statements";
18841
- const currencyStr = currency?.toUpperCase() || "";
18860
+ const currencyStr = currency?.toUpperCase();
18842
18861
  const dateRange = fromDate && untilDate ? ` ${fromDate} to ${untilDate}` : "";
18843
18862
  const txStr = transactionCount > 0 ? ` (${transactionCount} transactions)` : "";
18844
- return `Import: ${providerStr} ${currencyStr}${dateRange}${txStr}`.trim();
18863
+ const parts = ["Import:", providerStr];
18864
+ if (currencyStr) {
18865
+ parts.push(currencyStr);
18866
+ }
18867
+ return `${parts.join(" ")}${dateRange}${txStr}`;
18845
18868
  }
18846
- async function importPipelineCore(directory, agent, options, configLoader = loadImportConfig, hledgerExecutor = defaultHledgerExecutor) {
18869
+ async function executeClassifyStep(context, worktree) {
18870
+ if (context.options.skipClassify) {
18871
+ context.result.steps.classify = buildStepResult(true, "Classification skipped (skipClassify: true)");
18872
+ return;
18873
+ }
18874
+ const inWorktree = () => true;
18875
+ const classifyResult = await classifyStatements(worktree.path, context.agent, context.configLoader, inWorktree);
18876
+ const classifyParsed = JSON.parse(classifyResult);
18877
+ const success2 = classifyParsed.success !== false;
18878
+ let message = success2 ? "Classification complete" : "Classification had issues";
18879
+ if (classifyParsed.unrecognized?.length > 0) {
18880
+ message = `Classification complete with ${classifyParsed.unrecognized.length} unrecognized file(s)`;
18881
+ }
18882
+ const details = {
18883
+ success: success2,
18884
+ unrecognized: classifyParsed.unrecognized,
18885
+ classified: classifyParsed
18886
+ };
18887
+ context.result.steps.classify = buildStepResult(success2, message, details);
18888
+ }
18889
+ async function executeDryRunStep(context, worktree) {
18890
+ const inWorktree = () => true;
18891
+ const dryRunResult = await importStatements(worktree.path, context.agent, {
18892
+ provider: context.options.provider,
18893
+ currency: context.options.currency,
18894
+ checkOnly: true
18895
+ }, context.configLoader, context.hledgerExecutor, inWorktree);
18896
+ const dryRunParsed = JSON.parse(dryRunResult);
18897
+ const message = dryRunParsed.success ? `Dry run passed: ${dryRunParsed.summary?.totalTransactions || 0} transactions ready` : `Dry run failed: ${dryRunParsed.summary?.unknown || 0} unknown account(s)`;
18898
+ context.result.steps.dryRun = buildStepResult(dryRunParsed.success, message, {
18899
+ success: dryRunParsed.success,
18900
+ summary: dryRunParsed.summary
18901
+ });
18902
+ if (!dryRunParsed.success) {
18903
+ context.result.error = "Dry run found unknown accounts or errors";
18904
+ context.result.hint = "Add rules to categorize unknown transactions, then retry";
18905
+ throw new Error("Dry run failed");
18906
+ }
18907
+ if (dryRunParsed.summary?.totalTransactions === 0) {
18908
+ throw new NoTransactionsError;
18909
+ }
18910
+ }
18911
+ async function executeImportStep(context, worktree) {
18912
+ const inWorktree = () => true;
18913
+ const importResult = await importStatements(worktree.path, context.agent, {
18914
+ provider: context.options.provider,
18915
+ currency: context.options.currency,
18916
+ checkOnly: false
18917
+ }, context.configLoader, context.hledgerExecutor, inWorktree);
18918
+ const importParsed = JSON.parse(importResult);
18919
+ const message = importParsed.success ? `Imported ${importParsed.summary?.totalTransactions || 0} transactions` : `Import failed: ${importParsed.error || "Unknown error"}`;
18920
+ context.result.steps.import = buildStepResult(importParsed.success, message, {
18921
+ success: importParsed.success,
18922
+ summary: importParsed.summary,
18923
+ error: importParsed.error
18924
+ });
18925
+ if (!importParsed.success) {
18926
+ context.result.error = `Import failed: ${importParsed.error || "Unknown error"}`;
18927
+ throw new Error("Import failed");
18928
+ }
18929
+ }
18930
+ async function executeReconcileStep(context, worktree) {
18931
+ const inWorktree = () => true;
18932
+ const reconcileResult = await reconcileStatement(worktree.path, context.agent, {
18933
+ provider: context.options.provider,
18934
+ currency: context.options.currency,
18935
+ closingBalance: context.options.closingBalance,
18936
+ account: context.options.account
18937
+ }, context.configLoader, context.hledgerExecutor, inWorktree);
18938
+ const reconcileParsed = JSON.parse(reconcileResult);
18939
+ const message = reconcileParsed.success ? `Balance reconciled: ${reconcileParsed.actualBalance}` : `Balance mismatch: expected ${reconcileParsed.expectedBalance}, got ${reconcileParsed.actualBalance}`;
18940
+ context.result.steps.reconcile = buildStepResult(reconcileParsed.success, message, {
18941
+ success: reconcileParsed.success,
18942
+ actualBalance: reconcileParsed.actualBalance,
18943
+ expectedBalance: reconcileParsed.expectedBalance,
18944
+ metadata: reconcileParsed.metadata,
18945
+ error: reconcileParsed.error
18946
+ });
18947
+ if (!reconcileParsed.success) {
18948
+ context.result.error = `Reconciliation failed: ${reconcileParsed.error || "Balance mismatch"}`;
18949
+ context.result.hint = "Check for missing transactions or incorrect rules";
18950
+ throw new Error("Reconciliation failed");
18951
+ }
18952
+ }
18953
+ async function executeMergeStep(context, worktree) {
18954
+ const importDetails = context.result.steps.import?.details;
18955
+ const reconcileDetails = context.result.steps.reconcile?.details;
18956
+ if (!importDetails || !reconcileDetails) {
18957
+ throw new Error("Import or reconcile step not completed before merge");
18958
+ }
18959
+ const commitInfo = {
18960
+ fromDate: reconcileDetails.metadata?.from_date,
18961
+ untilDate: reconcileDetails.metadata?.until_date
18962
+ };
18963
+ const transactionCount = importDetails.summary?.totalTransactions || 0;
18964
+ const commitMessage = buildCommitMessage(context.options.provider, context.options.currency, commitInfo.fromDate, commitInfo.untilDate, transactionCount);
18965
+ try {
18966
+ mergeWorktree(worktree, commitMessage);
18967
+ const mergeDetails = { commitMessage };
18968
+ context.result.steps.merge = buildStepResult(true, `Merged to main: "${commitMessage}"`, mergeDetails);
18969
+ } catch (error45) {
18970
+ const message = `Merge failed: ${error45 instanceof Error ? error45.message : String(error45)}`;
18971
+ context.result.steps.merge = buildStepResult(false, message);
18972
+ context.result.error = "Merge to main branch failed";
18973
+ throw new Error("Merge failed");
18974
+ }
18975
+ }
18976
+ function handleNoTransactions(result) {
18977
+ result.steps.import = buildStepResult(true, "No transactions to import");
18978
+ result.steps.reconcile = buildStepResult(true, "Reconciliation skipped (no transactions)");
18979
+ result.steps.merge = buildStepResult(true, "Merge skipped (no changes)");
18980
+ return buildSuccessResult5(result, "No transactions found to import");
18981
+ }
18982
+ async function importPipeline(directory, agent, options, configLoader = loadImportConfig, hledgerExecutor = defaultHledgerExecutor) {
18847
18983
  const restrictionError = checkAccountantAgent(agent, "import pipeline");
18848
18984
  if (restrictionError) {
18849
18985
  return restrictionError;
@@ -18852,163 +18988,47 @@ async function importPipelineCore(directory, agent, options, configLoader = load
18852
18988
  success: false,
18853
18989
  steps: {}
18854
18990
  };
18855
- let worktree = null;
18991
+ const context = {
18992
+ directory,
18993
+ agent,
18994
+ options,
18995
+ configLoader,
18996
+ hledgerExecutor,
18997
+ result
18998
+ };
18856
18999
  try {
18857
- try {
18858
- worktree = createImportWorktree(directory);
19000
+ return await withWorktree(directory, async (worktree) => {
18859
19001
  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) {
19002
+ result.steps.worktree = buildStepResult(true, `Created worktree at ${worktree.path}`, {
19003
+ path: worktree.path,
19004
+ branch: worktree.branch
19005
+ });
18995
19006
  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
- };
19007
+ await executeClassifyStep(context, worktree);
19008
+ await executeDryRunStep(context, worktree);
19009
+ await executeImportStep(context, worktree);
19010
+ await executeReconcileStep(context, worktree);
19011
+ await executeMergeStep(context, worktree);
19012
+ result.steps.cleanup = buildStepResult(true, "Worktree cleaned up", {
19013
+ cleanedAfterSuccess: true
19014
+ });
19015
+ const transactionCount = context.result.steps.import?.details?.summary?.totalTransactions || 0;
19016
+ return buildSuccessResult5(result, `Successfully imported ${transactionCount} transaction(s)`);
19017
+ } catch (error45) {
19018
+ result.steps.cleanup = buildStepResult(true, "Worktree cleaned up after failure", { cleanedAfterFailure: true });
19019
+ if (error45 instanceof NoTransactionsError) {
19020
+ return handleNoTransactions(result);
19021
+ }
19022
+ if (!result.error) {
19023
+ result.error = error45 instanceof Error ? error45.message : String(error45);
19024
+ }
19025
+ return buildErrorResult5(result, result.error, result.hint);
19006
19026
  }
19007
- }
19008
- if (!result.error) {
19009
- result.error = error45 instanceof Error ? error45.message : String(error45);
19010
- }
19011
- return JSON.stringify(result);
19027
+ });
19028
+ } catch (error45) {
19029
+ result.steps.worktree = buildStepResult(false, `Failed to create worktree: ${error45 instanceof Error ? error45.message : String(error45)}`);
19030
+ result.error = "Failed to create worktree";
19031
+ return buildErrorResult5(result, result.error);
19012
19032
  }
19013
19033
  }
19014
19034
  var import_pipeline_default = tool({
@@ -19045,7 +19065,7 @@ This tool orchestrates the full import workflow in an isolated git worktree:
19045
19065
  },
19046
19066
  async execute(params, context) {
19047
19067
  const { directory, agent } = context;
19048
- return importPipelineCore(directory, agent, {
19068
+ return importPipeline(directory, agent, {
19049
19069
  provider: params.provider,
19050
19070
  currency: params.currency,
19051
19071
  closingBalance: params.closingBalance,
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",
4
4
  "description": "An OpenCode accounting agent, specialized in double-entry-bookkepping with hledger",
5
5
  "author": {
6
6
  "name": "ali bengali",