@fuzzle/opencode-accountant 0.5.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 +250 -296
- 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
|
|
4334
|
+
${lines.join(`
|
|
4358
4335
|
|
|
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}
|
|
4370
|
-
|
|
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 });
|
|
@@ -17079,9 +17076,6 @@ function buildPricehistArgs(startDate, endDate, currencyConfig) {
|
|
|
17079
17076
|
}
|
|
17080
17077
|
return cmdArgs;
|
|
17081
17078
|
}
|
|
17082
|
-
function buildErrorResult(error45) {
|
|
17083
|
-
return buildToolErrorResult(error45);
|
|
17084
|
-
}
|
|
17085
17079
|
function buildSuccessResult(results, endDate, backfill) {
|
|
17086
17080
|
return buildToolSuccessResult({
|
|
17087
17081
|
success: results.every((r) => !("error" in r)),
|
|
@@ -17116,7 +17110,7 @@ async function fetchCurrencyPrices(directory, agent, backfill, priceFetcher = de
|
|
|
17116
17110
|
config2 = configLoader(directory);
|
|
17117
17111
|
} catch (err) {
|
|
17118
17112
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
17119
|
-
return
|
|
17113
|
+
return buildToolErrorResult(errorMessage);
|
|
17120
17114
|
}
|
|
17121
17115
|
const endDate = getYesterday();
|
|
17122
17116
|
const defaultBackfillDate = getDefaultBackfillDate();
|
|
@@ -17433,28 +17427,15 @@ import { randomUUID } from "crypto";
|
|
|
17433
17427
|
function getContextPath(directory, contextId) {
|
|
17434
17428
|
return path5.join(directory, ".memory", `${contextId}.json`);
|
|
17435
17429
|
}
|
|
17436
|
-
function ensureMemoryDir(directory) {
|
|
17437
|
-
ensureDirectory(path5.join(directory, ".memory"));
|
|
17438
|
-
}
|
|
17439
17430
|
function createContext(directory, params) {
|
|
17440
17431
|
const now = new Date().toISOString();
|
|
17441
17432
|
const context = {
|
|
17442
17433
|
id: randomUUID(),
|
|
17443
17434
|
createdAt: now,
|
|
17444
17435
|
updatedAt: now,
|
|
17445
|
-
|
|
17446
|
-
|
|
17447
|
-
|
|
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);
|
|
17436
|
+
...params
|
|
17437
|
+
};
|
|
17438
|
+
ensureDirectory(path5.join(directory, ".memory"));
|
|
17458
17439
|
const contextPath = getContextPath(directory, context.id);
|
|
17459
17440
|
fs5.writeFileSync(contextPath, JSON.stringify(context, null, 2), "utf-8");
|
|
17460
17441
|
return context;
|
|
@@ -17515,20 +17496,6 @@ function buildSuccessResult2(classified, unrecognized, message) {
|
|
|
17515
17496
|
}
|
|
17516
17497
|
});
|
|
17517
17498
|
}
|
|
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
17499
|
function planMoves(csvFiles, importsDir, pendingDir, unrecognizedDir, config2) {
|
|
17533
17500
|
const plannedMoves = [];
|
|
17534
17501
|
const collisions = [];
|
|
@@ -17629,7 +17596,10 @@ async function classifyStatements(directory, agent, configLoader = loadImportCon
|
|
|
17629
17596
|
config2 = configLoader(directory);
|
|
17630
17597
|
} catch (err) {
|
|
17631
17598
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
17632
|
-
return
|
|
17599
|
+
return buildToolErrorResult(errorMessage, undefined, {
|
|
17600
|
+
classified: [],
|
|
17601
|
+
unrecognized: []
|
|
17602
|
+
});
|
|
17633
17603
|
}
|
|
17634
17604
|
const importsDir = path6.join(directory, config2.paths.import);
|
|
17635
17605
|
const pendingDir = path6.join(directory, config2.paths.pending);
|
|
@@ -17640,7 +17610,12 @@ async function classifyStatements(directory, agent, configLoader = loadImportCon
|
|
|
17640
17610
|
}
|
|
17641
17611
|
const { plannedMoves, collisions } = planMoves(csvFiles, importsDir, pendingDir, unrecognizedDir, config2);
|
|
17642
17612
|
if (collisions.length > 0) {
|
|
17643
|
-
|
|
17613
|
+
const error45 = `Cannot classify: ${collisions.length} file(s) would overwrite existing pending files.`;
|
|
17614
|
+
return buildToolErrorResult(error45, undefined, {
|
|
17615
|
+
collisions,
|
|
17616
|
+
classified: [],
|
|
17617
|
+
unrecognized: []
|
|
17618
|
+
});
|
|
17644
17619
|
}
|
|
17645
17620
|
const { classified, unrecognized } = executeMoves(plannedMoves, config2, unrecognizedDir, directory);
|
|
17646
17621
|
return buildSuccessResult2(classified, unrecognized);
|
|
@@ -23025,6 +23000,7 @@ function findRulesForCsv(csvPath, mapping) {
|
|
|
23025
23000
|
// src/utils/hledgerExecutor.ts
|
|
23026
23001
|
var {$: $2 } = globalThis.Bun;
|
|
23027
23002
|
var STDERR_TRUNCATE_LENGTH = 500;
|
|
23003
|
+
var TX_HEADER_PATTERN = /^(\d{4})-(\d{2}-\d{2})\s+(.+)$/;
|
|
23028
23004
|
async function defaultHledgerExecutor(cmdArgs) {
|
|
23029
23005
|
try {
|
|
23030
23006
|
const result = await $2`hledger ${cmdArgs}`.quiet().nothrow();
|
|
@@ -23059,10 +23035,10 @@ function parseUnknownPostings(hledgerOutput) {
|
|
|
23059
23035
|
let currentDate = "";
|
|
23060
23036
|
let currentDescription = "";
|
|
23061
23037
|
for (const line of lines) {
|
|
23062
|
-
const headerMatch = line.match(
|
|
23038
|
+
const headerMatch = line.match(TX_HEADER_PATTERN);
|
|
23063
23039
|
if (headerMatch) {
|
|
23064
|
-
currentDate = headerMatch[1]
|
|
23065
|
-
currentDescription = headerMatch[
|
|
23040
|
+
currentDate = `${headerMatch[1]}-${headerMatch[2]}`;
|
|
23041
|
+
currentDescription = headerMatch[3].trim();
|
|
23066
23042
|
continue;
|
|
23067
23043
|
}
|
|
23068
23044
|
const postingMatch = line.match(/^\s+(income:unknown|expenses:unknown)\s+([^\s]+(?:\s+[^\s=]+)?)\s*(?:=\s*(.+))?$/);
|
|
@@ -23083,7 +23059,7 @@ function countTransactions(hledgerOutput) {
|
|
|
23083
23059
|
`);
|
|
23084
23060
|
let count = 0;
|
|
23085
23061
|
for (const line of lines) {
|
|
23086
|
-
if (
|
|
23062
|
+
if (TX_HEADER_PATTERN.test(line)) {
|
|
23087
23063
|
count++;
|
|
23088
23064
|
}
|
|
23089
23065
|
}
|
|
@@ -23094,7 +23070,7 @@ function extractTransactionYears(hledgerOutput) {
|
|
|
23094
23070
|
const lines = hledgerOutput.split(`
|
|
23095
23071
|
`);
|
|
23096
23072
|
for (const line of lines) {
|
|
23097
|
-
const match2 = line.match(
|
|
23073
|
+
const match2 = line.match(TX_HEADER_PATTERN);
|
|
23098
23074
|
if (match2) {
|
|
23099
23075
|
years.add(parseInt(match2[1], 10));
|
|
23100
23076
|
}
|
|
@@ -23154,6 +23130,13 @@ async function getAccountBalance(mainJournalPath, account, asOfDate, executor =
|
|
|
23154
23130
|
|
|
23155
23131
|
// src/utils/rulesParser.ts
|
|
23156
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
|
+
}
|
|
23157
23140
|
function parseSkipRows(rulesContent) {
|
|
23158
23141
|
const match2 = rulesContent.match(/^skip\s+(\d+)/m);
|
|
23159
23142
|
return match2 ? parseInt(match2[1], 10) : 0;
|
|
@@ -23178,24 +23161,13 @@ function parseDateField(rulesContent, fieldNames) {
|
|
|
23178
23161
|
if (!match2) {
|
|
23179
23162
|
return fieldNames[0] || "date";
|
|
23180
23163
|
}
|
|
23181
|
-
|
|
23182
|
-
if (/^\d+$/.test(value)) {
|
|
23183
|
-
const index = parseInt(value, 10) - 1;
|
|
23184
|
-
return fieldNames[index] || value;
|
|
23185
|
-
}
|
|
23186
|
-
return value;
|
|
23164
|
+
return resolveFieldRef(match2[1], fieldNames);
|
|
23187
23165
|
}
|
|
23188
23166
|
function parseAmountFields(rulesContent, fieldNames) {
|
|
23189
23167
|
const result = {};
|
|
23190
23168
|
const simpleMatch = rulesContent.match(/^amount\s+(-?)%(\w+|\d+)/m);
|
|
23191
23169
|
if (simpleMatch) {
|
|
23192
|
-
|
|
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
|
-
}
|
|
23170
|
+
result.single = resolveFieldRef(simpleMatch[2], fieldNames);
|
|
23199
23171
|
}
|
|
23200
23172
|
const debitMatch = rulesContent.match(/if\s+%(\w+)\s+\.\s*\n\s*amount\s+-?%\1/m);
|
|
23201
23173
|
if (debitMatch) {
|
|
@@ -23260,29 +23232,34 @@ function parseBalance(balance) {
|
|
|
23260
23232
|
const amount = parseFloat(amountStr.replace(/,/g, ""));
|
|
23261
23233
|
return { currency, amount };
|
|
23262
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
|
+
}
|
|
23263
23240
|
function calculateDifference(expected, actual) {
|
|
23264
23241
|
const expectedParsed = parseBalance(expected);
|
|
23265
23242
|
const actualParsed = parseBalance(actual);
|
|
23266
23243
|
if (!expectedParsed || !actualParsed) {
|
|
23267
23244
|
throw new Error(`Cannot parse balances: expected="${expected}", actual="${actual}"`);
|
|
23268
23245
|
}
|
|
23269
|
-
|
|
23270
|
-
throw new Error(`Currency mismatch: expected ${expectedParsed.currency}, got ${actualParsed.currency}`);
|
|
23271
|
-
}
|
|
23246
|
+
validateCurrencies(expectedParsed, actualParsed);
|
|
23272
23247
|
const diff = actualParsed.amount - expectedParsed.amount;
|
|
23273
23248
|
const sign = diff >= 0 ? "+" : "";
|
|
23274
23249
|
const currency = expectedParsed.currency || actualParsed.currency;
|
|
23275
23250
|
return currency ? `${currency} ${sign}${diff.toFixed(2)}` : `${sign}${diff.toFixed(2)}`;
|
|
23276
23251
|
}
|
|
23252
|
+
function formatBalance(amount, currency) {
|
|
23253
|
+
const formattedAmount = amount.toFixed(2);
|
|
23254
|
+
return currency ? `${currency} ${formattedAmount}` : formattedAmount;
|
|
23255
|
+
}
|
|
23277
23256
|
function balancesMatch(balance1, balance2) {
|
|
23278
23257
|
const parsed1 = parseBalance(balance1);
|
|
23279
23258
|
const parsed2 = parseBalance(balance2);
|
|
23280
23259
|
if (!parsed1 || !parsed2) {
|
|
23281
23260
|
return false;
|
|
23282
23261
|
}
|
|
23283
|
-
|
|
23284
|
-
throw new Error(`Currency mismatch: ${parsed1.currency} vs ${parsed2.currency}`);
|
|
23285
|
-
}
|
|
23262
|
+
validateCurrencies(parsed1, parsed2);
|
|
23286
23263
|
return parsed1.amount === parsed2.amount;
|
|
23287
23264
|
}
|
|
23288
23265
|
|
|
@@ -23299,13 +23276,13 @@ function parseCsvFile(csvPath, config2) {
|
|
|
23299
23276
|
const csvWithHeader = lines.slice(headerIndex).join(`
|
|
23300
23277
|
`);
|
|
23301
23278
|
const useFieldNames = config2.fieldNames.length > 0;
|
|
23302
|
-
const result = import_papaparse2.default.parse(csvWithHeader, {
|
|
23303
|
-
header: !useFieldNames,
|
|
23304
|
-
delimiter: config2.separator,
|
|
23305
|
-
skipEmptyLines: true
|
|
23306
|
-
});
|
|
23307
23279
|
if (useFieldNames) {
|
|
23308
|
-
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;
|
|
23309
23286
|
const dataRows = rawRows.slice(1);
|
|
23310
23287
|
return dataRows.map((values) => {
|
|
23311
23288
|
const row = {};
|
|
@@ -23315,6 +23292,11 @@ function parseCsvFile(csvPath, config2) {
|
|
|
23315
23292
|
return row;
|
|
23316
23293
|
});
|
|
23317
23294
|
}
|
|
23295
|
+
const result = import_papaparse2.default.parse(csvWithHeader, {
|
|
23296
|
+
header: true,
|
|
23297
|
+
delimiter: config2.separator,
|
|
23298
|
+
skipEmptyLines: true
|
|
23299
|
+
});
|
|
23318
23300
|
return result.data;
|
|
23319
23301
|
}
|
|
23320
23302
|
function getRowAmount(row, amountFields) {
|
|
@@ -23375,8 +23357,7 @@ function looksLikeTransactionId(fieldName, value) {
|
|
|
23375
23357
|
if (!nameMatches)
|
|
23376
23358
|
return false;
|
|
23377
23359
|
const trimmedValue = value.trim();
|
|
23378
|
-
|
|
23379
|
-
return looksLikeId;
|
|
23360
|
+
return /^[A-Za-z0-9_-]+$/.test(trimmedValue) && trimmedValue.length >= 3;
|
|
23380
23361
|
}
|
|
23381
23362
|
function findTransactionId(row) {
|
|
23382
23363
|
for (const [field, value] of Object.entries(row)) {
|
|
@@ -23393,9 +23374,7 @@ function findMatchingCsvRow(posting, csvRows, config2) {
|
|
|
23393
23374
|
const rowAmount = getRowAmount(row, config2.amountFields);
|
|
23394
23375
|
if (rowDate !== posting.date)
|
|
23395
23376
|
return false;
|
|
23396
|
-
|
|
23397
|
-
return false;
|
|
23398
|
-
return true;
|
|
23377
|
+
return Math.abs(rowAmount - postingAmount) <= AMOUNT_MATCH_TOLERANCE;
|
|
23399
23378
|
});
|
|
23400
23379
|
if (candidates.length === 1) {
|
|
23401
23380
|
return candidates[0];
|
|
@@ -23416,17 +23395,11 @@ function findMatchingCsvRow(posting, csvRows, config2) {
|
|
|
23416
23395
|
const descMatches = candidates.filter((row) => {
|
|
23417
23396
|
return Object.values(row).some((value) => value && value.toLowerCase().includes(descriptionLower));
|
|
23418
23397
|
});
|
|
23419
|
-
|
|
23420
|
-
return descMatches[0];
|
|
23421
|
-
}
|
|
23422
|
-
if (descMatches.length > 1) {
|
|
23423
|
-
return descMatches[0];
|
|
23424
|
-
}
|
|
23425
|
-
return candidates[0];
|
|
23398
|
+
return descMatches[0] || candidates[0];
|
|
23426
23399
|
}
|
|
23427
23400
|
|
|
23428
23401
|
// src/tools/import-statements.ts
|
|
23429
|
-
function
|
|
23402
|
+
function buildErrorResult(error45, hint) {
|
|
23430
23403
|
return buildToolErrorResult(error45, hint, {
|
|
23431
23404
|
files: [],
|
|
23432
23405
|
summary: {
|
|
@@ -23447,23 +23420,16 @@ function buildSuccessResult3(files, summary, message) {
|
|
|
23447
23420
|
}
|
|
23448
23421
|
function findCsvFromRulesFile(rulesFile) {
|
|
23449
23422
|
const content = fs10.readFileSync(rulesFile, "utf-8");
|
|
23450
|
-
const
|
|
23451
|
-
if (!
|
|
23423
|
+
const sourcePath = parseSourceDirective(content);
|
|
23424
|
+
if (!sourcePath) {
|
|
23452
23425
|
return null;
|
|
23453
23426
|
}
|
|
23454
|
-
const
|
|
23455
|
-
const rulesDir = path9.dirname(rulesFile);
|
|
23456
|
-
const absolutePattern = path9.resolve(rulesDir, sourcePath);
|
|
23427
|
+
const absolutePattern = resolveSourcePath(sourcePath, rulesFile);
|
|
23457
23428
|
const matches = glob.sync(absolutePattern);
|
|
23458
23429
|
if (matches.length === 0) {
|
|
23459
23430
|
return null;
|
|
23460
23431
|
}
|
|
23461
|
-
matches
|
|
23462
|
-
const aStat = fs10.statSync(a);
|
|
23463
|
-
const bStat = fs10.statSync(b);
|
|
23464
|
-
return bStat.mtime.getTime() - aStat.mtime.getTime();
|
|
23465
|
-
});
|
|
23466
|
-
return matches[0];
|
|
23432
|
+
return sortByMtimeNewestFirst(matches)[0];
|
|
23467
23433
|
}
|
|
23468
23434
|
async function executeImports(fileResults, directory, pendingDir, doneDir, hledgerExecutor) {
|
|
23469
23435
|
const importedFiles = [];
|
|
@@ -23597,7 +23563,7 @@ async function importStatements(directory, agent, options, configLoader = loadIm
|
|
|
23597
23563
|
config2 = configLoader(directory);
|
|
23598
23564
|
} catch (error45) {
|
|
23599
23565
|
const errorMessage = `Failed to load configuration: ${error45 instanceof Error ? error45.message : String(error45)}`;
|
|
23600
|
-
return
|
|
23566
|
+
return buildErrorResult(errorMessage, 'Ensure config/import/providers.yaml exists with required paths including "rules"');
|
|
23601
23567
|
}
|
|
23602
23568
|
const pendingDir = path9.join(directory, config2.paths.pending);
|
|
23603
23569
|
const rulesDir = path9.join(directory, config2.paths.rules);
|
|
@@ -23606,7 +23572,7 @@ async function importStatements(directory, agent, options, configLoader = loadIm
|
|
|
23606
23572
|
const importContext = loadContext(directory, options.contextId);
|
|
23607
23573
|
const csvPath = path9.join(directory, importContext.filePath);
|
|
23608
23574
|
if (!fs10.existsSync(csvPath)) {
|
|
23609
|
-
return
|
|
23575
|
+
return buildErrorResult(`CSV file not found: ${importContext.filePath}`, "The file may have been moved or deleted");
|
|
23610
23576
|
}
|
|
23611
23577
|
const csvFiles = [csvPath];
|
|
23612
23578
|
const fileResults = [];
|
|
@@ -23639,12 +23605,7 @@ async function importStatements(directory, agent, options, configLoader = loadIm
|
|
|
23639
23605
|
totalUnknown += fileResult.unknownPostings.length;
|
|
23640
23606
|
}
|
|
23641
23607
|
for (const [_rulesFile, matchingCSVs] of rulesFileToCSVs.entries()) {
|
|
23642
|
-
|
|
23643
|
-
const aStat = fs10.statSync(a);
|
|
23644
|
-
const bStat = fs10.statSync(b);
|
|
23645
|
-
return bStat.mtime.getTime() - aStat.mtime.getTime();
|
|
23646
|
-
});
|
|
23647
|
-
const newestCSV = matchingCSVs[0];
|
|
23608
|
+
const newestCSV = sortByMtimeNewestFirst(matchingCSVs)[0];
|
|
23648
23609
|
const fileResult = await processCsvFile(newestCSV, rulesMapping, directory, hledgerExecutor);
|
|
23649
23610
|
fileResults.push(fileResult);
|
|
23650
23611
|
if (fileResult.error) {
|
|
@@ -23758,7 +23719,7 @@ Note: This tool is typically called via import-pipeline for the full workflow.`,
|
|
|
23758
23719
|
// src/tools/reconcile-statement.ts
|
|
23759
23720
|
import * as fs11 from "fs";
|
|
23760
23721
|
import * as path10 from "path";
|
|
23761
|
-
function
|
|
23722
|
+
function buildErrorResult2(params) {
|
|
23762
23723
|
return buildToolErrorResult(params.error, params.hint, {
|
|
23763
23724
|
account: params.account ?? "",
|
|
23764
23725
|
expectedBalance: params.expectedBalance ?? "",
|
|
@@ -23775,7 +23736,7 @@ function loadConfiguration(directory, configLoader) {
|
|
|
23775
23736
|
return { config: config2 };
|
|
23776
23737
|
} catch (error45) {
|
|
23777
23738
|
return {
|
|
23778
|
-
error:
|
|
23739
|
+
error: buildErrorResult2({
|
|
23779
23740
|
error: `Failed to load configuration: ${error45 instanceof Error ? error45.message : String(error45)}`,
|
|
23780
23741
|
hint: "Ensure config/import/providers.yaml exists"
|
|
23781
23742
|
})
|
|
@@ -23786,7 +23747,7 @@ function verifyCsvExists(directory, importContext) {
|
|
|
23786
23747
|
const csvFile = path10.join(directory, importContext.filePath);
|
|
23787
23748
|
if (!fs11.existsSync(csvFile)) {
|
|
23788
23749
|
return {
|
|
23789
|
-
error:
|
|
23750
|
+
error: buildErrorResult2({
|
|
23790
23751
|
error: `CSV file not found: ${importContext.filePath}`,
|
|
23791
23752
|
hint: `The file may have been moved or deleted. Context ID: ${importContext.id}`
|
|
23792
23753
|
})
|
|
@@ -23853,7 +23814,7 @@ function determineClosingBalance(csvFile, config2, importContext, manualClosingB
|
|
|
23853
23814
|
const exampleBalance = `${currency} <amount>`;
|
|
23854
23815
|
const retryCmd = buildRetryCommand(importContext.id, exampleBalance);
|
|
23855
23816
|
return {
|
|
23856
|
-
error:
|
|
23817
|
+
error: buildErrorResult2({
|
|
23857
23818
|
csvFile: relativeCsvPath,
|
|
23858
23819
|
error: "No closing balance found in CSV metadata or data",
|
|
23859
23820
|
hint: `Provide closingBalance parameter manually. Example retry: ${retryCmd}`,
|
|
@@ -23883,7 +23844,7 @@ function determineAccount(csvFile, rulesDir, importContext, manualAccount, relat
|
|
|
23883
23844
|
if (!account) {
|
|
23884
23845
|
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
23846
|
return {
|
|
23886
|
-
error:
|
|
23847
|
+
error: buildErrorResult2({
|
|
23887
23848
|
csvFile: relativeCsvPath,
|
|
23888
23849
|
error: "Could not determine account from rules file",
|
|
23889
23850
|
hint: rulesHint,
|
|
@@ -23945,7 +23906,7 @@ function tryExtractClosingBalanceFromCSV(csvFile, rulesDir) {
|
|
|
23945
23906
|
}
|
|
23946
23907
|
}
|
|
23947
23908
|
}
|
|
23948
|
-
const balanceStr =
|
|
23909
|
+
const balanceStr = formatBalance(numericValue, currency || undefined);
|
|
23949
23910
|
return {
|
|
23950
23911
|
balance: balanceStr,
|
|
23951
23912
|
confidence: "high",
|
|
@@ -23966,7 +23927,7 @@ async function reconcileStatement(directory, agent, options, configLoader = load
|
|
|
23966
23927
|
try {
|
|
23967
23928
|
importContext = loadContext(directory, options.contextId);
|
|
23968
23929
|
} catch {
|
|
23969
|
-
return
|
|
23930
|
+
return buildErrorResult2({
|
|
23970
23931
|
error: `Failed to load import context: ${options.contextId}`,
|
|
23971
23932
|
hint: "Ensure the context ID is valid and the context file exists in .memory/"
|
|
23972
23933
|
});
|
|
@@ -23995,7 +23956,7 @@ async function reconcileStatement(directory, agent, options, configLoader = load
|
|
|
23995
23956
|
const { account } = accountResult;
|
|
23996
23957
|
const lastTransactionDate = await getLastTransactionDate(mainJournalPath, account, hledgerExecutor);
|
|
23997
23958
|
if (!lastTransactionDate) {
|
|
23998
|
-
return
|
|
23959
|
+
return buildErrorResult2({
|
|
23999
23960
|
csvFile: relativeCsvPath,
|
|
24000
23961
|
account,
|
|
24001
23962
|
error: "No transactions found for account",
|
|
@@ -24005,7 +23966,7 @@ async function reconcileStatement(directory, agent, options, configLoader = load
|
|
|
24005
23966
|
}
|
|
24006
23967
|
const actualBalance = await getAccountBalance(mainJournalPath, account, lastTransactionDate, hledgerExecutor);
|
|
24007
23968
|
if (actualBalance === null) {
|
|
24008
|
-
return
|
|
23969
|
+
return buildErrorResult2({
|
|
24009
23970
|
csvFile: relativeCsvPath,
|
|
24010
23971
|
account,
|
|
24011
23972
|
lastTransactionDate,
|
|
@@ -24018,7 +23979,7 @@ async function reconcileStatement(directory, agent, options, configLoader = load
|
|
|
24018
23979
|
try {
|
|
24019
23980
|
doBalancesMatch = balancesMatch(closingBalance, actualBalance);
|
|
24020
23981
|
} catch (error45) {
|
|
24021
|
-
return
|
|
23982
|
+
return buildErrorResult2({
|
|
24022
23983
|
csvFile: relativeCsvPath,
|
|
24023
23984
|
account,
|
|
24024
23985
|
lastTransactionDate,
|
|
@@ -24047,7 +24008,7 @@ async function reconcileStatement(directory, agent, options, configLoader = load
|
|
|
24047
24008
|
try {
|
|
24048
24009
|
difference = calculateDifference(closingBalance, actualBalance);
|
|
24049
24010
|
} catch (error45) {
|
|
24050
|
-
return
|
|
24011
|
+
return buildErrorResult2({
|
|
24051
24012
|
csvFile: relativeCsvPath,
|
|
24052
24013
|
account,
|
|
24053
24014
|
lastTransactionDate,
|
|
@@ -24057,7 +24018,7 @@ async function reconcileStatement(directory, agent, options, configLoader = load
|
|
|
24057
24018
|
metadata
|
|
24058
24019
|
});
|
|
24059
24020
|
}
|
|
24060
|
-
return
|
|
24021
|
+
return buildErrorResult2({
|
|
24061
24022
|
csvFile: relativeCsvPath,
|
|
24062
24023
|
account,
|
|
24063
24024
|
lastTransactionDate,
|
|
@@ -24130,7 +24091,6 @@ function extractAccountsFromRulesFile(rulesPath) {
|
|
|
24130
24091
|
const account2Match = trimmed.match(/account2\s+(.+?)(?:\s+|$)/);
|
|
24131
24092
|
if (account2Match) {
|
|
24132
24093
|
accounts.add(account2Match[1].trim());
|
|
24133
|
-
continue;
|
|
24134
24094
|
}
|
|
24135
24095
|
}
|
|
24136
24096
|
return accounts;
|
|
@@ -24148,52 +24108,46 @@ function getAllAccountsFromRules(rulesPaths) {
|
|
|
24148
24108
|
function sortAccountDeclarations(accounts) {
|
|
24149
24109
|
return Array.from(accounts).sort((a, b) => a.localeCompare(b));
|
|
24150
24110
|
}
|
|
24151
|
-
function
|
|
24152
|
-
if (!fs12.existsSync(yearJournalPath)) {
|
|
24153
|
-
throw new Error(`Year journal not found: ${yearJournalPath}`);
|
|
24154
|
-
}
|
|
24155
|
-
const content = fs12.readFileSync(yearJournalPath, "utf-8");
|
|
24111
|
+
function parseJournalSections(content) {
|
|
24156
24112
|
const lines = content.split(`
|
|
24157
24113
|
`);
|
|
24158
24114
|
const existingAccounts = new Set;
|
|
24159
24115
|
const commentLines = [];
|
|
24160
|
-
const accountLines = [];
|
|
24161
24116
|
const otherLines = [];
|
|
24162
24117
|
let inAccountSection = false;
|
|
24163
24118
|
let accountSectionEnded = false;
|
|
24164
24119
|
for (const line of lines) {
|
|
24165
24120
|
const trimmed = line.trim();
|
|
24166
24121
|
if (trimmed.startsWith(";") || trimmed.startsWith("#")) {
|
|
24167
|
-
|
|
24168
|
-
commentLines.push(line);
|
|
24169
|
-
} else {
|
|
24170
|
-
otherLines.push(line);
|
|
24171
|
-
}
|
|
24122
|
+
(accountSectionEnded ? otherLines : commentLines).push(line);
|
|
24172
24123
|
continue;
|
|
24173
24124
|
}
|
|
24174
24125
|
if (trimmed.startsWith("account ")) {
|
|
24175
24126
|
inAccountSection = true;
|
|
24176
24127
|
const accountMatch = trimmed.match(/^account\s+(.+?)(?:\s+|$)/);
|
|
24177
24128
|
if (accountMatch) {
|
|
24178
|
-
|
|
24179
|
-
existingAccounts.add(accountName);
|
|
24180
|
-
accountLines.push(line);
|
|
24129
|
+
existingAccounts.add(accountMatch[1].trim());
|
|
24181
24130
|
}
|
|
24182
24131
|
continue;
|
|
24183
24132
|
}
|
|
24184
24133
|
if (trimmed === "") {
|
|
24185
|
-
if (inAccountSection && !accountSectionEnded)
|
|
24186
|
-
|
|
24187
|
-
|
|
24188
|
-
otherLines.push(line);
|
|
24189
|
-
}
|
|
24134
|
+
if (inAccountSection && !accountSectionEnded)
|
|
24135
|
+
continue;
|
|
24136
|
+
otherLines.push(line);
|
|
24190
24137
|
continue;
|
|
24191
24138
|
}
|
|
24192
|
-
if (inAccountSection)
|
|
24139
|
+
if (inAccountSection)
|
|
24193
24140
|
accountSectionEnded = true;
|
|
24194
|
-
}
|
|
24195
24141
|
otherLines.push(line);
|
|
24196
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);
|
|
24197
24151
|
const missingAccounts = new Set;
|
|
24198
24152
|
for (const account of accounts) {
|
|
24199
24153
|
if (!existingAccounts.has(account)) {
|
|
@@ -24259,14 +24213,10 @@ class MarkdownLogger {
|
|
|
24259
24213
|
}
|
|
24260
24214
|
}
|
|
24261
24215
|
info(message) {
|
|
24262
|
-
this.
|
|
24263
|
-
if (this.autoFlush)
|
|
24264
|
-
this.flushAsync();
|
|
24216
|
+
this.log(message);
|
|
24265
24217
|
}
|
|
24266
24218
|
warn(message) {
|
|
24267
|
-
this.
|
|
24268
|
-
if (this.autoFlush)
|
|
24269
|
-
this.flushAsync();
|
|
24219
|
+
this.log(`\u26A0\uFE0F **WARNING**: ${message}`);
|
|
24270
24220
|
}
|
|
24271
24221
|
error(message, error45) {
|
|
24272
24222
|
this.buffer.push(`\u274C **ERROR**: ${message}`);
|
|
@@ -24282,13 +24232,10 @@ class MarkdownLogger {
|
|
|
24282
24232
|
this.buffer.push("```");
|
|
24283
24233
|
this.buffer.push("");
|
|
24284
24234
|
}
|
|
24285
|
-
|
|
24286
|
-
this.flushAsync();
|
|
24235
|
+
this.autoFlushIfEnabled();
|
|
24287
24236
|
}
|
|
24288
24237
|
debug(message) {
|
|
24289
|
-
this.
|
|
24290
|
-
if (this.autoFlush)
|
|
24291
|
-
this.flushAsync();
|
|
24238
|
+
this.log(`\uD83D\uDD0D ${message}`);
|
|
24292
24239
|
}
|
|
24293
24240
|
logStep(stepName, status, details) {
|
|
24294
24241
|
const icon = status === "success" ? "\u2705" : status === "error" ? "\u274C" : "\u25B6\uFE0F";
|
|
@@ -24298,8 +24245,7 @@ class MarkdownLogger {
|
|
|
24298
24245
|
this.buffer.push(` ${details}`);
|
|
24299
24246
|
}
|
|
24300
24247
|
this.buffer.push("");
|
|
24301
|
-
|
|
24302
|
-
this.flushAsync();
|
|
24248
|
+
this.autoFlushIfEnabled();
|
|
24303
24249
|
}
|
|
24304
24250
|
logCommand(command, output) {
|
|
24305
24251
|
this.buffer.push("```bash");
|
|
@@ -24317,16 +24263,14 @@ class MarkdownLogger {
|
|
|
24317
24263
|
}
|
|
24318
24264
|
this.buffer.push("```");
|
|
24319
24265
|
this.buffer.push("");
|
|
24320
|
-
|
|
24321
|
-
this.flushAsync();
|
|
24266
|
+
this.autoFlushIfEnabled();
|
|
24322
24267
|
}
|
|
24323
24268
|
logResult(data) {
|
|
24324
24269
|
this.buffer.push("```json");
|
|
24325
24270
|
this.buffer.push(JSON.stringify(data, null, 2));
|
|
24326
24271
|
this.buffer.push("```");
|
|
24327
24272
|
this.buffer.push("");
|
|
24328
|
-
|
|
24329
|
-
this.flushAsync();
|
|
24273
|
+
this.autoFlushIfEnabled();
|
|
24330
24274
|
}
|
|
24331
24275
|
setContext(key, value) {
|
|
24332
24276
|
this.context[key] = value;
|
|
@@ -24346,6 +24290,15 @@ class MarkdownLogger {
|
|
|
24346
24290
|
getLogPath() {
|
|
24347
24291
|
return this.logPath;
|
|
24348
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
|
+
}
|
|
24349
24302
|
flushAsync() {
|
|
24350
24303
|
this.pendingFlush = this.flush().catch(() => {});
|
|
24351
24304
|
}
|
|
@@ -24490,7 +24443,8 @@ async function executeAccountDeclarationsStep(context, contextId, logger) {
|
|
|
24490
24443
|
break;
|
|
24491
24444
|
}
|
|
24492
24445
|
}
|
|
24493
|
-
} catch {
|
|
24446
|
+
} catch (error45) {
|
|
24447
|
+
logger?.debug(`Failed to extract year from rules file ${rulesFile}: ${error45 instanceof Error ? error45.message : String(error45)}`);
|
|
24494
24448
|
continue;
|
|
24495
24449
|
}
|
|
24496
24450
|
}
|
|
@@ -24528,6 +24482,37 @@ async function executeAccountDeclarationsStep(context, contextId, logger) {
|
|
|
24528
24482
|
});
|
|
24529
24483
|
logger?.endSection();
|
|
24530
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
|
+
}
|
|
24531
24516
|
async function executeDryRunStep(context, contextId, logger) {
|
|
24532
24517
|
logger?.startSection("Step 3: Dry Run Import");
|
|
24533
24518
|
logger?.logStep("Dry Run", "start");
|
|
@@ -24551,39 +24536,8 @@ async function executeDryRunStep(context, contextId, logger) {
|
|
|
24551
24536
|
}
|
|
24552
24537
|
if (allUnknownPostings.length > 0) {
|
|
24553
24538
|
try {
|
|
24554
|
-
const {
|
|
24555
|
-
|
|
24556
|
-
loadExistingAccounts: loadExistingAccounts2,
|
|
24557
|
-
extractRulePatternsFromFile: extractRulePatternsFromFile2
|
|
24558
|
-
} = await Promise.resolve().then(() => (init_accountSuggester(), exports_accountSuggester));
|
|
24559
|
-
const config2 = context.configLoader(context.directory);
|
|
24560
|
-
const rulesDir = path12.join(context.directory, config2.paths.rules);
|
|
24561
|
-
const importCtx = loadContext(context.directory, contextId);
|
|
24562
|
-
const csvPath = path12.join(context.directory, importCtx.filePath);
|
|
24563
|
-
const rulesMapping = loadRulesMapping(rulesDir);
|
|
24564
|
-
let yearJournalPath;
|
|
24565
|
-
let firstRulesFile;
|
|
24566
|
-
const rulesFile = findRulesForCsv(csvPath, rulesMapping);
|
|
24567
|
-
if (rulesFile) {
|
|
24568
|
-
firstRulesFile = rulesFile;
|
|
24569
|
-
try {
|
|
24570
|
-
const result = await context.hledgerExecutor(["print", "-f", rulesFile]);
|
|
24571
|
-
if (result.exitCode === 0) {
|
|
24572
|
-
const years = extractTransactionYears(result.stdout);
|
|
24573
|
-
if (years.size > 0) {
|
|
24574
|
-
const transactionYear = Array.from(years)[0];
|
|
24575
|
-
yearJournalPath = ensureYearJournalExists(context.directory, transactionYear);
|
|
24576
|
-
}
|
|
24577
|
-
}
|
|
24578
|
-
} catch {}
|
|
24579
|
-
}
|
|
24580
|
-
const suggestionContext = {
|
|
24581
|
-
existingAccounts: yearJournalPath ? loadExistingAccounts2(yearJournalPath) : [],
|
|
24582
|
-
rulesFilePath: firstRulesFile,
|
|
24583
|
-
existingRules: firstRulesFile ? extractRulePatternsFromFile2(firstRulesFile) : undefined,
|
|
24584
|
-
yearJournalPath,
|
|
24585
|
-
logger
|
|
24586
|
-
};
|
|
24539
|
+
const { suggestAccountsForPostingsBatch: suggestAccountsForPostingsBatch2 } = await Promise.resolve().then(() => (init_accountSuggester(), exports_accountSuggester));
|
|
24540
|
+
const suggestionContext = await buildSuggestionContext(context, contextId, logger);
|
|
24587
24541
|
postingsWithSuggestions = await suggestAccountsForPostingsBatch2(allUnknownPostings, suggestionContext);
|
|
24588
24542
|
} catch (error45) {
|
|
24589
24543
|
logger?.error(`[ERROR] Failed to generate account suggestions: ${error45 instanceof Error ? error45.message : String(error45)}`);
|