@fuzzle/opencode-accountant 0.5.1 → 0.5.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 +182 -237
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -2687,16 +2687,11 @@ function loadAgent(filePath) {
2687
2687
  throw new Error(`Invalid frontmatter format in ${filePath}`);
2688
2688
  }
2689
2689
  const data = jsYaml.load(match[1]);
2690
+ const { description, ...optional } = data;
2690
2691
  return {
2691
- description: data.description,
2692
+ description,
2692
2693
  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 }
2694
+ ...Object.fromEntries(Object.entries(optional).filter(([, v]) => v !== undefined))
2700
2695
  };
2701
2696
  }
2702
2697
  var init_agentLoader = __esm(() => {
@@ -4293,94 +4288,63 @@ function extractRulePatternsFromFile(rulesPath) {
4293
4288
  }
4294
4289
  return patterns;
4295
4290
  }
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
4291
+ function buildAccountHierarchySection(accounts) {
4292
+ if (accounts.length === 0)
4293
+ return "";
4294
+ return `## Existing Account Hierarchy
4305
4295
 
4306
- `;
4307
- prompt += context.existingAccounts.map((acc) => `- ${acc}`).join(`
4308
- `);
4309
- prompt += `
4296
+ ${accounts.map((acc) => `- ${acc}`).join(`
4297
+ `)}
4310
4298
 
4311
4299
  `;
4312
- }
4313
- if (context.existingRules && context.existingRules.length > 0) {
4314
- prompt += `## Example Classification Patterns from Rules
4300
+ }
4301
+ function buildRuleExamplesSection(rules) {
4302
+ if (!rules || rules.length === 0)
4303
+ return "";
4304
+ const sampleSize = Math.min(EXAMPLE_PATTERN_SAMPLE_SIZE, rules.length);
4305
+ const lines = rules.slice(0, sampleSize).map((p) => `- If description matches "${p.condition}" \u2192 ${p.account}`);
4306
+ return `## Example Classification Patterns from Rules
4315
4307
 
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
4308
+ ${lines.join(`
4309
+ `)}
4327
4310
 
4328
4311
  `;
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
- `;
4312
+ }
4313
+ function buildTransactionsSection(postings) {
4314
+ const lines = postings.map((posting, index) => {
4315
+ const parts = [
4316
+ `Transaction ${index + 1}:`,
4317
+ `- Type: ${posting.account === "income:unknown" ? "Income" : "Expense"}`,
4318
+ `- Date: ${posting.date}`,
4319
+ `- Description: ${posting.description}`,
4320
+ `- Amount: ${posting.amount}`
4321
+ ];
4340
4322
  if (posting.csvRow) {
4341
- prompt += `- CSV Data: ${JSON.stringify(posting.csvRow)}
4342
- `;
4323
+ parts.push(`- CSV Data: ${JSON.stringify(posting.csvRow)}`);
4343
4324
  }
4344
- prompt += `
4345
- `;
4325
+ return parts.join(`
4326
+ `);
4346
4327
  });
4347
- prompt += `## Task
4348
-
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
4328
+ return `## Transactions to Classify
4358
4329
 
4359
- `;
4360
- prompt += `Respond with suggestions for ALL transactions in this exact format:
4330
+ ${lines.join(`
4361
4331
 
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}
4332
+ `)}
4370
4333
 
4371
4334
  `;
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}
4379
-
4380
- `;
4381
- prompt += `... (continue for all transactions)
4382
- `;
4383
- return prompt;
4335
+ }
4336
+ function buildBatchSuggestionPrompt(postings, context) {
4337
+ return [
4338
+ `You are an accounting assistant helping categorize bank transactions.
4339
+ `,
4340
+ `I have ${postings.length} transaction(s) that need account classification:
4341
+ `,
4342
+ buildAccountHierarchySection(context.existingAccounts),
4343
+ buildRuleExamplesSection(context.existingRules),
4344
+ buildTransactionsSection(postings),
4345
+ RESPONSE_FORMAT_SECTION
4346
+ ].join(`
4347
+ `);
4384
4348
  }
4385
4349
  function parseBatchSuggestionResponse(response) {
4386
4350
  const suggestions = [];
@@ -4399,20 +4363,36 @@ function parseBatchSuggestionResponse(response) {
4399
4363
  }
4400
4364
  return suggestions;
4401
4365
  }
4402
- async function suggestAccountsForPostingsBatch(postings, context) {
4403
- if (postings.length === 0) {
4404
- return [];
4405
- }
4406
- const uncachedPostings = [];
4407
- const cachedResults = new Map;
4366
+ function partitionByCacheStatus(postings) {
4367
+ const uncached = [];
4368
+ const cached2 = new Map;
4408
4369
  postings.forEach((posting, index) => {
4409
4370
  const hash2 = hashTransaction(posting);
4410
4371
  if (suggestionCache[hash2]) {
4411
- cachedResults.set(index, suggestionCache[hash2]);
4372
+ cached2.set(index, suggestionCache[hash2]);
4412
4373
  } else {
4413
- uncachedPostings.push(posting);
4374
+ uncached.push(posting);
4414
4375
  }
4415
4376
  });
4377
+ return { uncached, cached: cached2 };
4378
+ }
4379
+ function mergeSuggestions(postings, cachedResults, newSuggestions) {
4380
+ let uncachedIndex = 0;
4381
+ return postings.map((posting, index) => {
4382
+ const suggestion = cachedResults.get(index) || newSuggestions[uncachedIndex++];
4383
+ return {
4384
+ ...posting,
4385
+ suggestedAccount: suggestion?.account,
4386
+ suggestionConfidence: suggestion?.confidence,
4387
+ suggestionReasoning: suggestion?.reasoning
4388
+ };
4389
+ });
4390
+ }
4391
+ async function suggestAccountsForPostingsBatch(postings, context) {
4392
+ if (postings.length === 0) {
4393
+ return [];
4394
+ }
4395
+ const { uncached: uncachedPostings, cached: cachedResults } = partitionByCacheStatus(postings);
4416
4396
  context.logger?.info(`Account suggestions: ${cachedResults.size} cached, ${uncachedPostings.length} to generate`);
4417
4397
  let newSuggestions = [];
4418
4398
  if (uncachedPostings.length > 0) {
@@ -4445,19 +4425,7 @@ ${userPrompt}`;
4445
4425
  return postings;
4446
4426
  }
4447
4427
  }
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;
4428
+ return mergeSuggestions(postings, cachedResults, newSuggestions);
4461
4429
  }
4462
4430
  function generateMockSuggestions(postings) {
4463
4431
  let response = "";
@@ -4492,10 +4460,34 @@ function generateMockSuggestions(postings) {
4492
4460
  });
4493
4461
  return response;
4494
4462
  }
4495
- var EXAMPLE_PATTERN_SAMPLE_SIZE = 10, suggestionCache;
4463
+ var EXAMPLE_PATTERN_SAMPLE_SIZE = 10, suggestionCache, RESPONSE_FORMAT_SECTION;
4496
4464
  var init_accountSuggester = __esm(() => {
4497
4465
  init_agentLoader();
4498
4466
  suggestionCache = {};
4467
+ RESPONSE_FORMAT_SECTION = [
4468
+ `## Task
4469
+ `,
4470
+ "For EACH transaction, suggest the most appropriate account. You may:",
4471
+ "1. Suggest an existing account from the hierarchy above",
4472
+ `2. Propose a NEW account following the existing naming patterns
4473
+ `,
4474
+ `## Response Format
4475
+ `,
4476
+ `Respond with suggestions for ALL transactions in this exact format:
4477
+ `,
4478
+ "TRANSACTION 1:",
4479
+ "ACCOUNT: {account_name}",
4480
+ "CONFIDENCE: {high|medium|low}",
4481
+ `REASONING: {brief one-sentence explanation}
4482
+ `,
4483
+ "TRANSACTION 2:",
4484
+ "ACCOUNT: {account_name}",
4485
+ "CONFIDENCE: {high|medium|low}",
4486
+ `REASONING: {brief one-sentence explanation}
4487
+ `,
4488
+ "... (continue for all transactions)"
4489
+ ].join(`
4490
+ `);
4499
4491
  });
4500
4492
 
4501
4493
  // src/index.ts
@@ -16920,13 +16912,7 @@ function findCsvFiles(baseDir, options = {}) {
16920
16912
  if (!fs3.existsSync(baseDir)) {
16921
16913
  return [];
16922
16914
  }
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
- }
16915
+ const searchDir = path2.join(...[baseDir, options.subdir, options.subsubdir].filter((p) => !!p));
16930
16916
  if (!fs3.existsSync(searchDir)) {
16931
16917
  return [];
16932
16918
  }
@@ -17079,9 +17065,6 @@ function buildPricehistArgs(startDate, endDate, currencyConfig) {
17079
17065
  }
17080
17066
  return cmdArgs;
17081
17067
  }
17082
- function buildErrorResult(error45) {
17083
- return buildToolErrorResult(error45);
17084
- }
17085
17068
  function buildSuccessResult(results, endDate, backfill) {
17086
17069
  return buildToolSuccessResult({
17087
17070
  success: results.every((r) => !("error" in r)),
@@ -17116,7 +17099,7 @@ async function fetchCurrencyPrices(directory, agent, backfill, priceFetcher = de
17116
17099
  config2 = configLoader(directory);
17117
17100
  } catch (err) {
17118
17101
  const errorMessage = err instanceof Error ? err.message : String(err);
17119
- return buildErrorResult(errorMessage);
17102
+ return buildToolErrorResult(errorMessage);
17120
17103
  }
17121
17104
  const endDate = getYesterday();
17122
17105
  const defaultBackfillDate = getDefaultBackfillDate();
@@ -17433,28 +17416,15 @@ import { randomUUID } from "crypto";
17433
17416
  function getContextPath(directory, contextId) {
17434
17417
  return path5.join(directory, ".memory", `${contextId}.json`);
17435
17418
  }
17436
- function ensureMemoryDir(directory) {
17437
- ensureDirectory(path5.join(directory, ".memory"));
17438
- }
17439
17419
  function createContext(directory, params) {
17440
17420
  const now = new Date().toISOString();
17441
17421
  const context = {
17442
17422
  id: randomUUID(),
17443
17423
  createdAt: now,
17444
17424
  updatedAt: now,
17445
- filename: params.filename,
17446
- filePath: params.filePath,
17447
- provider: params.provider,
17448
- currency: params.currency,
17449
- accountNumber: params.accountNumber,
17450
- originalFilename: params.originalFilename,
17451
- fromDate: params.fromDate,
17452
- untilDate: params.untilDate,
17453
- openingBalance: params.openingBalance,
17454
- closingBalance: params.closingBalance,
17455
- account: params.account
17456
- };
17457
- ensureMemoryDir(directory);
17425
+ ...params
17426
+ };
17427
+ ensureDirectory(path5.join(directory, ".memory"));
17458
17428
  const contextPath = getContextPath(directory, context.id);
17459
17429
  fs5.writeFileSync(contextPath, JSON.stringify(context, null, 2), "utf-8");
17460
17430
  return context;
@@ -17515,20 +17485,6 @@ function buildSuccessResult2(classified, unrecognized, message) {
17515
17485
  }
17516
17486
  });
17517
17487
  }
17518
- function buildErrorResult2(error45, hint) {
17519
- return buildToolErrorResult(error45, hint, {
17520
- classified: [],
17521
- unrecognized: []
17522
- });
17523
- }
17524
- function buildCollisionError(collisions) {
17525
- const error45 = `Cannot classify: ${collisions.length} file(s) would overwrite existing pending files.`;
17526
- return buildToolErrorResult(error45, undefined, {
17527
- collisions,
17528
- classified: [],
17529
- unrecognized: []
17530
- });
17531
- }
17532
17488
  function planMoves(csvFiles, importsDir, pendingDir, unrecognizedDir, config2) {
17533
17489
  const plannedMoves = [];
17534
17490
  const collisions = [];
@@ -17629,7 +17585,10 @@ async function classifyStatements(directory, agent, configLoader = loadImportCon
17629
17585
  config2 = configLoader(directory);
17630
17586
  } catch (err) {
17631
17587
  const errorMessage = err instanceof Error ? err.message : String(err);
17632
- return buildErrorResult2(errorMessage);
17588
+ return buildToolErrorResult(errorMessage, undefined, {
17589
+ classified: [],
17590
+ unrecognized: []
17591
+ });
17633
17592
  }
17634
17593
  const importsDir = path6.join(directory, config2.paths.import);
17635
17594
  const pendingDir = path6.join(directory, config2.paths.pending);
@@ -17640,7 +17599,12 @@ async function classifyStatements(directory, agent, configLoader = loadImportCon
17640
17599
  }
17641
17600
  const { plannedMoves, collisions } = planMoves(csvFiles, importsDir, pendingDir, unrecognizedDir, config2);
17642
17601
  if (collisions.length > 0) {
17643
- return buildCollisionError(collisions);
17602
+ const error45 = `Cannot classify: ${collisions.length} file(s) would overwrite existing pending files.`;
17603
+ return buildToolErrorResult(error45, undefined, {
17604
+ collisions,
17605
+ classified: [],
17606
+ unrecognized: []
17607
+ });
17644
17608
  }
17645
17609
  const { classified, unrecognized } = executeMoves(plannedMoves, config2, unrecognizedDir, directory);
17646
17610
  return buildSuccessResult2(classified, unrecognized);
@@ -23025,6 +22989,7 @@ function findRulesForCsv(csvPath, mapping) {
23025
22989
  // src/utils/hledgerExecutor.ts
23026
22990
  var {$: $2 } = globalThis.Bun;
23027
22991
  var STDERR_TRUNCATE_LENGTH = 500;
22992
+ var TX_HEADER_PATTERN = /^(\d{4})-(\d{2}-\d{2})\s+(.+)$/;
23028
22993
  async function defaultHledgerExecutor(cmdArgs) {
23029
22994
  try {
23030
22995
  const result = await $2`hledger ${cmdArgs}`.quiet().nothrow();
@@ -23059,10 +23024,10 @@ function parseUnknownPostings(hledgerOutput) {
23059
23024
  let currentDate = "";
23060
23025
  let currentDescription = "";
23061
23026
  for (const line of lines) {
23062
- const headerMatch = line.match(/^(\d{4}-\d{2}-\d{2})\s+(.+)$/);
23027
+ const headerMatch = line.match(TX_HEADER_PATTERN);
23063
23028
  if (headerMatch) {
23064
- currentDate = headerMatch[1];
23065
- currentDescription = headerMatch[2].trim();
23029
+ currentDate = `${headerMatch[1]}-${headerMatch[2]}`;
23030
+ currentDescription = headerMatch[3].trim();
23066
23031
  continue;
23067
23032
  }
23068
23033
  const postingMatch = line.match(/^\s+(income:unknown|expenses:unknown)\s+([^\s]+(?:\s+[^\s=]+)?)\s*(?:=\s*(.+))?$/);
@@ -23083,7 +23048,7 @@ function countTransactions(hledgerOutput) {
23083
23048
  `);
23084
23049
  let count = 0;
23085
23050
  for (const line of lines) {
23086
- if (/^\d{4}-\d{2}-\d{2}\s+/.test(line)) {
23051
+ if (TX_HEADER_PATTERN.test(line)) {
23087
23052
  count++;
23088
23053
  }
23089
23054
  }
@@ -23094,7 +23059,7 @@ function extractTransactionYears(hledgerOutput) {
23094
23059
  const lines = hledgerOutput.split(`
23095
23060
  `);
23096
23061
  for (const line of lines) {
23097
- const match2 = line.match(/^(\d{4})-\d{2}-\d{2}\s+/);
23062
+ const match2 = line.match(TX_HEADER_PATTERN);
23098
23063
  if (match2) {
23099
23064
  years.add(parseInt(match2[1], 10));
23100
23065
  }
@@ -23154,6 +23119,13 @@ async function getAccountBalance(mainJournalPath, account, asOfDate, executor =
23154
23119
 
23155
23120
  // src/utils/rulesParser.ts
23156
23121
  import * as fs8 from "fs";
23122
+ function resolveFieldRef(fieldRef, fieldNames) {
23123
+ if (/^\d+$/.test(fieldRef)) {
23124
+ const index = parseInt(fieldRef, 10) - 1;
23125
+ return fieldNames[index] || fieldRef;
23126
+ }
23127
+ return fieldRef;
23128
+ }
23157
23129
  function parseSkipRows(rulesContent) {
23158
23130
  const match2 = rulesContent.match(/^skip\s+(\d+)/m);
23159
23131
  return match2 ? parseInt(match2[1], 10) : 0;
@@ -23178,24 +23150,13 @@ function parseDateField(rulesContent, fieldNames) {
23178
23150
  if (!match2) {
23179
23151
  return fieldNames[0] || "date";
23180
23152
  }
23181
- const value = match2[1];
23182
- if (/^\d+$/.test(value)) {
23183
- const index = parseInt(value, 10) - 1;
23184
- return fieldNames[index] || value;
23185
- }
23186
- return value;
23153
+ return resolveFieldRef(match2[1], fieldNames);
23187
23154
  }
23188
23155
  function parseAmountFields(rulesContent, fieldNames) {
23189
23156
  const result = {};
23190
23157
  const simpleMatch = rulesContent.match(/^amount\s+(-?)%(\w+|\d+)/m);
23191
23158
  if (simpleMatch) {
23192
- const fieldRef = simpleMatch[2];
23193
- if (/^\d+$/.test(fieldRef)) {
23194
- const index = parseInt(fieldRef, 10) - 1;
23195
- result.single = fieldNames[index] || fieldRef;
23196
- } else {
23197
- result.single = fieldRef;
23198
- }
23159
+ result.single = resolveFieldRef(simpleMatch[2], fieldNames);
23199
23160
  }
23200
23161
  const debitMatch = rulesContent.match(/if\s+%(\w+)\s+\.\s*\n\s*amount\s+-?%\1/m);
23201
23162
  if (debitMatch) {
@@ -23260,15 +23221,18 @@ function parseBalance(balance) {
23260
23221
  const amount = parseFloat(amountStr.replace(/,/g, ""));
23261
23222
  return { currency, amount };
23262
23223
  }
23224
+ function validateCurrencies(a, b) {
23225
+ if (a.currency && b.currency && a.currency !== b.currency) {
23226
+ throw new Error(`Currency mismatch: ${a.currency} vs ${b.currency}`);
23227
+ }
23228
+ }
23263
23229
  function calculateDifference(expected, actual) {
23264
23230
  const expectedParsed = parseBalance(expected);
23265
23231
  const actualParsed = parseBalance(actual);
23266
23232
  if (!expectedParsed || !actualParsed) {
23267
23233
  throw new Error(`Cannot parse balances: expected="${expected}", actual="${actual}"`);
23268
23234
  }
23269
- if (expectedParsed.currency && actualParsed.currency && expectedParsed.currency !== actualParsed.currency) {
23270
- throw new Error(`Currency mismatch: expected ${expectedParsed.currency}, got ${actualParsed.currency}`);
23271
- }
23235
+ validateCurrencies(expectedParsed, actualParsed);
23272
23236
  const diff = actualParsed.amount - expectedParsed.amount;
23273
23237
  const sign = diff >= 0 ? "+" : "";
23274
23238
  const currency = expectedParsed.currency || actualParsed.currency;
@@ -23280,9 +23244,7 @@ function balancesMatch(balance1, balance2) {
23280
23244
  if (!parsed1 || !parsed2) {
23281
23245
  return false;
23282
23246
  }
23283
- if (parsed1.currency && parsed2.currency && parsed1.currency !== parsed2.currency) {
23284
- throw new Error(`Currency mismatch: ${parsed1.currency} vs ${parsed2.currency}`);
23285
- }
23247
+ validateCurrencies(parsed1, parsed2);
23286
23248
  return parsed1.amount === parsed2.amount;
23287
23249
  }
23288
23250
 
@@ -23375,8 +23337,7 @@ function looksLikeTransactionId(fieldName, value) {
23375
23337
  if (!nameMatches)
23376
23338
  return false;
23377
23339
  const trimmedValue = value.trim();
23378
- const looksLikeId = /^[A-Za-z0-9_-]+$/.test(trimmedValue) && trimmedValue.length >= 3;
23379
- return looksLikeId;
23340
+ return /^[A-Za-z0-9_-]+$/.test(trimmedValue) && trimmedValue.length >= 3;
23380
23341
  }
23381
23342
  function findTransactionId(row) {
23382
23343
  for (const [field, value] of Object.entries(row)) {
@@ -23393,9 +23354,7 @@ function findMatchingCsvRow(posting, csvRows, config2) {
23393
23354
  const rowAmount = getRowAmount(row, config2.amountFields);
23394
23355
  if (rowDate !== posting.date)
23395
23356
  return false;
23396
- if (Math.abs(rowAmount - postingAmount) > AMOUNT_MATCH_TOLERANCE)
23397
- return false;
23398
- return true;
23357
+ return Math.abs(rowAmount - postingAmount) <= AMOUNT_MATCH_TOLERANCE;
23399
23358
  });
23400
23359
  if (candidates.length === 1) {
23401
23360
  return candidates[0];
@@ -23416,17 +23375,11 @@ function findMatchingCsvRow(posting, csvRows, config2) {
23416
23375
  const descMatches = candidates.filter((row) => {
23417
23376
  return Object.values(row).some((value) => value && value.toLowerCase().includes(descriptionLower));
23418
23377
  });
23419
- if (descMatches.length === 1) {
23420
- return descMatches[0];
23421
- }
23422
- if (descMatches.length > 1) {
23423
- return descMatches[0];
23424
- }
23425
- return candidates[0];
23378
+ return descMatches[0] || candidates[0];
23426
23379
  }
23427
23380
 
23428
23381
  // src/tools/import-statements.ts
23429
- function buildErrorResult3(error45, hint) {
23382
+ function buildErrorResult(error45, hint) {
23430
23383
  return buildToolErrorResult(error45, hint, {
23431
23384
  files: [],
23432
23385
  summary: {
@@ -23597,7 +23550,7 @@ async function importStatements(directory, agent, options, configLoader = loadIm
23597
23550
  config2 = configLoader(directory);
23598
23551
  } catch (error45) {
23599
23552
  const errorMessage = `Failed to load configuration: ${error45 instanceof Error ? error45.message : String(error45)}`;
23600
- return buildErrorResult3(errorMessage, 'Ensure config/import/providers.yaml exists with required paths including "rules"');
23553
+ return buildErrorResult(errorMessage, 'Ensure config/import/providers.yaml exists with required paths including "rules"');
23601
23554
  }
23602
23555
  const pendingDir = path9.join(directory, config2.paths.pending);
23603
23556
  const rulesDir = path9.join(directory, config2.paths.rules);
@@ -23606,7 +23559,7 @@ async function importStatements(directory, agent, options, configLoader = loadIm
23606
23559
  const importContext = loadContext(directory, options.contextId);
23607
23560
  const csvPath = path9.join(directory, importContext.filePath);
23608
23561
  if (!fs10.existsSync(csvPath)) {
23609
- return buildErrorResult3(`CSV file not found: ${importContext.filePath}`, "The file may have been moved or deleted");
23562
+ return buildErrorResult(`CSV file not found: ${importContext.filePath}`, "The file may have been moved or deleted");
23610
23563
  }
23611
23564
  const csvFiles = [csvPath];
23612
23565
  const fileResults = [];
@@ -23758,7 +23711,7 @@ Note: This tool is typically called via import-pipeline for the full workflow.`,
23758
23711
  // src/tools/reconcile-statement.ts
23759
23712
  import * as fs11 from "fs";
23760
23713
  import * as path10 from "path";
23761
- function buildErrorResult4(params) {
23714
+ function buildErrorResult2(params) {
23762
23715
  return buildToolErrorResult(params.error, params.hint, {
23763
23716
  account: params.account ?? "",
23764
23717
  expectedBalance: params.expectedBalance ?? "",
@@ -23775,7 +23728,7 @@ function loadConfiguration(directory, configLoader) {
23775
23728
  return { config: config2 };
23776
23729
  } catch (error45) {
23777
23730
  return {
23778
- error: buildErrorResult4({
23731
+ error: buildErrorResult2({
23779
23732
  error: `Failed to load configuration: ${error45 instanceof Error ? error45.message : String(error45)}`,
23780
23733
  hint: "Ensure config/import/providers.yaml exists"
23781
23734
  })
@@ -23786,7 +23739,7 @@ function verifyCsvExists(directory, importContext) {
23786
23739
  const csvFile = path10.join(directory, importContext.filePath);
23787
23740
  if (!fs11.existsSync(csvFile)) {
23788
23741
  return {
23789
- error: buildErrorResult4({
23742
+ error: buildErrorResult2({
23790
23743
  error: `CSV file not found: ${importContext.filePath}`,
23791
23744
  hint: `The file may have been moved or deleted. Context ID: ${importContext.id}`
23792
23745
  })
@@ -23853,7 +23806,7 @@ function determineClosingBalance(csvFile, config2, importContext, manualClosingB
23853
23806
  const exampleBalance = `${currency} <amount>`;
23854
23807
  const retryCmd = buildRetryCommand(importContext.id, exampleBalance);
23855
23808
  return {
23856
- error: buildErrorResult4({
23809
+ error: buildErrorResult2({
23857
23810
  csvFile: relativeCsvPath,
23858
23811
  error: "No closing balance found in CSV metadata or data",
23859
23812
  hint: `Provide closingBalance parameter manually. Example retry: ${retryCmd}`,
@@ -23883,7 +23836,7 @@ function determineAccount(csvFile, rulesDir, importContext, manualAccount, relat
23883
23836
  if (!account) {
23884
23837
  const rulesHint = rulesFile ? `Add 'account1 assets:bank:...' to ${rulesFile} or retry with: ${buildRetryCommand(importContext.id, undefined, "assets:bank:...")}` : `Create a rules file in ${rulesDir} with 'account1' directive or retry with: ${buildRetryCommand(importContext.id, undefined, "assets:bank:...")}`;
23885
23838
  return {
23886
- error: buildErrorResult4({
23839
+ error: buildErrorResult2({
23887
23840
  csvFile: relativeCsvPath,
23888
23841
  error: "Could not determine account from rules file",
23889
23842
  hint: rulesHint,
@@ -23966,7 +23919,7 @@ async function reconcileStatement(directory, agent, options, configLoader = load
23966
23919
  try {
23967
23920
  importContext = loadContext(directory, options.contextId);
23968
23921
  } catch {
23969
- return buildErrorResult4({
23922
+ return buildErrorResult2({
23970
23923
  error: `Failed to load import context: ${options.contextId}`,
23971
23924
  hint: "Ensure the context ID is valid and the context file exists in .memory/"
23972
23925
  });
@@ -23995,7 +23948,7 @@ async function reconcileStatement(directory, agent, options, configLoader = load
23995
23948
  const { account } = accountResult;
23996
23949
  const lastTransactionDate = await getLastTransactionDate(mainJournalPath, account, hledgerExecutor);
23997
23950
  if (!lastTransactionDate) {
23998
- return buildErrorResult4({
23951
+ return buildErrorResult2({
23999
23952
  csvFile: relativeCsvPath,
24000
23953
  account,
24001
23954
  error: "No transactions found for account",
@@ -24005,7 +23958,7 @@ async function reconcileStatement(directory, agent, options, configLoader = load
24005
23958
  }
24006
23959
  const actualBalance = await getAccountBalance(mainJournalPath, account, lastTransactionDate, hledgerExecutor);
24007
23960
  if (actualBalance === null) {
24008
- return buildErrorResult4({
23961
+ return buildErrorResult2({
24009
23962
  csvFile: relativeCsvPath,
24010
23963
  account,
24011
23964
  lastTransactionDate,
@@ -24018,7 +23971,7 @@ async function reconcileStatement(directory, agent, options, configLoader = load
24018
23971
  try {
24019
23972
  doBalancesMatch = balancesMatch(closingBalance, actualBalance);
24020
23973
  } catch (error45) {
24021
- return buildErrorResult4({
23974
+ return buildErrorResult2({
24022
23975
  csvFile: relativeCsvPath,
24023
23976
  account,
24024
23977
  lastTransactionDate,
@@ -24047,7 +24000,7 @@ async function reconcileStatement(directory, agent, options, configLoader = load
24047
24000
  try {
24048
24001
  difference = calculateDifference(closingBalance, actualBalance);
24049
24002
  } catch (error45) {
24050
- return buildErrorResult4({
24003
+ return buildErrorResult2({
24051
24004
  csvFile: relativeCsvPath,
24052
24005
  account,
24053
24006
  lastTransactionDate,
@@ -24057,7 +24010,7 @@ async function reconcileStatement(directory, agent, options, configLoader = load
24057
24010
  metadata
24058
24011
  });
24059
24012
  }
24060
- return buildErrorResult4({
24013
+ return buildErrorResult2({
24061
24014
  csvFile: relativeCsvPath,
24062
24015
  account,
24063
24016
  lastTransactionDate,
@@ -24130,7 +24083,6 @@ function extractAccountsFromRulesFile(rulesPath) {
24130
24083
  const account2Match = trimmed.match(/account2\s+(.+?)(?:\s+|$)/);
24131
24084
  if (account2Match) {
24132
24085
  accounts.add(account2Match[1].trim());
24133
- continue;
24134
24086
  }
24135
24087
  }
24136
24088
  return accounts;
@@ -24148,52 +24100,46 @@ function getAllAccountsFromRules(rulesPaths) {
24148
24100
  function sortAccountDeclarations(accounts) {
24149
24101
  return Array.from(accounts).sort((a, b) => a.localeCompare(b));
24150
24102
  }
24151
- function ensureAccountDeclarations(yearJournalPath, accounts) {
24152
- if (!fs12.existsSync(yearJournalPath)) {
24153
- throw new Error(`Year journal not found: ${yearJournalPath}`);
24154
- }
24155
- const content = fs12.readFileSync(yearJournalPath, "utf-8");
24103
+ function parseJournalSections(content) {
24156
24104
  const lines = content.split(`
24157
24105
  `);
24158
24106
  const existingAccounts = new Set;
24159
24107
  const commentLines = [];
24160
- const accountLines = [];
24161
24108
  const otherLines = [];
24162
24109
  let inAccountSection = false;
24163
24110
  let accountSectionEnded = false;
24164
24111
  for (const line of lines) {
24165
24112
  const trimmed = line.trim();
24166
24113
  if (trimmed.startsWith(";") || trimmed.startsWith("#")) {
24167
- if (!accountSectionEnded) {
24168
- commentLines.push(line);
24169
- } else {
24170
- otherLines.push(line);
24171
- }
24114
+ (accountSectionEnded ? otherLines : commentLines).push(line);
24172
24115
  continue;
24173
24116
  }
24174
24117
  if (trimmed.startsWith("account ")) {
24175
24118
  inAccountSection = true;
24176
24119
  const accountMatch = trimmed.match(/^account\s+(.+?)(?:\s+|$)/);
24177
24120
  if (accountMatch) {
24178
- const accountName = accountMatch[1].trim();
24179
- existingAccounts.add(accountName);
24180
- accountLines.push(line);
24121
+ existingAccounts.add(accountMatch[1].trim());
24181
24122
  }
24182
24123
  continue;
24183
24124
  }
24184
24125
  if (trimmed === "") {
24185
- if (inAccountSection && !accountSectionEnded) {
24186
- accountLines.push(line);
24187
- } else {
24188
- otherLines.push(line);
24189
- }
24126
+ if (inAccountSection && !accountSectionEnded)
24127
+ continue;
24128
+ otherLines.push(line);
24190
24129
  continue;
24191
24130
  }
24192
- if (inAccountSection) {
24131
+ if (inAccountSection)
24193
24132
  accountSectionEnded = true;
24194
- }
24195
24133
  otherLines.push(line);
24196
24134
  }
24135
+ return { existingAccounts, commentLines, otherLines };
24136
+ }
24137
+ function ensureAccountDeclarations(yearJournalPath, accounts) {
24138
+ if (!fs12.existsSync(yearJournalPath)) {
24139
+ throw new Error(`Year journal not found: ${yearJournalPath}`);
24140
+ }
24141
+ const content = fs12.readFileSync(yearJournalPath, "utf-8");
24142
+ const { existingAccounts, commentLines, otherLines } = parseJournalSections(content);
24197
24143
  const missingAccounts = new Set;
24198
24144
  for (const account of accounts) {
24199
24145
  if (!existingAccounts.has(account)) {
@@ -24259,14 +24205,10 @@ class MarkdownLogger {
24259
24205
  }
24260
24206
  }
24261
24207
  info(message) {
24262
- this.buffer.push(message);
24263
- if (this.autoFlush)
24264
- this.flushAsync();
24208
+ this.log(message);
24265
24209
  }
24266
24210
  warn(message) {
24267
- this.buffer.push(`\u26A0\uFE0F **WARNING**: ${message}`);
24268
- if (this.autoFlush)
24269
- this.flushAsync();
24211
+ this.log(`\u26A0\uFE0F **WARNING**: ${message}`);
24270
24212
  }
24271
24213
  error(message, error45) {
24272
24214
  this.buffer.push(`\u274C **ERROR**: ${message}`);
@@ -24282,13 +24224,10 @@ class MarkdownLogger {
24282
24224
  this.buffer.push("```");
24283
24225
  this.buffer.push("");
24284
24226
  }
24285
- if (this.autoFlush)
24286
- this.flushAsync();
24227
+ this.autoFlushIfEnabled();
24287
24228
  }
24288
24229
  debug(message) {
24289
- this.buffer.push(`\uD83D\uDD0D ${message}`);
24290
- if (this.autoFlush)
24291
- this.flushAsync();
24230
+ this.log(`\uD83D\uDD0D ${message}`);
24292
24231
  }
24293
24232
  logStep(stepName, status, details) {
24294
24233
  const icon = status === "success" ? "\u2705" : status === "error" ? "\u274C" : "\u25B6\uFE0F";
@@ -24298,8 +24237,7 @@ class MarkdownLogger {
24298
24237
  this.buffer.push(` ${details}`);
24299
24238
  }
24300
24239
  this.buffer.push("");
24301
- if (this.autoFlush)
24302
- this.flushAsync();
24240
+ this.autoFlushIfEnabled();
24303
24241
  }
24304
24242
  logCommand(command, output) {
24305
24243
  this.buffer.push("```bash");
@@ -24317,16 +24255,14 @@ class MarkdownLogger {
24317
24255
  }
24318
24256
  this.buffer.push("```");
24319
24257
  this.buffer.push("");
24320
- if (this.autoFlush)
24321
- this.flushAsync();
24258
+ this.autoFlushIfEnabled();
24322
24259
  }
24323
24260
  logResult(data) {
24324
24261
  this.buffer.push("```json");
24325
24262
  this.buffer.push(JSON.stringify(data, null, 2));
24326
24263
  this.buffer.push("```");
24327
24264
  this.buffer.push("");
24328
- if (this.autoFlush)
24329
- this.flushAsync();
24265
+ this.autoFlushIfEnabled();
24330
24266
  }
24331
24267
  setContext(key, value) {
24332
24268
  this.context[key] = value;
@@ -24346,6 +24282,15 @@ class MarkdownLogger {
24346
24282
  getLogPath() {
24347
24283
  return this.logPath;
24348
24284
  }
24285
+ log(message) {
24286
+ this.buffer.push(message);
24287
+ this.autoFlushIfEnabled();
24288
+ }
24289
+ autoFlushIfEnabled() {
24290
+ if (!this.autoFlush)
24291
+ return;
24292
+ this.flushAsync();
24293
+ }
24349
24294
  flushAsync() {
24350
24295
  this.pendingFlush = this.flush().catch(() => {});
24351
24296
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fuzzle/opencode-accountant",
3
- "version": "0.5.1",
3
+ "version": "0.5.2",
4
4
  "description": "An OpenCode accounting agent, specialized in double-entry-bookkepping with hledger",
5
5
  "author": {
6
6
  "name": "ali bengali",