@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.
- package/dist/index.js +182 -237
- 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
|
|
2692
|
+
description,
|
|
2692
2693
|
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 }
|
|
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
|
|
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
|
|
4291
|
+
function buildAccountHierarchySection(accounts) {
|
|
4292
|
+
if (accounts.length === 0)
|
|
4293
|
+
return "";
|
|
4294
|
+
return `## Existing Account Hierarchy
|
|
4305
4295
|
|
|
4306
|
-
|
|
4307
|
-
|
|
4308
|
-
`);
|
|
4309
|
-
prompt += `
|
|
4296
|
+
${accounts.map((acc) => `- ${acc}`).join(`
|
|
4297
|
+
`)}
|
|
4310
4298
|
|
|
4311
4299
|
`;
|
|
4312
|
-
|
|
4313
|
-
|
|
4314
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4330
|
-
|
|
4331
|
-
|
|
4332
|
-
|
|
4333
|
-
|
|
4334
|
-
|
|
4335
|
-
|
|
4336
|
-
|
|
4337
|
-
|
|
4338
|
-
|
|
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
|
-
|
|
4342
|
-
`;
|
|
4323
|
+
parts.push(`- CSV Data: ${JSON.stringify(posting.csvRow)}`);
|
|
4343
4324
|
}
|
|
4344
|
-
|
|
4345
|
-
|
|
4325
|
+
return parts.join(`
|
|
4326
|
+
`);
|
|
4346
4327
|
});
|
|
4347
|
-
|
|
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
|
-
|
|
4373
|
-
|
|
4374
|
-
|
|
4375
|
-
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
|
|
4379
|
-
|
|
4380
|
-
|
|
4381
|
-
|
|
4382
|
-
|
|
4383
|
-
|
|
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
|
-
|
|
4403
|
-
|
|
4404
|
-
|
|
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
|
-
|
|
4372
|
+
cached2.set(index, suggestionCache[hash2]);
|
|
4412
4373
|
} else {
|
|
4413
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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);
|
|
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
|
|
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
|
-
|
|
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(
|
|
23027
|
+
const headerMatch = line.match(TX_HEADER_PATTERN);
|
|
23063
23028
|
if (headerMatch) {
|
|
23064
|
-
currentDate = headerMatch[1]
|
|
23065
|
-
currentDescription = headerMatch[
|
|
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 (
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
24187
|
-
|
|
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.
|
|
24263
|
-
if (this.autoFlush)
|
|
24264
|
-
this.flushAsync();
|
|
24208
|
+
this.log(message);
|
|
24265
24209
|
}
|
|
24266
24210
|
warn(message) {
|
|
24267
|
-
this.
|
|
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
|
-
|
|
24286
|
-
this.flushAsync();
|
|
24227
|
+
this.autoFlushIfEnabled();
|
|
24287
24228
|
}
|
|
24288
24229
|
debug(message) {
|
|
24289
|
-
this.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|