@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.
- package/dist/index.js +223 -257
- 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
|
|
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
|
|
2696
|
+
description,
|
|
2692
2697
|
prompt: match[2].trim(),
|
|
2693
|
-
...
|
|
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
|
|
4297
|
-
|
|
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
|
-
|
|
4308
|
-
`);
|
|
4309
|
-
prompt += `
|
|
4300
|
+
${accounts.map((acc) => `- ${acc}`).join(`
|
|
4301
|
+
`)}
|
|
4310
4302
|
|
|
4311
4303
|
`;
|
|
4312
|
-
|
|
4313
|
-
|
|
4314
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4330
|
-
|
|
4331
|
-
|
|
4332
|
-
|
|
4333
|
-
|
|
4334
|
-
|
|
4335
|
-
|
|
4336
|
-
|
|
4337
|
-
|
|
4338
|
-
|
|
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
|
-
|
|
4342
|
-
`;
|
|
4327
|
+
parts.push(`- CSV Data: ${JSON.stringify(posting.csvRow)}`);
|
|
4343
4328
|
}
|
|
4344
|
-
|
|
4345
|
-
|
|
4329
|
+
return parts.join(`
|
|
4330
|
+
`);
|
|
4346
4331
|
});
|
|
4347
|
-
|
|
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
|
-
|
|
4382
|
-
|
|
4383
|
-
return
|
|
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
|
-
|
|
4403
|
-
|
|
4404
|
-
|
|
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
|
-
|
|
4376
|
+
cached2.set(index, suggestionCache[hash2]);
|
|
4412
4377
|
} else {
|
|
4413
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
23038
|
+
const headerMatch = line.match(TX_HEADER_PATTERN);
|
|
23051
23039
|
if (headerMatch) {
|
|
23052
|
-
currentDate = headerMatch[1]
|
|
23053
|
-
currentDescription = headerMatch[
|
|
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 (
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
23439
|
-
if (!
|
|
23423
|
+
const sourcePath = parseSourceDirective(content);
|
|
23424
|
+
if (!sourcePath) {
|
|
23440
23425
|
return null;
|
|
23441
23426
|
}
|
|
23442
|
-
const
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
24175
|
-
|
|
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.
|
|
24251
|
-
if (this.autoFlush)
|
|
24252
|
-
this.flushAsync();
|
|
24216
|
+
this.log(message);
|
|
24253
24217
|
}
|
|
24254
24218
|
warn(message) {
|
|
24255
|
-
this.
|
|
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
|
-
|
|
24274
|
-
this.flushAsync();
|
|
24235
|
+
this.autoFlushIfEnabled();
|
|
24275
24236
|
}
|
|
24276
24237
|
debug(message) {
|
|
24277
|
-
this.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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