@fuzzle/opencode-accountant 0.5.1-next.1 → 0.5.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 +223 -257
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -2686,17 +2686,16 @@ function loadAgent(filePath) {
2686
2686
  if (!match) {
2687
2687
  throw new Error(`Invalid frontmatter format in ${filePath}`);
2688
2688
  }
2689
- const data = jsYaml.load(match[1]);
2689
+ const raw = jsYaml.load(match[1]);
2690
+ if (typeof raw !== "object" || raw === null || typeof raw.description !== "string") {
2691
+ throw new Error(`Invalid frontmatter in ${filePath}: must be an object with a "description" string`);
2692
+ }
2693
+ const data = raw;
2694
+ const { description, ...optional } = data;
2690
2695
  return {
2691
- description: data.description,
2696
+ description,
2692
2697
  prompt: match[2].trim(),
2693
- ...data.mode && { mode: data.mode },
2694
- ...data.model && { model: data.model },
2695
- ...data.temperature !== undefined && { temperature: data.temperature },
2696
- ...data.maxSteps !== undefined && { maxSteps: data.maxSteps },
2697
- ...data.disable !== undefined && { disable: data.disable },
2698
- ...data.tools && { tools: data.tools },
2699
- ...data.permissions && { permissions: data.permissions }
2698
+ ...Object.fromEntries(Object.entries(optional).filter(([, v]) => v !== undefined))
2700
2699
  };
2701
2700
  }
2702
2701
  var init_agentLoader = __esm(() => {
@@ -4293,94 +4292,63 @@ function extractRulePatternsFromFile(rulesPath) {
4293
4292
  }
4294
4293
  return patterns;
4295
4294
  }
4296
- function buildBatchSuggestionPrompt(postings, context) {
4297
- let prompt = `You are an accounting assistant helping categorize bank transactions.
4298
-
4299
- `;
4300
- prompt += `I have ${postings.length} transaction(s) that need account classification:
4301
-
4302
- `;
4303
- if (context.existingAccounts.length > 0) {
4304
- prompt += `## Existing Account Hierarchy
4295
+ function buildAccountHierarchySection(accounts) {
4296
+ if (accounts.length === 0)
4297
+ return "";
4298
+ return `## Existing Account Hierarchy
4305
4299
 
4306
- `;
4307
- prompt += context.existingAccounts.map((acc) => `- ${acc}`).join(`
4308
- `);
4309
- prompt += `
4300
+ ${accounts.map((acc) => `- ${acc}`).join(`
4301
+ `)}
4310
4302
 
4311
4303
  `;
4312
- }
4313
- if (context.existingRules && context.existingRules.length > 0) {
4314
- prompt += `## Example Classification Patterns from Rules
4304
+ }
4305
+ function buildRuleExamplesSection(rules) {
4306
+ if (!rules || rules.length === 0)
4307
+ return "";
4308
+ const sampleSize = Math.min(EXAMPLE_PATTERN_SAMPLE_SIZE, rules.length);
4309
+ const lines = rules.slice(0, sampleSize).map((p) => `- If description matches "${p.condition}" \u2192 ${p.account}`);
4310
+ return `## Example Classification Patterns from Rules
4315
4311
 
4316
- `;
4317
- const sampleSize = Math.min(EXAMPLE_PATTERN_SAMPLE_SIZE, context.existingRules.length);
4318
- for (let i2 = 0;i2 < sampleSize; i2++) {
4319
- const pattern = context.existingRules[i2];
4320
- prompt += `- If description matches "${pattern.condition}" \u2192 ${pattern.account}
4321
- `;
4322
- }
4323
- prompt += `
4324
- `;
4325
- }
4326
- prompt += `## Transactions to Classify
4312
+ ${lines.join(`
4313
+ `)}
4327
4314
 
4328
4315
  `;
4329
- postings.forEach((posting, index) => {
4330
- prompt += `Transaction ${index + 1}:
4331
- `;
4332
- prompt += `- Type: ${posting.account === "income:unknown" ? "Income" : "Expense"}
4333
- `;
4334
- prompt += `- Date: ${posting.date}
4335
- `;
4336
- prompt += `- Description: ${posting.description}
4337
- `;
4338
- prompt += `- Amount: ${posting.amount}
4339
- `;
4316
+ }
4317
+ function buildTransactionsSection(postings) {
4318
+ const lines = postings.map((posting, index) => {
4319
+ const parts = [
4320
+ `Transaction ${index + 1}:`,
4321
+ `- Type: ${posting.account === "income:unknown" ? "Income" : "Expense"}`,
4322
+ `- Date: ${posting.date}`,
4323
+ `- Description: ${posting.description}`,
4324
+ `- Amount: ${posting.amount}`
4325
+ ];
4340
4326
  if (posting.csvRow) {
4341
- prompt += `- CSV Data: ${JSON.stringify(posting.csvRow)}
4342
- `;
4327
+ parts.push(`- CSV Data: ${JSON.stringify(posting.csvRow)}`);
4343
4328
  }
4344
- prompt += `
4345
- `;
4329
+ return parts.join(`
4330
+ `);
4346
4331
  });
4347
- prompt += `## Task
4332
+ return `## Transactions to Classify
4348
4333
 
4349
- `;
4350
- prompt += `For EACH transaction, suggest the most appropriate account. You may:
4351
- `;
4352
- prompt += `1. Suggest an existing account from the hierarchy above
4353
- `;
4354
- prompt += `2. Propose a NEW account following the existing naming patterns
4355
-
4356
- `;
4357
- prompt += `## Response Format
4358
-
4359
- `;
4360
- prompt += `Respond with suggestions for ALL transactions in this exact format:
4361
-
4362
- `;
4363
- prompt += `TRANSACTION 1:
4364
- `;
4365
- prompt += `ACCOUNT: {account_name}
4366
- `;
4367
- prompt += `CONFIDENCE: {high|medium|low}
4368
- `;
4369
- prompt += `REASONING: {brief one-sentence explanation}
4334
+ ${lines.join(`
4370
4335
 
4371
- `;
4372
- prompt += `TRANSACTION 2:
4373
- `;
4374
- prompt += `ACCOUNT: {account_name}
4375
- `;
4376
- prompt += `CONFIDENCE: {high|medium|low}
4377
- `;
4378
- prompt += `REASONING: {brief one-sentence explanation}
4336
+ `)}
4379
4337
 
4380
4338
  `;
4381
- prompt += `... (continue for all transactions)
4382
- `;
4383
- return prompt;
4339
+ }
4340
+ function buildBatchSuggestionPrompt(postings, context) {
4341
+ return [
4342
+ `You are an accounting assistant helping categorize bank transactions.
4343
+ `,
4344
+ `I have ${postings.length} transaction(s) that need account classification:
4345
+ `,
4346
+ buildAccountHierarchySection(context.existingAccounts),
4347
+ buildRuleExamplesSection(context.existingRules),
4348
+ buildTransactionsSection(postings),
4349
+ RESPONSE_FORMAT_SECTION
4350
+ ].join(`
4351
+ `);
4384
4352
  }
4385
4353
  function parseBatchSuggestionResponse(response) {
4386
4354
  const suggestions = [];
@@ -4399,20 +4367,36 @@ function parseBatchSuggestionResponse(response) {
4399
4367
  }
4400
4368
  return suggestions;
4401
4369
  }
4402
- async function suggestAccountsForPostingsBatch(postings, context) {
4403
- if (postings.length === 0) {
4404
- return [];
4405
- }
4406
- const uncachedPostings = [];
4407
- const cachedResults = new Map;
4370
+ function partitionByCacheStatus(postings) {
4371
+ const uncached = [];
4372
+ const cached2 = new Map;
4408
4373
  postings.forEach((posting, index) => {
4409
4374
  const hash2 = hashTransaction(posting);
4410
4375
  if (suggestionCache[hash2]) {
4411
- cachedResults.set(index, suggestionCache[hash2]);
4376
+ cached2.set(index, suggestionCache[hash2]);
4412
4377
  } else {
4413
- uncachedPostings.push(posting);
4378
+ uncached.push(posting);
4414
4379
  }
4415
4380
  });
4381
+ return { uncached, cached: cached2 };
4382
+ }
4383
+ function mergeSuggestions(postings, cachedResults, newSuggestions) {
4384
+ let uncachedIndex = 0;
4385
+ return postings.map((posting, index) => {
4386
+ const suggestion = cachedResults.get(index) || newSuggestions[uncachedIndex++];
4387
+ return {
4388
+ ...posting,
4389
+ suggestedAccount: suggestion?.account,
4390
+ suggestionConfidence: suggestion?.confidence,
4391
+ suggestionReasoning: suggestion?.reasoning
4392
+ };
4393
+ });
4394
+ }
4395
+ async function suggestAccountsForPostingsBatch(postings, context) {
4396
+ if (postings.length === 0) {
4397
+ return [];
4398
+ }
4399
+ const { uncached: uncachedPostings, cached: cachedResults } = partitionByCacheStatus(postings);
4416
4400
  context.logger?.info(`Account suggestions: ${cachedResults.size} cached, ${uncachedPostings.length} to generate`);
4417
4401
  let newSuggestions = [];
4418
4402
  if (uncachedPostings.length > 0) {
@@ -4445,19 +4429,7 @@ ${userPrompt}`;
4445
4429
  return postings;
4446
4430
  }
4447
4431
  }
4448
- const results = [];
4449
- let uncachedIndex = 0;
4450
- postings.forEach((posting, index) => {
4451
- const cachedSuggestion = cachedResults.get(index);
4452
- const suggestion = cachedSuggestion || newSuggestions[uncachedIndex++];
4453
- results.push({
4454
- ...posting,
4455
- suggestedAccount: suggestion?.account,
4456
- suggestionConfidence: suggestion?.confidence,
4457
- suggestionReasoning: suggestion?.reasoning
4458
- });
4459
- });
4460
- return results;
4432
+ return mergeSuggestions(postings, cachedResults, newSuggestions);
4461
4433
  }
4462
4434
  function generateMockSuggestions(postings) {
4463
4435
  let response = "";
@@ -4492,10 +4464,34 @@ function generateMockSuggestions(postings) {
4492
4464
  });
4493
4465
  return response;
4494
4466
  }
4495
- var EXAMPLE_PATTERN_SAMPLE_SIZE = 10, suggestionCache;
4467
+ var EXAMPLE_PATTERN_SAMPLE_SIZE = 10, suggestionCache, RESPONSE_FORMAT_SECTION;
4496
4468
  var init_accountSuggester = __esm(() => {
4497
4469
  init_agentLoader();
4498
4470
  suggestionCache = {};
4471
+ RESPONSE_FORMAT_SECTION = [
4472
+ `## Task
4473
+ `,
4474
+ "For EACH transaction, suggest the most appropriate account. You may:",
4475
+ "1. Suggest an existing account from the hierarchy above",
4476
+ `2. Propose a NEW account following the existing naming patterns
4477
+ `,
4478
+ `## Response Format
4479
+ `,
4480
+ `Respond with suggestions for ALL transactions in this exact format:
4481
+ `,
4482
+ "TRANSACTION 1:",
4483
+ "ACCOUNT: {account_name}",
4484
+ "CONFIDENCE: {high|medium|low}",
4485
+ `REASONING: {brief one-sentence explanation}
4486
+ `,
4487
+ "TRANSACTION 2:",
4488
+ "ACCOUNT: {account_name}",
4489
+ "CONFIDENCE: {high|medium|low}",
4490
+ `REASONING: {brief one-sentence explanation}
4491
+ `,
4492
+ "... (continue for all transactions)"
4493
+ ].join(`
4494
+ `);
4499
4495
  });
4500
4496
 
4501
4497
  // src/index.ts
@@ -16920,13 +16916,7 @@ function findCsvFiles(baseDir, options = {}) {
16920
16916
  if (!fs3.existsSync(baseDir)) {
16921
16917
  return [];
16922
16918
  }
16923
- let searchDir = baseDir;
16924
- if (options.subdir) {
16925
- searchDir = path2.join(searchDir, options.subdir);
16926
- if (options.subsubdir) {
16927
- searchDir = path2.join(searchDir, options.subsubdir);
16928
- }
16929
- }
16919
+ const searchDir = path2.join(...[baseDir, options.subdir, options.subsubdir].filter((p) => !!p));
16930
16920
  if (!fs3.existsSync(searchDir)) {
16931
16921
  return [];
16932
16922
  }
@@ -16957,6 +16947,13 @@ function findCsvFiles(baseDir, options = {}) {
16957
16947
  }
16958
16948
  return csvFiles.sort();
16959
16949
  }
16950
+ function sortByMtimeNewestFirst(files) {
16951
+ return [...files].sort((a, b) => {
16952
+ const aStat = fs3.statSync(a);
16953
+ const bStat = fs3.statSync(b);
16954
+ return bStat.mtime.getTime() - aStat.mtime.getTime();
16955
+ });
16956
+ }
16960
16957
  function ensureDirectory(dirPath) {
16961
16958
  if (!fs3.existsSync(dirPath)) {
16962
16959
  fs3.mkdirSync(dirPath, { recursive: true });
@@ -17436,17 +17433,7 @@ function createContext(directory, params) {
17436
17433
  id: randomUUID(),
17437
17434
  createdAt: now,
17438
17435
  updatedAt: now,
17439
- filename: params.filename,
17440
- filePath: params.filePath,
17441
- provider: params.provider,
17442
- currency: params.currency,
17443
- accountNumber: params.accountNumber,
17444
- originalFilename: params.originalFilename,
17445
- fromDate: params.fromDate,
17446
- untilDate: params.untilDate,
17447
- openingBalance: params.openingBalance,
17448
- closingBalance: params.closingBalance,
17449
- account: params.account
17436
+ ...params
17450
17437
  };
17451
17438
  ensureDirectory(path5.join(directory, ".memory"));
17452
17439
  const contextPath = getContextPath(directory, context.id);
@@ -23013,6 +23000,7 @@ function findRulesForCsv(csvPath, mapping) {
23013
23000
  // src/utils/hledgerExecutor.ts
23014
23001
  var {$: $2 } = globalThis.Bun;
23015
23002
  var STDERR_TRUNCATE_LENGTH = 500;
23003
+ var TX_HEADER_PATTERN = /^(\d{4})-(\d{2}-\d{2})\s+(.+)$/;
23016
23004
  async function defaultHledgerExecutor(cmdArgs) {
23017
23005
  try {
23018
23006
  const result = await $2`hledger ${cmdArgs}`.quiet().nothrow();
@@ -23047,10 +23035,10 @@ function parseUnknownPostings(hledgerOutput) {
23047
23035
  let currentDate = "";
23048
23036
  let currentDescription = "";
23049
23037
  for (const line of lines) {
23050
- const headerMatch = line.match(/^(\d{4}-\d{2}-\d{2})\s+(.+)$/);
23038
+ const headerMatch = line.match(TX_HEADER_PATTERN);
23051
23039
  if (headerMatch) {
23052
- currentDate = headerMatch[1];
23053
- currentDescription = headerMatch[2].trim();
23040
+ currentDate = `${headerMatch[1]}-${headerMatch[2]}`;
23041
+ currentDescription = headerMatch[3].trim();
23054
23042
  continue;
23055
23043
  }
23056
23044
  const postingMatch = line.match(/^\s+(income:unknown|expenses:unknown)\s+([^\s]+(?:\s+[^\s=]+)?)\s*(?:=\s*(.+))?$/);
@@ -23071,7 +23059,7 @@ function countTransactions(hledgerOutput) {
23071
23059
  `);
23072
23060
  let count = 0;
23073
23061
  for (const line of lines) {
23074
- if (/^\d{4}-\d{2}-\d{2}\s+/.test(line)) {
23062
+ if (TX_HEADER_PATTERN.test(line)) {
23075
23063
  count++;
23076
23064
  }
23077
23065
  }
@@ -23082,7 +23070,7 @@ function extractTransactionYears(hledgerOutput) {
23082
23070
  const lines = hledgerOutput.split(`
23083
23071
  `);
23084
23072
  for (const line of lines) {
23085
- const match2 = line.match(/^(\d{4})-\d{2}-\d{2}\s+/);
23073
+ const match2 = line.match(TX_HEADER_PATTERN);
23086
23074
  if (match2) {
23087
23075
  years.add(parseInt(match2[1], 10));
23088
23076
  }
@@ -23142,6 +23130,13 @@ async function getAccountBalance(mainJournalPath, account, asOfDate, executor =
23142
23130
 
23143
23131
  // src/utils/rulesParser.ts
23144
23132
  import * as fs8 from "fs";
23133
+ function resolveFieldRef(fieldRef, fieldNames) {
23134
+ if (/^\d+$/.test(fieldRef)) {
23135
+ const index = parseInt(fieldRef, 10) - 1;
23136
+ return fieldNames[index] || fieldRef;
23137
+ }
23138
+ return fieldRef;
23139
+ }
23145
23140
  function parseSkipRows(rulesContent) {
23146
23141
  const match2 = rulesContent.match(/^skip\s+(\d+)/m);
23147
23142
  return match2 ? parseInt(match2[1], 10) : 0;
@@ -23166,24 +23161,13 @@ function parseDateField(rulesContent, fieldNames) {
23166
23161
  if (!match2) {
23167
23162
  return fieldNames[0] || "date";
23168
23163
  }
23169
- const value = match2[1];
23170
- if (/^\d+$/.test(value)) {
23171
- const index = parseInt(value, 10) - 1;
23172
- return fieldNames[index] || value;
23173
- }
23174
- return value;
23164
+ return resolveFieldRef(match2[1], fieldNames);
23175
23165
  }
23176
23166
  function parseAmountFields(rulesContent, fieldNames) {
23177
23167
  const result = {};
23178
23168
  const simpleMatch = rulesContent.match(/^amount\s+(-?)%(\w+|\d+)/m);
23179
23169
  if (simpleMatch) {
23180
- const fieldRef = simpleMatch[2];
23181
- if (/^\d+$/.test(fieldRef)) {
23182
- const index = parseInt(fieldRef, 10) - 1;
23183
- result.single = fieldNames[index] || fieldRef;
23184
- } else {
23185
- result.single = fieldRef;
23186
- }
23170
+ result.single = resolveFieldRef(simpleMatch[2], fieldNames);
23187
23171
  }
23188
23172
  const debitMatch = rulesContent.match(/if\s+%(\w+)\s+\.\s*\n\s*amount\s+-?%\1/m);
23189
23173
  if (debitMatch) {
@@ -23248,29 +23232,34 @@ function parseBalance(balance) {
23248
23232
  const amount = parseFloat(amountStr.replace(/,/g, ""));
23249
23233
  return { currency, amount };
23250
23234
  }
23235
+ function validateCurrencies(a, b) {
23236
+ if (a.currency && b.currency && a.currency !== b.currency) {
23237
+ throw new Error(`Currency mismatch: ${a.currency} vs ${b.currency}`);
23238
+ }
23239
+ }
23251
23240
  function calculateDifference(expected, actual) {
23252
23241
  const expectedParsed = parseBalance(expected);
23253
23242
  const actualParsed = parseBalance(actual);
23254
23243
  if (!expectedParsed || !actualParsed) {
23255
23244
  throw new Error(`Cannot parse balances: expected="${expected}", actual="${actual}"`);
23256
23245
  }
23257
- if (expectedParsed.currency && actualParsed.currency && expectedParsed.currency !== actualParsed.currency) {
23258
- throw new Error(`Currency mismatch: expected ${expectedParsed.currency}, got ${actualParsed.currency}`);
23259
- }
23246
+ validateCurrencies(expectedParsed, actualParsed);
23260
23247
  const diff = actualParsed.amount - expectedParsed.amount;
23261
23248
  const sign = diff >= 0 ? "+" : "";
23262
23249
  const currency = expectedParsed.currency || actualParsed.currency;
23263
23250
  return currency ? `${currency} ${sign}${diff.toFixed(2)}` : `${sign}${diff.toFixed(2)}`;
23264
23251
  }
23252
+ function formatBalance(amount, currency) {
23253
+ const formattedAmount = amount.toFixed(2);
23254
+ return currency ? `${currency} ${formattedAmount}` : formattedAmount;
23255
+ }
23265
23256
  function balancesMatch(balance1, balance2) {
23266
23257
  const parsed1 = parseBalance(balance1);
23267
23258
  const parsed2 = parseBalance(balance2);
23268
23259
  if (!parsed1 || !parsed2) {
23269
23260
  return false;
23270
23261
  }
23271
- if (parsed1.currency && parsed2.currency && parsed1.currency !== parsed2.currency) {
23272
- throw new Error(`Currency mismatch: ${parsed1.currency} vs ${parsed2.currency}`);
23273
- }
23262
+ validateCurrencies(parsed1, parsed2);
23274
23263
  return parsed1.amount === parsed2.amount;
23275
23264
  }
23276
23265
 
@@ -23287,13 +23276,13 @@ function parseCsvFile(csvPath, config2) {
23287
23276
  const csvWithHeader = lines.slice(headerIndex).join(`
23288
23277
  `);
23289
23278
  const useFieldNames = config2.fieldNames.length > 0;
23290
- const result = import_papaparse2.default.parse(csvWithHeader, {
23291
- header: !useFieldNames,
23292
- delimiter: config2.separator,
23293
- skipEmptyLines: true
23294
- });
23295
23279
  if (useFieldNames) {
23296
- const rawRows = result.data;
23280
+ const result2 = import_papaparse2.default.parse(csvWithHeader, {
23281
+ header: false,
23282
+ delimiter: config2.separator,
23283
+ skipEmptyLines: true
23284
+ });
23285
+ const rawRows = result2.data;
23297
23286
  const dataRows = rawRows.slice(1);
23298
23287
  return dataRows.map((values) => {
23299
23288
  const row = {};
@@ -23303,6 +23292,11 @@ function parseCsvFile(csvPath, config2) {
23303
23292
  return row;
23304
23293
  });
23305
23294
  }
23295
+ const result = import_papaparse2.default.parse(csvWithHeader, {
23296
+ header: true,
23297
+ delimiter: config2.separator,
23298
+ skipEmptyLines: true
23299
+ });
23306
23300
  return result.data;
23307
23301
  }
23308
23302
  function getRowAmount(row, amountFields) {
@@ -23363,8 +23357,7 @@ function looksLikeTransactionId(fieldName, value) {
23363
23357
  if (!nameMatches)
23364
23358
  return false;
23365
23359
  const trimmedValue = value.trim();
23366
- const looksLikeId = /^[A-Za-z0-9_-]+$/.test(trimmedValue) && trimmedValue.length >= 3;
23367
- return looksLikeId;
23360
+ return /^[A-Za-z0-9_-]+$/.test(trimmedValue) && trimmedValue.length >= 3;
23368
23361
  }
23369
23362
  function findTransactionId(row) {
23370
23363
  for (const [field, value] of Object.entries(row)) {
@@ -23381,9 +23374,7 @@ function findMatchingCsvRow(posting, csvRows, config2) {
23381
23374
  const rowAmount = getRowAmount(row, config2.amountFields);
23382
23375
  if (rowDate !== posting.date)
23383
23376
  return false;
23384
- if (Math.abs(rowAmount - postingAmount) > AMOUNT_MATCH_TOLERANCE)
23385
- return false;
23386
- return true;
23377
+ return Math.abs(rowAmount - postingAmount) <= AMOUNT_MATCH_TOLERANCE;
23387
23378
  });
23388
23379
  if (candidates.length === 1) {
23389
23380
  return candidates[0];
@@ -23404,13 +23395,7 @@ function findMatchingCsvRow(posting, csvRows, config2) {
23404
23395
  const descMatches = candidates.filter((row) => {
23405
23396
  return Object.values(row).some((value) => value && value.toLowerCase().includes(descriptionLower));
23406
23397
  });
23407
- if (descMatches.length === 1) {
23408
- return descMatches[0];
23409
- }
23410
- if (descMatches.length > 1) {
23411
- return descMatches[0];
23412
- }
23413
- return candidates[0];
23398
+ return descMatches[0] || candidates[0];
23414
23399
  }
23415
23400
 
23416
23401
  // src/tools/import-statements.ts
@@ -23435,23 +23420,16 @@ function buildSuccessResult3(files, summary, message) {
23435
23420
  }
23436
23421
  function findCsvFromRulesFile(rulesFile) {
23437
23422
  const content = fs10.readFileSync(rulesFile, "utf-8");
23438
- const match2 = content.match(/^source\s+([^\n#]+)/m);
23439
- if (!match2) {
23423
+ const sourcePath = parseSourceDirective(content);
23424
+ if (!sourcePath) {
23440
23425
  return null;
23441
23426
  }
23442
- const sourcePath = match2[1].trim();
23443
- const rulesDir = path9.dirname(rulesFile);
23444
- const absolutePattern = path9.resolve(rulesDir, sourcePath);
23427
+ const absolutePattern = resolveSourcePath(sourcePath, rulesFile);
23445
23428
  const matches = glob.sync(absolutePattern);
23446
23429
  if (matches.length === 0) {
23447
23430
  return null;
23448
23431
  }
23449
- matches.sort((a, b) => {
23450
- const aStat = fs10.statSync(a);
23451
- const bStat = fs10.statSync(b);
23452
- return bStat.mtime.getTime() - aStat.mtime.getTime();
23453
- });
23454
- return matches[0];
23432
+ return sortByMtimeNewestFirst(matches)[0];
23455
23433
  }
23456
23434
  async function executeImports(fileResults, directory, pendingDir, doneDir, hledgerExecutor) {
23457
23435
  const importedFiles = [];
@@ -23627,12 +23605,7 @@ async function importStatements(directory, agent, options, configLoader = loadIm
23627
23605
  totalUnknown += fileResult.unknownPostings.length;
23628
23606
  }
23629
23607
  for (const [_rulesFile, matchingCSVs] of rulesFileToCSVs.entries()) {
23630
- matchingCSVs.sort((a, b) => {
23631
- const aStat = fs10.statSync(a);
23632
- const bStat = fs10.statSync(b);
23633
- return bStat.mtime.getTime() - aStat.mtime.getTime();
23634
- });
23635
- const newestCSV = matchingCSVs[0];
23608
+ const newestCSV = sortByMtimeNewestFirst(matchingCSVs)[0];
23636
23609
  const fileResult = await processCsvFile(newestCSV, rulesMapping, directory, hledgerExecutor);
23637
23610
  fileResults.push(fileResult);
23638
23611
  if (fileResult.error) {
@@ -23933,7 +23906,7 @@ function tryExtractClosingBalanceFromCSV(csvFile, rulesDir) {
23933
23906
  }
23934
23907
  }
23935
23908
  }
23936
- const balanceStr = currency ? `${currency} ${numericValue.toFixed(2)}` : numericValue.toFixed(2);
23909
+ const balanceStr = formatBalance(numericValue, currency || undefined);
23937
23910
  return {
23938
23911
  balance: balanceStr,
23939
23912
  confidence: "high",
@@ -24118,7 +24091,6 @@ function extractAccountsFromRulesFile(rulesPath) {
24118
24091
  const account2Match = trimmed.match(/account2\s+(.+?)(?:\s+|$)/);
24119
24092
  if (account2Match) {
24120
24093
  accounts.add(account2Match[1].trim());
24121
- continue;
24122
24094
  }
24123
24095
  }
24124
24096
  return accounts;
@@ -24136,52 +24108,46 @@ function getAllAccountsFromRules(rulesPaths) {
24136
24108
  function sortAccountDeclarations(accounts) {
24137
24109
  return Array.from(accounts).sort((a, b) => a.localeCompare(b));
24138
24110
  }
24139
- function ensureAccountDeclarations(yearJournalPath, accounts) {
24140
- if (!fs12.existsSync(yearJournalPath)) {
24141
- throw new Error(`Year journal not found: ${yearJournalPath}`);
24142
- }
24143
- const content = fs12.readFileSync(yearJournalPath, "utf-8");
24111
+ function parseJournalSections(content) {
24144
24112
  const lines = content.split(`
24145
24113
  `);
24146
24114
  const existingAccounts = new Set;
24147
24115
  const commentLines = [];
24148
- const accountLines = [];
24149
24116
  const otherLines = [];
24150
24117
  let inAccountSection = false;
24151
24118
  let accountSectionEnded = false;
24152
24119
  for (const line of lines) {
24153
24120
  const trimmed = line.trim();
24154
24121
  if (trimmed.startsWith(";") || trimmed.startsWith("#")) {
24155
- if (!accountSectionEnded) {
24156
- commentLines.push(line);
24157
- } else {
24158
- otherLines.push(line);
24159
- }
24122
+ (accountSectionEnded ? otherLines : commentLines).push(line);
24160
24123
  continue;
24161
24124
  }
24162
24125
  if (trimmed.startsWith("account ")) {
24163
24126
  inAccountSection = true;
24164
24127
  const accountMatch = trimmed.match(/^account\s+(.+?)(?:\s+|$)/);
24165
24128
  if (accountMatch) {
24166
- const accountName = accountMatch[1].trim();
24167
- existingAccounts.add(accountName);
24168
- accountLines.push(line);
24129
+ existingAccounts.add(accountMatch[1].trim());
24169
24130
  }
24170
24131
  continue;
24171
24132
  }
24172
24133
  if (trimmed === "") {
24173
- if (inAccountSection && !accountSectionEnded) {
24174
- accountLines.push(line);
24175
- } else {
24176
- otherLines.push(line);
24177
- }
24134
+ if (inAccountSection && !accountSectionEnded)
24135
+ continue;
24136
+ otherLines.push(line);
24178
24137
  continue;
24179
24138
  }
24180
- if (inAccountSection) {
24139
+ if (inAccountSection)
24181
24140
  accountSectionEnded = true;
24182
- }
24183
24141
  otherLines.push(line);
24184
24142
  }
24143
+ return { existingAccounts, commentLines, otherLines };
24144
+ }
24145
+ function ensureAccountDeclarations(yearJournalPath, accounts) {
24146
+ if (!fs12.existsSync(yearJournalPath)) {
24147
+ throw new Error(`Year journal not found: ${yearJournalPath}`);
24148
+ }
24149
+ const content = fs12.readFileSync(yearJournalPath, "utf-8");
24150
+ const { existingAccounts, commentLines, otherLines } = parseJournalSections(content);
24185
24151
  const missingAccounts = new Set;
24186
24152
  for (const account of accounts) {
24187
24153
  if (!existingAccounts.has(account)) {
@@ -24247,14 +24213,10 @@ class MarkdownLogger {
24247
24213
  }
24248
24214
  }
24249
24215
  info(message) {
24250
- this.buffer.push(message);
24251
- if (this.autoFlush)
24252
- this.flushAsync();
24216
+ this.log(message);
24253
24217
  }
24254
24218
  warn(message) {
24255
- this.buffer.push(`\u26A0\uFE0F **WARNING**: ${message}`);
24256
- if (this.autoFlush)
24257
- this.flushAsync();
24219
+ this.log(`\u26A0\uFE0F **WARNING**: ${message}`);
24258
24220
  }
24259
24221
  error(message, error45) {
24260
24222
  this.buffer.push(`\u274C **ERROR**: ${message}`);
@@ -24270,13 +24232,10 @@ class MarkdownLogger {
24270
24232
  this.buffer.push("```");
24271
24233
  this.buffer.push("");
24272
24234
  }
24273
- if (this.autoFlush)
24274
- this.flushAsync();
24235
+ this.autoFlushIfEnabled();
24275
24236
  }
24276
24237
  debug(message) {
24277
- this.buffer.push(`\uD83D\uDD0D ${message}`);
24278
- if (this.autoFlush)
24279
- this.flushAsync();
24238
+ this.log(`\uD83D\uDD0D ${message}`);
24280
24239
  }
24281
24240
  logStep(stepName, status, details) {
24282
24241
  const icon = status === "success" ? "\u2705" : status === "error" ? "\u274C" : "\u25B6\uFE0F";
@@ -24286,8 +24245,7 @@ class MarkdownLogger {
24286
24245
  this.buffer.push(` ${details}`);
24287
24246
  }
24288
24247
  this.buffer.push("");
24289
- if (this.autoFlush)
24290
- this.flushAsync();
24248
+ this.autoFlushIfEnabled();
24291
24249
  }
24292
24250
  logCommand(command, output) {
24293
24251
  this.buffer.push("```bash");
@@ -24305,16 +24263,14 @@ class MarkdownLogger {
24305
24263
  }
24306
24264
  this.buffer.push("```");
24307
24265
  this.buffer.push("");
24308
- if (this.autoFlush)
24309
- this.flushAsync();
24266
+ this.autoFlushIfEnabled();
24310
24267
  }
24311
24268
  logResult(data) {
24312
24269
  this.buffer.push("```json");
24313
24270
  this.buffer.push(JSON.stringify(data, null, 2));
24314
24271
  this.buffer.push("```");
24315
24272
  this.buffer.push("");
24316
- if (this.autoFlush)
24317
- this.flushAsync();
24273
+ this.autoFlushIfEnabled();
24318
24274
  }
24319
24275
  setContext(key, value) {
24320
24276
  this.context[key] = value;
@@ -24334,6 +24290,15 @@ class MarkdownLogger {
24334
24290
  getLogPath() {
24335
24291
  return this.logPath;
24336
24292
  }
24293
+ log(message) {
24294
+ this.buffer.push(message);
24295
+ this.autoFlushIfEnabled();
24296
+ }
24297
+ autoFlushIfEnabled() {
24298
+ if (!this.autoFlush)
24299
+ return;
24300
+ this.flushAsync();
24301
+ }
24337
24302
  flushAsync() {
24338
24303
  this.pendingFlush = this.flush().catch(() => {});
24339
24304
  }
@@ -24478,7 +24443,8 @@ async function executeAccountDeclarationsStep(context, contextId, logger) {
24478
24443
  break;
24479
24444
  }
24480
24445
  }
24481
- } catch {
24446
+ } catch (error45) {
24447
+ logger?.debug(`Failed to extract year from rules file ${rulesFile}: ${error45 instanceof Error ? error45.message : String(error45)}`);
24482
24448
  continue;
24483
24449
  }
24484
24450
  }
@@ -24516,6 +24482,37 @@ async function executeAccountDeclarationsStep(context, contextId, logger) {
24516
24482
  });
24517
24483
  logger?.endSection();
24518
24484
  }
24485
+ async function buildSuggestionContext(context, contextId, logger) {
24486
+ const { loadExistingAccounts: loadExistingAccounts2, extractRulePatternsFromFile: extractRulePatternsFromFile2 } = await Promise.resolve().then(() => (init_accountSuggester(), exports_accountSuggester));
24487
+ const config2 = context.configLoader(context.directory);
24488
+ const rulesDir = path12.join(context.directory, config2.paths.rules);
24489
+ const importCtx = loadContext(context.directory, contextId);
24490
+ const csvPath = path12.join(context.directory, importCtx.filePath);
24491
+ const rulesMapping = loadRulesMapping(rulesDir);
24492
+ const rulesFile = findRulesForCsv(csvPath, rulesMapping);
24493
+ if (!rulesFile) {
24494
+ return { existingAccounts: [], logger };
24495
+ }
24496
+ let yearJournalPath;
24497
+ try {
24498
+ const result = await context.hledgerExecutor(["print", "-f", rulesFile]);
24499
+ if (result.exitCode === 0) {
24500
+ const years = extractTransactionYears(result.stdout);
24501
+ if (years.size > 0) {
24502
+ yearJournalPath = ensureYearJournalExists(context.directory, Array.from(years)[0]);
24503
+ }
24504
+ }
24505
+ } catch (error45) {
24506
+ logger?.debug(`Could not determine year journal: ${error45 instanceof Error ? error45.message : String(error45)}`);
24507
+ }
24508
+ return {
24509
+ existingAccounts: yearJournalPath ? loadExistingAccounts2(yearJournalPath) : [],
24510
+ rulesFilePath: rulesFile,
24511
+ existingRules: extractRulePatternsFromFile2(rulesFile),
24512
+ yearJournalPath,
24513
+ logger
24514
+ };
24515
+ }
24519
24516
  async function executeDryRunStep(context, contextId, logger) {
24520
24517
  logger?.startSection("Step 3: Dry Run Import");
24521
24518
  logger?.logStep("Dry Run", "start");
@@ -24539,39 +24536,8 @@ async function executeDryRunStep(context, contextId, logger) {
24539
24536
  }
24540
24537
  if (allUnknownPostings.length > 0) {
24541
24538
  try {
24542
- const {
24543
- suggestAccountsForPostingsBatch: suggestAccountsForPostingsBatch2,
24544
- loadExistingAccounts: loadExistingAccounts2,
24545
- extractRulePatternsFromFile: extractRulePatternsFromFile2
24546
- } = await Promise.resolve().then(() => (init_accountSuggester(), exports_accountSuggester));
24547
- const config2 = context.configLoader(context.directory);
24548
- const rulesDir = path12.join(context.directory, config2.paths.rules);
24549
- const importCtx = loadContext(context.directory, contextId);
24550
- const csvPath = path12.join(context.directory, importCtx.filePath);
24551
- const rulesMapping = loadRulesMapping(rulesDir);
24552
- let yearJournalPath;
24553
- let firstRulesFile;
24554
- const rulesFile = findRulesForCsv(csvPath, rulesMapping);
24555
- if (rulesFile) {
24556
- firstRulesFile = rulesFile;
24557
- try {
24558
- const result = await context.hledgerExecutor(["print", "-f", rulesFile]);
24559
- if (result.exitCode === 0) {
24560
- const years = extractTransactionYears(result.stdout);
24561
- if (years.size > 0) {
24562
- const transactionYear = Array.from(years)[0];
24563
- yearJournalPath = ensureYearJournalExists(context.directory, transactionYear);
24564
- }
24565
- }
24566
- } catch {}
24567
- }
24568
- const suggestionContext = {
24569
- existingAccounts: yearJournalPath ? loadExistingAccounts2(yearJournalPath) : [],
24570
- rulesFilePath: firstRulesFile,
24571
- existingRules: firstRulesFile ? extractRulePatternsFromFile2(firstRulesFile) : undefined,
24572
- yearJournalPath,
24573
- logger
24574
- };
24539
+ const { suggestAccountsForPostingsBatch: suggestAccountsForPostingsBatch2 } = await Promise.resolve().then(() => (init_accountSuggester(), exports_accountSuggester));
24540
+ const suggestionContext = await buildSuggestionContext(context, contextId, logger);
24575
24541
  postingsWithSuggestions = await suggestAccountsForPostingsBatch2(allUnknownPostings, suggestionContext);
24576
24542
  } catch (error45) {
24577
24543
  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.1-next.1",
3
+ "version": "0.5.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",