@fuzzle/opencode-accountant 0.5.2 → 0.5.4

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.
@@ -3,13 +3,13 @@ description: Specialized agent for hledger accounting tasks and transaction mana
3
3
  prompt: You are an accounting specialist with expertise in hledger and double-entry bookkeeping. Your role is to help maintain accurate financial records following the user's conventions.
4
4
  mode: subagent
5
5
  temperature: 0.1
6
- steps: 8
6
+ maxSteps: 8
7
7
  tools:
8
8
  bash: true
9
9
  edit: true
10
10
  write: true
11
11
  # MCP tools available: import-pipeline, fetch-currency-prices
12
- permission:
12
+ permissions:
13
13
  bash: allow
14
14
  edit: allow
15
15
  glob: allow
@@ -151,13 +151,13 @@ The following are MCP tools available to you. Always call these tools directly -
151
151
 
152
152
  **Arguments:**
153
153
 
154
- | Argument | Type | Default | Description |
155
- | ---------------- | ------- | ------- | ------------------------------------------------------------------------------ |
156
- | `provider` | string | - | Logger metadata (does not filter CSV selection — use classify step for that) |
157
- | `currency` | string | - | Logger metadata (does not filter CSV selection — use classify step for that) |
158
- | `skipClassify` | boolean | `false` | Skip classification step |
159
- | `closingBalance` | string | - | Manual closing balance for reconciliation |
160
- | `account` | string | - | Manual account override (auto-detected from rules) |
154
+ | Argument | Type | Default | Description |
155
+ | ---------------- | ------- | ------- | ---------------------------------------------------------------------------- |
156
+ | `provider` | string | - | Logger metadata (does not filter CSV selection — use classify step for that) |
157
+ | `currency` | string | - | Logger metadata (does not filter CSV selection — use classify step for that) |
158
+ | `skipClassify` | boolean | `false` | Skip classification step |
159
+ | `closingBalance` | string | - | Manual closing balance for reconciliation |
160
+ | `account` | string | - | Manual account override (auto-detected from rules) |
161
161
 
162
162
  **Behavior:**
163
163
 
package/dist/index.js CHANGED
@@ -2677,6 +2677,12 @@ var init_js_yaml = __esm(() => {
2677
2677
 
2678
2678
  // src/utils/agentLoader.ts
2679
2679
  import * as fs from "fs";
2680
+ function validateAgentFields(data, filePath) {
2681
+ const unknownFields = Object.keys(data).filter((key) => !VALID_AGENT_FIELDS.has(key));
2682
+ if (unknownFields.length > 0) {
2683
+ console.warn(`Warning: Unknown fields in ${filePath} frontmatter: ${unknownFields.join(", ")}. ` + `Valid fields are: ${Array.from(VALID_AGENT_FIELDS).join(", ")}`);
2684
+ }
2685
+ }
2680
2686
  function loadAgent(filePath) {
2681
2687
  if (!fs.existsSync(filePath)) {
2682
2688
  return null;
@@ -2686,7 +2692,12 @@ function loadAgent(filePath) {
2686
2692
  if (!match) {
2687
2693
  throw new Error(`Invalid frontmatter format in ${filePath}`);
2688
2694
  }
2689
- const data = jsYaml.load(match[1]);
2695
+ const raw = jsYaml.load(match[1]);
2696
+ if (typeof raw !== "object" || raw === null || typeof raw.description !== "string") {
2697
+ throw new Error(`Invalid frontmatter in ${filePath}: must be an object with a "description" string`);
2698
+ }
2699
+ const data = raw;
2700
+ validateAgentFields(data, filePath);
2690
2701
  const { description, ...optional } = data;
2691
2702
  return {
2692
2703
  description,
@@ -2694,8 +2705,19 @@ function loadAgent(filePath) {
2694
2705
  ...Object.fromEntries(Object.entries(optional).filter(([, v]) => v !== undefined))
2695
2706
  };
2696
2707
  }
2708
+ var VALID_AGENT_FIELDS;
2697
2709
  var init_agentLoader = __esm(() => {
2698
2710
  init_js_yaml();
2711
+ VALID_AGENT_FIELDS = new Set([
2712
+ "description",
2713
+ "mode",
2714
+ "model",
2715
+ "temperature",
2716
+ "maxSteps",
2717
+ "disable",
2718
+ "tools",
2719
+ "permissions"
2720
+ ]);
2699
2721
  });
2700
2722
 
2701
2723
  // node_modules/papaparse/papaparse.js
@@ -16943,6 +16965,13 @@ function findCsvFiles(baseDir, options = {}) {
16943
16965
  }
16944
16966
  return csvFiles.sort();
16945
16967
  }
16968
+ function sortByMtimeNewestFirst(files) {
16969
+ return [...files].sort((a, b) => {
16970
+ const aStat = fs3.statSync(a);
16971
+ const bStat = fs3.statSync(b);
16972
+ return bStat.mtime.getTime() - aStat.mtime.getTime();
16973
+ });
16974
+ }
16946
16975
  function ensureDirectory(dirPath) {
16947
16976
  if (!fs3.existsSync(dirPath)) {
16948
16977
  fs3.mkdirSync(dirPath, { recursive: true });
@@ -23238,6 +23267,10 @@ function calculateDifference(expected, actual) {
23238
23267
  const currency = expectedParsed.currency || actualParsed.currency;
23239
23268
  return currency ? `${currency} ${sign}${diff.toFixed(2)}` : `${sign}${diff.toFixed(2)}`;
23240
23269
  }
23270
+ function formatBalance(amount, currency) {
23271
+ const formattedAmount = amount.toFixed(2);
23272
+ return currency ? `${currency} ${formattedAmount}` : formattedAmount;
23273
+ }
23241
23274
  function balancesMatch(balance1, balance2) {
23242
23275
  const parsed1 = parseBalance(balance1);
23243
23276
  const parsed2 = parseBalance(balance2);
@@ -23261,13 +23294,13 @@ function parseCsvFile(csvPath, config2) {
23261
23294
  const csvWithHeader = lines.slice(headerIndex).join(`
23262
23295
  `);
23263
23296
  const useFieldNames = config2.fieldNames.length > 0;
23264
- const result = import_papaparse2.default.parse(csvWithHeader, {
23265
- header: !useFieldNames,
23266
- delimiter: config2.separator,
23267
- skipEmptyLines: true
23268
- });
23269
23297
  if (useFieldNames) {
23270
- const rawRows = result.data;
23298
+ const result2 = import_papaparse2.default.parse(csvWithHeader, {
23299
+ header: false,
23300
+ delimiter: config2.separator,
23301
+ skipEmptyLines: true
23302
+ });
23303
+ const rawRows = result2.data;
23271
23304
  const dataRows = rawRows.slice(1);
23272
23305
  return dataRows.map((values) => {
23273
23306
  const row = {};
@@ -23277,6 +23310,11 @@ function parseCsvFile(csvPath, config2) {
23277
23310
  return row;
23278
23311
  });
23279
23312
  }
23313
+ const result = import_papaparse2.default.parse(csvWithHeader, {
23314
+ header: true,
23315
+ delimiter: config2.separator,
23316
+ skipEmptyLines: true
23317
+ });
23280
23318
  return result.data;
23281
23319
  }
23282
23320
  function getRowAmount(row, amountFields) {
@@ -23400,23 +23438,16 @@ function buildSuccessResult3(files, summary, message) {
23400
23438
  }
23401
23439
  function findCsvFromRulesFile(rulesFile) {
23402
23440
  const content = fs10.readFileSync(rulesFile, "utf-8");
23403
- const match2 = content.match(/^source\s+([^\n#]+)/m);
23404
- if (!match2) {
23441
+ const sourcePath = parseSourceDirective(content);
23442
+ if (!sourcePath) {
23405
23443
  return null;
23406
23444
  }
23407
- const sourcePath = match2[1].trim();
23408
- const rulesDir = path9.dirname(rulesFile);
23409
- const absolutePattern = path9.resolve(rulesDir, sourcePath);
23445
+ const absolutePattern = resolveSourcePath(sourcePath, rulesFile);
23410
23446
  const matches = glob.sync(absolutePattern);
23411
23447
  if (matches.length === 0) {
23412
23448
  return null;
23413
23449
  }
23414
- matches.sort((a, b) => {
23415
- const aStat = fs10.statSync(a);
23416
- const bStat = fs10.statSync(b);
23417
- return bStat.mtime.getTime() - aStat.mtime.getTime();
23418
- });
23419
- return matches[0];
23450
+ return sortByMtimeNewestFirst(matches)[0];
23420
23451
  }
23421
23452
  async function executeImports(fileResults, directory, pendingDir, doneDir, hledgerExecutor) {
23422
23453
  const importedFiles = [];
@@ -23592,12 +23623,7 @@ async function importStatements(directory, agent, options, configLoader = loadIm
23592
23623
  totalUnknown += fileResult.unknownPostings.length;
23593
23624
  }
23594
23625
  for (const [_rulesFile, matchingCSVs] of rulesFileToCSVs.entries()) {
23595
- matchingCSVs.sort((a, b) => {
23596
- const aStat = fs10.statSync(a);
23597
- const bStat = fs10.statSync(b);
23598
- return bStat.mtime.getTime() - aStat.mtime.getTime();
23599
- });
23600
- const newestCSV = matchingCSVs[0];
23626
+ const newestCSV = sortByMtimeNewestFirst(matchingCSVs)[0];
23601
23627
  const fileResult = await processCsvFile(newestCSV, rulesMapping, directory, hledgerExecutor);
23602
23628
  fileResults.push(fileResult);
23603
23629
  if (fileResult.error) {
@@ -23898,7 +23924,7 @@ function tryExtractClosingBalanceFromCSV(csvFile, rulesDir) {
23898
23924
  }
23899
23925
  }
23900
23926
  }
23901
- const balanceStr = currency ? `${currency} ${numericValue.toFixed(2)}` : numericValue.toFixed(2);
23927
+ const balanceStr = formatBalance(numericValue, currency || undefined);
23902
23928
  return {
23903
23929
  balance: balanceStr,
23904
23930
  confidence: "high",
@@ -24435,7 +24461,8 @@ async function executeAccountDeclarationsStep(context, contextId, logger) {
24435
24461
  break;
24436
24462
  }
24437
24463
  }
24438
- } catch {
24464
+ } catch (error45) {
24465
+ logger?.debug(`Failed to extract year from rules file ${rulesFile}: ${error45 instanceof Error ? error45.message : String(error45)}`);
24439
24466
  continue;
24440
24467
  }
24441
24468
  }
@@ -24473,6 +24500,37 @@ async function executeAccountDeclarationsStep(context, contextId, logger) {
24473
24500
  });
24474
24501
  logger?.endSection();
24475
24502
  }
24503
+ async function buildSuggestionContext(context, contextId, logger) {
24504
+ const { loadExistingAccounts: loadExistingAccounts2, extractRulePatternsFromFile: extractRulePatternsFromFile2 } = await Promise.resolve().then(() => (init_accountSuggester(), exports_accountSuggester));
24505
+ const config2 = context.configLoader(context.directory);
24506
+ const rulesDir = path12.join(context.directory, config2.paths.rules);
24507
+ const importCtx = loadContext(context.directory, contextId);
24508
+ const csvPath = path12.join(context.directory, importCtx.filePath);
24509
+ const rulesMapping = loadRulesMapping(rulesDir);
24510
+ const rulesFile = findRulesForCsv(csvPath, rulesMapping);
24511
+ if (!rulesFile) {
24512
+ return { existingAccounts: [], logger };
24513
+ }
24514
+ let yearJournalPath;
24515
+ try {
24516
+ const result = await context.hledgerExecutor(["print", "-f", rulesFile]);
24517
+ if (result.exitCode === 0) {
24518
+ const years = extractTransactionYears(result.stdout);
24519
+ if (years.size > 0) {
24520
+ yearJournalPath = ensureYearJournalExists(context.directory, Array.from(years)[0]);
24521
+ }
24522
+ }
24523
+ } catch (error45) {
24524
+ logger?.debug(`Could not determine year journal: ${error45 instanceof Error ? error45.message : String(error45)}`);
24525
+ }
24526
+ return {
24527
+ existingAccounts: yearJournalPath ? loadExistingAccounts2(yearJournalPath) : [],
24528
+ rulesFilePath: rulesFile,
24529
+ existingRules: extractRulePatternsFromFile2(rulesFile),
24530
+ yearJournalPath,
24531
+ logger
24532
+ };
24533
+ }
24476
24534
  async function executeDryRunStep(context, contextId, logger) {
24477
24535
  logger?.startSection("Step 3: Dry Run Import");
24478
24536
  logger?.logStep("Dry Run", "start");
@@ -24496,39 +24554,8 @@ async function executeDryRunStep(context, contextId, logger) {
24496
24554
  }
24497
24555
  if (allUnknownPostings.length > 0) {
24498
24556
  try {
24499
- const {
24500
- suggestAccountsForPostingsBatch: suggestAccountsForPostingsBatch2,
24501
- loadExistingAccounts: loadExistingAccounts2,
24502
- extractRulePatternsFromFile: extractRulePatternsFromFile2
24503
- } = await Promise.resolve().then(() => (init_accountSuggester(), exports_accountSuggester));
24504
- const config2 = context.configLoader(context.directory);
24505
- const rulesDir = path12.join(context.directory, config2.paths.rules);
24506
- const importCtx = loadContext(context.directory, contextId);
24507
- const csvPath = path12.join(context.directory, importCtx.filePath);
24508
- const rulesMapping = loadRulesMapping(rulesDir);
24509
- let yearJournalPath;
24510
- let firstRulesFile;
24511
- const rulesFile = findRulesForCsv(csvPath, rulesMapping);
24512
- if (rulesFile) {
24513
- firstRulesFile = rulesFile;
24514
- try {
24515
- const result = await context.hledgerExecutor(["print", "-f", rulesFile]);
24516
- if (result.exitCode === 0) {
24517
- const years = extractTransactionYears(result.stdout);
24518
- if (years.size > 0) {
24519
- const transactionYear = Array.from(years)[0];
24520
- yearJournalPath = ensureYearJournalExists(context.directory, transactionYear);
24521
- }
24522
- }
24523
- } catch {}
24524
- }
24525
- const suggestionContext = {
24526
- existingAccounts: yearJournalPath ? loadExistingAccounts2(yearJournalPath) : [],
24527
- rulesFilePath: firstRulesFile,
24528
- existingRules: firstRulesFile ? extractRulePatternsFromFile2(firstRulesFile) : undefined,
24529
- yearJournalPath,
24530
- logger
24531
- };
24557
+ const { suggestAccountsForPostingsBatch: suggestAccountsForPostingsBatch2 } = await Promise.resolve().then(() => (init_accountSuggester(), exports_accountSuggester));
24558
+ const suggestionContext = await buildSuggestionContext(context, contextId, logger);
24532
24559
  postingsWithSuggestions = await suggestAccountsForPostingsBatch2(allUnknownPostings, suggestionContext);
24533
24560
  } catch (error45) {
24534
24561
  logger?.error(`[ERROR] Failed to generate account suggestions: ${error45 instanceof Error ? error45.message : String(error45)}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fuzzle/opencode-accountant",
3
- "version": "0.5.2",
3
+ "version": "0.5.4",
4
4
  "description": "An OpenCode accounting agent, specialized in double-entry-bookkepping with hledger",
5
5
  "author": {
6
6
  "name": "ali bengali",