@fuzzle/opencode-accountant 0.4.6 → 0.5.0-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/README.md +5 -9
- package/agent/accountant.md +27 -87
- package/dist/index.js +588 -990
- package/docs/architecture/import-context.md +674 -0
- package/docs/tools/classify-statements.md +84 -7
- package/docs/tools/import-pipeline.md +611 -0
- package/docs/tools/import-statements.md +43 -5
- package/docs/tools/reconcile-statement.md +529 -0
- package/package.json +3 -4
package/dist/index.js
CHANGED
|
@@ -2676,12 +2676,12 @@ var init_js_yaml = __esm(() => {
|
|
|
2676
2676
|
});
|
|
2677
2677
|
|
|
2678
2678
|
// src/utils/agentLoader.ts
|
|
2679
|
-
import
|
|
2679
|
+
import * as fs from "fs";
|
|
2680
2680
|
function loadAgent(filePath) {
|
|
2681
|
-
if (!existsSync(filePath)) {
|
|
2681
|
+
if (!fs.existsSync(filePath)) {
|
|
2682
2682
|
return null;
|
|
2683
2683
|
}
|
|
2684
|
-
const content = readFileSync(filePath, "utf-8");
|
|
2684
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
2685
2685
|
const match = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
2686
2686
|
if (!match) {
|
|
2687
2687
|
throw new Error(`Invalid frontmatter format in ${filePath}`);
|
|
@@ -4222,606 +4222,6 @@ var require_brace_expansion = __commonJS((exports, module) => {
|
|
|
4222
4222
|
}
|
|
4223
4223
|
});
|
|
4224
4224
|
|
|
4225
|
-
// node_modules/convert-csv-to-json/src/util/fileUtils.js
|
|
4226
|
-
var require_fileUtils = __commonJS((exports, module) => {
|
|
4227
|
-
var fs8 = __require("fs");
|
|
4228
|
-
|
|
4229
|
-
class FileUtils {
|
|
4230
|
-
readFile(fileInputName, encoding) {
|
|
4231
|
-
return fs8.readFileSync(fileInputName, encoding).toString();
|
|
4232
|
-
}
|
|
4233
|
-
readFileAsync(fileInputName, encoding = "utf8") {
|
|
4234
|
-
if (fs8.promises && typeof fs8.promises.readFile === "function") {
|
|
4235
|
-
return fs8.promises.readFile(fileInputName, encoding).then((buf) => buf.toString());
|
|
4236
|
-
}
|
|
4237
|
-
return new Promise((resolve2, reject) => {
|
|
4238
|
-
fs8.readFile(fileInputName, encoding, (err, data) => {
|
|
4239
|
-
if (err) {
|
|
4240
|
-
reject(err);
|
|
4241
|
-
return;
|
|
4242
|
-
}
|
|
4243
|
-
resolve2(data.toString());
|
|
4244
|
-
});
|
|
4245
|
-
});
|
|
4246
|
-
}
|
|
4247
|
-
writeFile(json3, fileOutputName) {
|
|
4248
|
-
fs8.writeFile(fileOutputName, json3, function(err) {
|
|
4249
|
-
if (err) {
|
|
4250
|
-
throw err;
|
|
4251
|
-
} else {
|
|
4252
|
-
console.log("File saved: " + fileOutputName);
|
|
4253
|
-
}
|
|
4254
|
-
});
|
|
4255
|
-
}
|
|
4256
|
-
writeFileAsync(json3, fileOutputName) {
|
|
4257
|
-
if (fs8.promises && typeof fs8.promises.writeFile === "function") {
|
|
4258
|
-
return fs8.promises.writeFile(fileOutputName, json3);
|
|
4259
|
-
}
|
|
4260
|
-
return new Promise((resolve2, reject) => {
|
|
4261
|
-
fs8.writeFile(fileOutputName, json3, (err) => {
|
|
4262
|
-
if (err)
|
|
4263
|
-
return reject(err);
|
|
4264
|
-
resolve2();
|
|
4265
|
-
});
|
|
4266
|
-
});
|
|
4267
|
-
}
|
|
4268
|
-
}
|
|
4269
|
-
module.exports = new FileUtils;
|
|
4270
|
-
});
|
|
4271
|
-
|
|
4272
|
-
// node_modules/convert-csv-to-json/src/util/stringUtils.js
|
|
4273
|
-
var require_stringUtils = __commonJS((exports, module) => {
|
|
4274
|
-
class StringUtils {
|
|
4275
|
-
static PATTERNS = {
|
|
4276
|
-
INTEGER: /^-?\d+$/,
|
|
4277
|
-
FLOAT: /^-?\d*\.\d+$/,
|
|
4278
|
-
WHITESPACE: /\s/g
|
|
4279
|
-
};
|
|
4280
|
-
static BOOLEAN_VALUES = {
|
|
4281
|
-
TRUE: "true",
|
|
4282
|
-
FALSE: "false"
|
|
4283
|
-
};
|
|
4284
|
-
trimPropertyName(shouldTrimAll, propertyName) {
|
|
4285
|
-
if (!propertyName) {
|
|
4286
|
-
return "";
|
|
4287
|
-
}
|
|
4288
|
-
return shouldTrimAll ? propertyName.replace(StringUtils.PATTERNS.WHITESPACE, "") : propertyName.trim();
|
|
4289
|
-
}
|
|
4290
|
-
getValueFormatByType(value) {
|
|
4291
|
-
if (this.isEmpty(value)) {
|
|
4292
|
-
return String();
|
|
4293
|
-
}
|
|
4294
|
-
if (this.isBoolean(value)) {
|
|
4295
|
-
return this.convertToBoolean(value);
|
|
4296
|
-
}
|
|
4297
|
-
if (this.isInteger(value)) {
|
|
4298
|
-
return this.convertInteger(value);
|
|
4299
|
-
}
|
|
4300
|
-
if (this.isFloat(value)) {
|
|
4301
|
-
return this.convertFloat(value);
|
|
4302
|
-
}
|
|
4303
|
-
return String(value);
|
|
4304
|
-
}
|
|
4305
|
-
hasContent(values = []) {
|
|
4306
|
-
return Array.isArray(values) && values.some((value) => Boolean(value));
|
|
4307
|
-
}
|
|
4308
|
-
isEmpty(value) {
|
|
4309
|
-
return value === undefined || value === "";
|
|
4310
|
-
}
|
|
4311
|
-
isBoolean(value) {
|
|
4312
|
-
const normalizedValue = value.toLowerCase();
|
|
4313
|
-
return normalizedValue === StringUtils.BOOLEAN_VALUES.TRUE || normalizedValue === StringUtils.BOOLEAN_VALUES.FALSE;
|
|
4314
|
-
}
|
|
4315
|
-
isInteger(value) {
|
|
4316
|
-
return StringUtils.PATTERNS.INTEGER.test(value);
|
|
4317
|
-
}
|
|
4318
|
-
isFloat(value) {
|
|
4319
|
-
return StringUtils.PATTERNS.FLOAT.test(value);
|
|
4320
|
-
}
|
|
4321
|
-
hasLeadingZero(value) {
|
|
4322
|
-
const isPositiveWithLeadingZero = value.length > 1 && value[0] === "0";
|
|
4323
|
-
const isNegativeWithLeadingZero = value.length > 2 && value[0] === "-" && value[1] === "0";
|
|
4324
|
-
return isPositiveWithLeadingZero || isNegativeWithLeadingZero;
|
|
4325
|
-
}
|
|
4326
|
-
convertToBoolean(value) {
|
|
4327
|
-
return JSON.parse(value.toLowerCase());
|
|
4328
|
-
}
|
|
4329
|
-
convertInteger(value) {
|
|
4330
|
-
if (this.hasLeadingZero(value)) {
|
|
4331
|
-
return String(value);
|
|
4332
|
-
}
|
|
4333
|
-
const num = Number(value);
|
|
4334
|
-
return Number.isSafeInteger(num) ? num : String(value);
|
|
4335
|
-
}
|
|
4336
|
-
convertFloat(value) {
|
|
4337
|
-
const num = Number(value);
|
|
4338
|
-
return Number.isFinite(num) ? num : String(value);
|
|
4339
|
-
}
|
|
4340
|
-
}
|
|
4341
|
-
module.exports = new StringUtils;
|
|
4342
|
-
});
|
|
4343
|
-
|
|
4344
|
-
// node_modules/convert-csv-to-json/src/util/jsonUtils.js
|
|
4345
|
-
var require_jsonUtils = __commonJS((exports, module) => {
|
|
4346
|
-
class JsonUtil {
|
|
4347
|
-
validateJson(json3) {
|
|
4348
|
-
try {
|
|
4349
|
-
JSON.parse(json3);
|
|
4350
|
-
} catch (err) {
|
|
4351
|
-
throw Error(`Parsed csv has generated an invalid json!!!
|
|
4352
|
-
` + err);
|
|
4353
|
-
}
|
|
4354
|
-
}
|
|
4355
|
-
}
|
|
4356
|
-
module.exports = new JsonUtil;
|
|
4357
|
-
});
|
|
4358
|
-
|
|
4359
|
-
// node_modules/convert-csv-to-json/src/csvToJson.js
|
|
4360
|
-
var require_csvToJson = __commonJS((exports, module) => {
|
|
4361
|
-
var fileUtils = require_fileUtils();
|
|
4362
|
-
var stringUtils = require_stringUtils();
|
|
4363
|
-
var jsonUtils = require_jsonUtils();
|
|
4364
|
-
var newLine = /\r?\n/;
|
|
4365
|
-
var defaultFieldDelimiter = ";";
|
|
4366
|
-
|
|
4367
|
-
class CsvToJson {
|
|
4368
|
-
formatValueByType(active) {
|
|
4369
|
-
this.printValueFormatByType = active;
|
|
4370
|
-
return this;
|
|
4371
|
-
}
|
|
4372
|
-
supportQuotedField(active) {
|
|
4373
|
-
this.isSupportQuotedField = active;
|
|
4374
|
-
return this;
|
|
4375
|
-
}
|
|
4376
|
-
fieldDelimiter(delimiter) {
|
|
4377
|
-
this.delimiter = delimiter;
|
|
4378
|
-
return this;
|
|
4379
|
-
}
|
|
4380
|
-
trimHeaderFieldWhiteSpace(active) {
|
|
4381
|
-
this.isTrimHeaderFieldWhiteSpace = active;
|
|
4382
|
-
return this;
|
|
4383
|
-
}
|
|
4384
|
-
indexHeader(indexHeaderValue) {
|
|
4385
|
-
if (isNaN(indexHeaderValue)) {
|
|
4386
|
-
throw new Error("The index Header must be a Number!");
|
|
4387
|
-
}
|
|
4388
|
-
this.indexHeaderValue = indexHeaderValue;
|
|
4389
|
-
return this;
|
|
4390
|
-
}
|
|
4391
|
-
parseSubArray(delimiter = "*", separator = ",") {
|
|
4392
|
-
this.parseSubArrayDelimiter = delimiter;
|
|
4393
|
-
this.parseSubArraySeparator = separator;
|
|
4394
|
-
}
|
|
4395
|
-
encoding(encoding) {
|
|
4396
|
-
this.encoding = encoding;
|
|
4397
|
-
return this;
|
|
4398
|
-
}
|
|
4399
|
-
generateJsonFileFromCsv(fileInputName, fileOutputName) {
|
|
4400
|
-
let jsonStringified = this.getJsonFromCsvStringified(fileInputName);
|
|
4401
|
-
fileUtils.writeFile(jsonStringified, fileOutputName);
|
|
4402
|
-
}
|
|
4403
|
-
getJsonFromCsvStringified(fileInputName) {
|
|
4404
|
-
let json3 = this.getJsonFromCsv(fileInputName);
|
|
4405
|
-
let jsonStringified = JSON.stringify(json3, undefined, 1);
|
|
4406
|
-
jsonUtils.validateJson(jsonStringified);
|
|
4407
|
-
return jsonStringified;
|
|
4408
|
-
}
|
|
4409
|
-
getJsonFromCsv(fileInputName) {
|
|
4410
|
-
let parsedCsv = fileUtils.readFile(fileInputName, this.encoding);
|
|
4411
|
-
return this.csvToJson(parsedCsv);
|
|
4412
|
-
}
|
|
4413
|
-
csvStringToJson(csvString) {
|
|
4414
|
-
return this.csvToJson(csvString);
|
|
4415
|
-
}
|
|
4416
|
-
csvStringToJsonStringified(csvString) {
|
|
4417
|
-
let json3 = this.csvStringToJson(csvString);
|
|
4418
|
-
let jsonStringified = JSON.stringify(json3, undefined, 1);
|
|
4419
|
-
jsonUtils.validateJson(jsonStringified);
|
|
4420
|
-
return jsonStringified;
|
|
4421
|
-
}
|
|
4422
|
-
csvToJson(parsedCsv) {
|
|
4423
|
-
this.validateInputConfig();
|
|
4424
|
-
let lines = parsedCsv.split(newLine);
|
|
4425
|
-
let fieldDelimiter = this.getFieldDelimiter();
|
|
4426
|
-
let index = this.getIndexHeader();
|
|
4427
|
-
let headers;
|
|
4428
|
-
if (this.isSupportQuotedField) {
|
|
4429
|
-
headers = this.split(lines[index]);
|
|
4430
|
-
} else {
|
|
4431
|
-
headers = lines[index].split(fieldDelimiter);
|
|
4432
|
-
}
|
|
4433
|
-
while (!stringUtils.hasContent(headers) && index <= lines.length) {
|
|
4434
|
-
index = index + 1;
|
|
4435
|
-
headers = lines[index].split(fieldDelimiter);
|
|
4436
|
-
}
|
|
4437
|
-
let jsonResult = [];
|
|
4438
|
-
for (let i2 = index + 1;i2 < lines.length; i2++) {
|
|
4439
|
-
let currentLine;
|
|
4440
|
-
if (this.isSupportQuotedField) {
|
|
4441
|
-
currentLine = this.split(lines[i2]);
|
|
4442
|
-
} else {
|
|
4443
|
-
currentLine = lines[i2].split(fieldDelimiter);
|
|
4444
|
-
}
|
|
4445
|
-
if (stringUtils.hasContent(currentLine)) {
|
|
4446
|
-
jsonResult.push(this.buildJsonResult(headers, currentLine));
|
|
4447
|
-
}
|
|
4448
|
-
}
|
|
4449
|
-
return jsonResult;
|
|
4450
|
-
}
|
|
4451
|
-
getFieldDelimiter() {
|
|
4452
|
-
if (this.delimiter) {
|
|
4453
|
-
return this.delimiter;
|
|
4454
|
-
}
|
|
4455
|
-
return defaultFieldDelimiter;
|
|
4456
|
-
}
|
|
4457
|
-
getIndexHeader() {
|
|
4458
|
-
if (this.indexHeaderValue !== null && !isNaN(this.indexHeaderValue)) {
|
|
4459
|
-
return this.indexHeaderValue;
|
|
4460
|
-
}
|
|
4461
|
-
return 0;
|
|
4462
|
-
}
|
|
4463
|
-
buildJsonResult(headers, currentLine) {
|
|
4464
|
-
let jsonObject = {};
|
|
4465
|
-
for (let j = 0;j < headers.length; j++) {
|
|
4466
|
-
let propertyName = stringUtils.trimPropertyName(this.isTrimHeaderFieldWhiteSpace, headers[j]);
|
|
4467
|
-
let value = currentLine[j];
|
|
4468
|
-
if (this.isParseSubArray(value)) {
|
|
4469
|
-
value = this.buildJsonSubArray(value);
|
|
4470
|
-
}
|
|
4471
|
-
if (this.printValueFormatByType && !Array.isArray(value)) {
|
|
4472
|
-
value = stringUtils.getValueFormatByType(currentLine[j]);
|
|
4473
|
-
}
|
|
4474
|
-
jsonObject[propertyName] = value;
|
|
4475
|
-
}
|
|
4476
|
-
return jsonObject;
|
|
4477
|
-
}
|
|
4478
|
-
buildJsonSubArray(value) {
|
|
4479
|
-
let extractedValues = value.substring(value.indexOf(this.parseSubArrayDelimiter) + 1, value.lastIndexOf(this.parseSubArrayDelimiter));
|
|
4480
|
-
extractedValues.trim();
|
|
4481
|
-
value = extractedValues.split(this.parseSubArraySeparator);
|
|
4482
|
-
if (this.printValueFormatByType) {
|
|
4483
|
-
for (let i2 = 0;i2 < value.length; i2++) {
|
|
4484
|
-
value[i2] = stringUtils.getValueFormatByType(value[i2]);
|
|
4485
|
-
}
|
|
4486
|
-
}
|
|
4487
|
-
return value;
|
|
4488
|
-
}
|
|
4489
|
-
isParseSubArray(value) {
|
|
4490
|
-
if (this.parseSubArrayDelimiter) {
|
|
4491
|
-
if (value && (value.indexOf(this.parseSubArrayDelimiter) === 0 && value.lastIndexOf(this.parseSubArrayDelimiter) === value.length - 1)) {
|
|
4492
|
-
return true;
|
|
4493
|
-
}
|
|
4494
|
-
}
|
|
4495
|
-
return false;
|
|
4496
|
-
}
|
|
4497
|
-
validateInputConfig() {
|
|
4498
|
-
if (this.isSupportQuotedField) {
|
|
4499
|
-
if (this.getFieldDelimiter() === '"') {
|
|
4500
|
-
throw new Error('When SupportQuotedFields is enabled you cannot defined the field delimiter as quote -> ["]');
|
|
4501
|
-
}
|
|
4502
|
-
if (this.parseSubArraySeparator === '"') {
|
|
4503
|
-
throw new Error('When SupportQuotedFields is enabled you cannot defined the field parseSubArraySeparator as quote -> ["]');
|
|
4504
|
-
}
|
|
4505
|
-
if (this.parseSubArrayDelimiter === '"') {
|
|
4506
|
-
throw new Error('When SupportQuotedFields is enabled you cannot defined the field parseSubArrayDelimiter as quote -> ["]');
|
|
4507
|
-
}
|
|
4508
|
-
}
|
|
4509
|
-
}
|
|
4510
|
-
hasQuotes(line) {
|
|
4511
|
-
return line.includes('"');
|
|
4512
|
-
}
|
|
4513
|
-
split(line) {
|
|
4514
|
-
if (line.length == 0) {
|
|
4515
|
-
return [];
|
|
4516
|
-
}
|
|
4517
|
-
let delim = this.getFieldDelimiter();
|
|
4518
|
-
let subSplits = [""];
|
|
4519
|
-
if (this.hasQuotes(line)) {
|
|
4520
|
-
let chars = line.split("");
|
|
4521
|
-
let subIndex = 0;
|
|
4522
|
-
let startQuote = false;
|
|
4523
|
-
let isDouble = false;
|
|
4524
|
-
chars.forEach((c, i2, arr) => {
|
|
4525
|
-
if (isDouble) {
|
|
4526
|
-
subSplits[subIndex] += c;
|
|
4527
|
-
isDouble = false;
|
|
4528
|
-
return;
|
|
4529
|
-
}
|
|
4530
|
-
if (c != '"' && c != delim) {
|
|
4531
|
-
subSplits[subIndex] += c;
|
|
4532
|
-
} else if (c == delim && startQuote) {
|
|
4533
|
-
subSplits[subIndex] += c;
|
|
4534
|
-
} else if (c == delim) {
|
|
4535
|
-
subIndex++;
|
|
4536
|
-
subSplits[subIndex] = "";
|
|
4537
|
-
return;
|
|
4538
|
-
} else {
|
|
4539
|
-
if (arr[i2 + 1] === '"') {
|
|
4540
|
-
isDouble = true;
|
|
4541
|
-
} else {
|
|
4542
|
-
if (!startQuote) {
|
|
4543
|
-
startQuote = true;
|
|
4544
|
-
} else {
|
|
4545
|
-
startQuote = false;
|
|
4546
|
-
}
|
|
4547
|
-
}
|
|
4548
|
-
}
|
|
4549
|
-
});
|
|
4550
|
-
if (startQuote) {
|
|
4551
|
-
throw new Error("Row contains mismatched quotes!");
|
|
4552
|
-
}
|
|
4553
|
-
return subSplits;
|
|
4554
|
-
} else {
|
|
4555
|
-
return line.split(delim);
|
|
4556
|
-
}
|
|
4557
|
-
}
|
|
4558
|
-
}
|
|
4559
|
-
module.exports = new CsvToJson;
|
|
4560
|
-
});
|
|
4561
|
-
|
|
4562
|
-
// node_modules/convert-csv-to-json/src/csvToJsonAsync.js
|
|
4563
|
-
var require_csvToJsonAsync = __commonJS((exports, module) => {
|
|
4564
|
-
var fileUtils = require_fileUtils();
|
|
4565
|
-
var csvToJson = require_csvToJson();
|
|
4566
|
-
|
|
4567
|
-
class CsvToJsonAsync {
|
|
4568
|
-
constructor() {
|
|
4569
|
-
this.csvToJson = csvToJson;
|
|
4570
|
-
}
|
|
4571
|
-
formatValueByType(active) {
|
|
4572
|
-
this.csvToJson.formatValueByType(active);
|
|
4573
|
-
return this;
|
|
4574
|
-
}
|
|
4575
|
-
supportQuotedField(active) {
|
|
4576
|
-
this.csvToJson.supportQuotedField(active);
|
|
4577
|
-
return this;
|
|
4578
|
-
}
|
|
4579
|
-
fieldDelimiter(delimiter) {
|
|
4580
|
-
this.csvToJson.fieldDelimiter(delimiter);
|
|
4581
|
-
return this;
|
|
4582
|
-
}
|
|
4583
|
-
trimHeaderFieldWhiteSpace(active) {
|
|
4584
|
-
this.csvToJson.trimHeaderFieldWhiteSpace(active);
|
|
4585
|
-
return this;
|
|
4586
|
-
}
|
|
4587
|
-
indexHeader(indexHeader) {
|
|
4588
|
-
this.csvToJson.indexHeader(indexHeader);
|
|
4589
|
-
return this;
|
|
4590
|
-
}
|
|
4591
|
-
parseSubArray(delimiter = "*", separator = ",") {
|
|
4592
|
-
this.csvToJson.parseSubArray(delimiter, separator);
|
|
4593
|
-
return this;
|
|
4594
|
-
}
|
|
4595
|
-
encoding(encoding) {
|
|
4596
|
-
this.csvToJson.encoding = encoding;
|
|
4597
|
-
return this;
|
|
4598
|
-
}
|
|
4599
|
-
async generateJsonFileFromCsv(fileInputName, fileOutputName) {
|
|
4600
|
-
const jsonStringified = await this.getJsonFromCsvStringified(fileInputName);
|
|
4601
|
-
await fileUtils.writeFileAsync(jsonStringified, fileOutputName);
|
|
4602
|
-
}
|
|
4603
|
-
async getJsonFromCsvStringified(fileInputName) {
|
|
4604
|
-
const json3 = await this.getJsonFromCsvAsync(fileInputName);
|
|
4605
|
-
return JSON.stringify(json3, undefined, 1);
|
|
4606
|
-
}
|
|
4607
|
-
async getJsonFromCsvAsync(inputFileNameOrCsv, options = {}) {
|
|
4608
|
-
if (inputFileNameOrCsv === null || inputFileNameOrCsv === undefined) {
|
|
4609
|
-
throw new Error("inputFileNameOrCsv is not defined!!!");
|
|
4610
|
-
}
|
|
4611
|
-
if (options.raw) {
|
|
4612
|
-
if (inputFileNameOrCsv === "") {
|
|
4613
|
-
return [];
|
|
4614
|
-
}
|
|
4615
|
-
return this.csvToJson.csvToJson(inputFileNameOrCsv);
|
|
4616
|
-
}
|
|
4617
|
-
const parsedCsv = await fileUtils.readFileAsync(inputFileNameOrCsv, this.csvToJson.encoding || "utf8");
|
|
4618
|
-
return this.csvToJson.csvToJson(parsedCsv);
|
|
4619
|
-
}
|
|
4620
|
-
csvStringToJsonAsync(csvString, options = { raw: true }) {
|
|
4621
|
-
return this.getJsonFromCsvAsync(csvString, options);
|
|
4622
|
-
}
|
|
4623
|
-
async csvStringToJsonStringifiedAsync(csvString) {
|
|
4624
|
-
const json3 = await this.csvStringToJsonAsync(csvString);
|
|
4625
|
-
return JSON.stringify(json3, undefined, 1);
|
|
4626
|
-
}
|
|
4627
|
-
}
|
|
4628
|
-
module.exports = new CsvToJsonAsync;
|
|
4629
|
-
});
|
|
4630
|
-
|
|
4631
|
-
// node_modules/convert-csv-to-json/src/browserApi.js
|
|
4632
|
-
var require_browserApi = __commonJS((exports, module) => {
|
|
4633
|
-
var csvToJson = require_csvToJson();
|
|
4634
|
-
|
|
4635
|
-
class BrowserApi {
|
|
4636
|
-
constructor() {
|
|
4637
|
-
this.csvToJson = csvToJson;
|
|
4638
|
-
}
|
|
4639
|
-
formatValueByType(active = true) {
|
|
4640
|
-
this.csvToJson.formatValueByType(active);
|
|
4641
|
-
return this;
|
|
4642
|
-
}
|
|
4643
|
-
supportQuotedField(active = false) {
|
|
4644
|
-
this.csvToJson.supportQuotedField(active);
|
|
4645
|
-
return this;
|
|
4646
|
-
}
|
|
4647
|
-
fieldDelimiter(delimiter) {
|
|
4648
|
-
this.csvToJson.fieldDelimiter(delimiter);
|
|
4649
|
-
return this;
|
|
4650
|
-
}
|
|
4651
|
-
trimHeaderFieldWhiteSpace(active = false) {
|
|
4652
|
-
this.csvToJson.trimHeaderFieldWhiteSpace(active);
|
|
4653
|
-
return this;
|
|
4654
|
-
}
|
|
4655
|
-
indexHeader(index) {
|
|
4656
|
-
this.csvToJson.indexHeader(index);
|
|
4657
|
-
return this;
|
|
4658
|
-
}
|
|
4659
|
-
parseSubArray(delimiter = "*", separator = ",") {
|
|
4660
|
-
this.csvToJson.parseSubArray(delimiter, separator);
|
|
4661
|
-
return this;
|
|
4662
|
-
}
|
|
4663
|
-
csvStringToJson(csvString) {
|
|
4664
|
-
if (csvString === undefined || csvString === null) {
|
|
4665
|
-
throw new Error("csvString is not defined!!!");
|
|
4666
|
-
}
|
|
4667
|
-
return this.csvToJson.csvToJson(csvString);
|
|
4668
|
-
}
|
|
4669
|
-
csvStringToJsonStringified(csvString) {
|
|
4670
|
-
if (csvString === undefined || csvString === null) {
|
|
4671
|
-
throw new Error("csvString is not defined!!!");
|
|
4672
|
-
}
|
|
4673
|
-
return this.csvToJson.csvStringToJsonStringified(csvString);
|
|
4674
|
-
}
|
|
4675
|
-
csvStringToJsonAsync(csvString) {
|
|
4676
|
-
return Promise.resolve(this.csvStringToJson(csvString));
|
|
4677
|
-
}
|
|
4678
|
-
csvStringToJsonStringifiedAsync(csvString) {
|
|
4679
|
-
return Promise.resolve(this.csvStringToJsonStringified(csvString));
|
|
4680
|
-
}
|
|
4681
|
-
parseFile(file2, options = {}) {
|
|
4682
|
-
if (!file2) {
|
|
4683
|
-
return Promise.reject(new Error("file is not defined!!!"));
|
|
4684
|
-
}
|
|
4685
|
-
return new Promise((resolve2, reject) => {
|
|
4686
|
-
if (typeof FileReader === "undefined") {
|
|
4687
|
-
reject(new Error("FileReader is not available in this environment"));
|
|
4688
|
-
return;
|
|
4689
|
-
}
|
|
4690
|
-
const reader = new FileReader;
|
|
4691
|
-
reader.onerror = () => reject(reader.error || new Error("Failed to read file"));
|
|
4692
|
-
reader.onload = () => {
|
|
4693
|
-
try {
|
|
4694
|
-
const text = reader.result;
|
|
4695
|
-
const result = this.csvToJson.csvToJson(String(text));
|
|
4696
|
-
resolve2(result);
|
|
4697
|
-
} catch (err) {
|
|
4698
|
-
reject(err);
|
|
4699
|
-
}
|
|
4700
|
-
};
|
|
4701
|
-
if (options.encoding) {
|
|
4702
|
-
reader.readAsText(file2, options.encoding);
|
|
4703
|
-
} else {
|
|
4704
|
-
reader.readAsText(file2);
|
|
4705
|
-
}
|
|
4706
|
-
});
|
|
4707
|
-
}
|
|
4708
|
-
}
|
|
4709
|
-
module.exports = new BrowserApi;
|
|
4710
|
-
});
|
|
4711
|
-
|
|
4712
|
-
// node_modules/convert-csv-to-json/index.js
|
|
4713
|
-
var require_convert_csv_to_json = __commonJS((exports) => {
|
|
4714
|
-
var csvToJson = require_csvToJson();
|
|
4715
|
-
var encodingOps = {
|
|
4716
|
-
utf8: "utf8",
|
|
4717
|
-
ucs2: "ucs2",
|
|
4718
|
-
utf16le: "utf16le",
|
|
4719
|
-
latin1: "latin1",
|
|
4720
|
-
ascii: "ascii",
|
|
4721
|
-
base64: "base64",
|
|
4722
|
-
hex: "hex"
|
|
4723
|
-
};
|
|
4724
|
-
exports.formatValueByType = function(active = true) {
|
|
4725
|
-
csvToJson.formatValueByType(active);
|
|
4726
|
-
return this;
|
|
4727
|
-
};
|
|
4728
|
-
exports.supportQuotedField = function(active = false) {
|
|
4729
|
-
csvToJson.supportQuotedField(active);
|
|
4730
|
-
return this;
|
|
4731
|
-
};
|
|
4732
|
-
exports.fieldDelimiter = function(delimiter) {
|
|
4733
|
-
csvToJson.fieldDelimiter(delimiter);
|
|
4734
|
-
return this;
|
|
4735
|
-
};
|
|
4736
|
-
exports.trimHeaderFieldWhiteSpace = function(active = false) {
|
|
4737
|
-
csvToJson.trimHeaderFieldWhiteSpace(active);
|
|
4738
|
-
return this;
|
|
4739
|
-
};
|
|
4740
|
-
exports.indexHeader = function(index) {
|
|
4741
|
-
csvToJson.indexHeader(index);
|
|
4742
|
-
return this;
|
|
4743
|
-
};
|
|
4744
|
-
exports.parseSubArray = function(delimiter, separator) {
|
|
4745
|
-
csvToJson.parseSubArray(delimiter, separator);
|
|
4746
|
-
return this;
|
|
4747
|
-
};
|
|
4748
|
-
exports.customEncoding = function(encoding) {
|
|
4749
|
-
csvToJson.encoding = encoding;
|
|
4750
|
-
return this;
|
|
4751
|
-
};
|
|
4752
|
-
exports.utf8Encoding = function utf8Encoding() {
|
|
4753
|
-
csvToJson.encoding = encodingOps.utf8;
|
|
4754
|
-
return this;
|
|
4755
|
-
};
|
|
4756
|
-
exports.ucs2Encoding = function() {
|
|
4757
|
-
csvToJson.encoding = encodingOps.ucs2;
|
|
4758
|
-
return this;
|
|
4759
|
-
};
|
|
4760
|
-
exports.utf16leEncoding = function() {
|
|
4761
|
-
csvToJson.encoding = encodingOps.utf16le;
|
|
4762
|
-
return this;
|
|
4763
|
-
};
|
|
4764
|
-
exports.latin1Encoding = function() {
|
|
4765
|
-
csvToJson.encoding = encodingOps.latin1;
|
|
4766
|
-
return this;
|
|
4767
|
-
};
|
|
4768
|
-
exports.asciiEncoding = function() {
|
|
4769
|
-
csvToJson.encoding = encodingOps.ascii;
|
|
4770
|
-
return this;
|
|
4771
|
-
};
|
|
4772
|
-
exports.base64Encoding = function() {
|
|
4773
|
-
this.csvToJson = encodingOps.base64;
|
|
4774
|
-
return this;
|
|
4775
|
-
};
|
|
4776
|
-
exports.hexEncoding = function() {
|
|
4777
|
-
this.csvToJson = encodingOps.hex;
|
|
4778
|
-
return this;
|
|
4779
|
-
};
|
|
4780
|
-
exports.generateJsonFileFromCsv = function(inputFileName, outputFileName) {
|
|
4781
|
-
if (!inputFileName) {
|
|
4782
|
-
throw new Error("inputFileName is not defined!!!");
|
|
4783
|
-
}
|
|
4784
|
-
if (!outputFileName) {
|
|
4785
|
-
throw new Error("outputFileName is not defined!!!");
|
|
4786
|
-
}
|
|
4787
|
-
csvToJson.generateJsonFileFromCsv(inputFileName, outputFileName);
|
|
4788
|
-
};
|
|
4789
|
-
exports.getJsonFromCsv = function(inputFileName) {
|
|
4790
|
-
if (!inputFileName) {
|
|
4791
|
-
throw new Error("inputFileName is not defined!!!");
|
|
4792
|
-
}
|
|
4793
|
-
return csvToJson.getJsonFromCsv(inputFileName);
|
|
4794
|
-
};
|
|
4795
|
-
var csvToJsonAsync = require_csvToJsonAsync();
|
|
4796
|
-
Object.assign(exports, {
|
|
4797
|
-
getJsonFromCsvAsync: function(input, options) {
|
|
4798
|
-
return csvToJsonAsync.getJsonFromCsvAsync(input, options);
|
|
4799
|
-
},
|
|
4800
|
-
csvStringToJsonAsync: function(input, options) {
|
|
4801
|
-
return csvToJsonAsync.csvStringToJsonAsync(input, options);
|
|
4802
|
-
},
|
|
4803
|
-
csvStringToJsonStringifiedAsync: function(input) {
|
|
4804
|
-
return csvToJsonAsync.csvStringToJsonStringifiedAsync(input);
|
|
4805
|
-
},
|
|
4806
|
-
generateJsonFileFromCsvAsync: function(input, output) {
|
|
4807
|
-
return csvToJsonAsync.generateJsonFileFromCsv(input, output);
|
|
4808
|
-
}
|
|
4809
|
-
});
|
|
4810
|
-
exports.csvStringToJson = function(csvString) {
|
|
4811
|
-
return csvToJson.csvStringToJson(csvString);
|
|
4812
|
-
};
|
|
4813
|
-
exports.csvStringToJsonStringified = function(csvString) {
|
|
4814
|
-
if (csvString === undefined || csvString === null) {
|
|
4815
|
-
throw new Error("csvString is not defined!!!");
|
|
4816
|
-
}
|
|
4817
|
-
return csvToJson.csvStringToJsonStringified(csvString);
|
|
4818
|
-
};
|
|
4819
|
-
exports.jsonToCsv = function(inputFileName, outputFileName) {
|
|
4820
|
-
csvToJson.generateJsonFileFromCsv(inputFileName, outputFileName);
|
|
4821
|
-
};
|
|
4822
|
-
exports.browser = require_browserApi();
|
|
4823
|
-
});
|
|
4824
|
-
|
|
4825
4225
|
// src/utils/accountSuggester.ts
|
|
4826
4226
|
var exports_accountSuggester = {};
|
|
4827
4227
|
__export(exports_accountSuggester, {
|
|
@@ -4831,7 +4231,7 @@ __export(exports_accountSuggester, {
|
|
|
4831
4231
|
extractRulePatternsFromFile: () => extractRulePatternsFromFile,
|
|
4832
4232
|
clearSuggestionCache: () => clearSuggestionCache
|
|
4833
4233
|
});
|
|
4834
|
-
import * as
|
|
4234
|
+
import * as fs14 from "fs";
|
|
4835
4235
|
import * as crypto from "crypto";
|
|
4836
4236
|
function clearSuggestionCache() {
|
|
4837
4237
|
Object.keys(suggestionCache).forEach((key) => delete suggestionCache[key]);
|
|
@@ -4840,11 +4240,11 @@ function hashTransaction(posting) {
|
|
|
4840
4240
|
const data = `${posting.description}|${posting.amount}|${posting.account}`;
|
|
4841
4241
|
return crypto.createHash("md5").update(data).digest("hex");
|
|
4842
4242
|
}
|
|
4843
|
-
|
|
4844
|
-
if (!
|
|
4243
|
+
function loadExistingAccounts(yearJournalPath) {
|
|
4244
|
+
if (!fs14.existsSync(yearJournalPath)) {
|
|
4845
4245
|
return [];
|
|
4846
4246
|
}
|
|
4847
|
-
const content =
|
|
4247
|
+
const content = fs14.readFileSync(yearJournalPath, "utf-8");
|
|
4848
4248
|
const lines = content.split(`
|
|
4849
4249
|
`);
|
|
4850
4250
|
const accounts = [];
|
|
@@ -4859,11 +4259,11 @@ async function loadExistingAccounts(yearJournalPath) {
|
|
|
4859
4259
|
}
|
|
4860
4260
|
return accounts.sort();
|
|
4861
4261
|
}
|
|
4862
|
-
|
|
4863
|
-
if (!
|
|
4262
|
+
function extractRulePatternsFromFile(rulesPath) {
|
|
4263
|
+
if (!fs14.existsSync(rulesPath)) {
|
|
4864
4264
|
return [];
|
|
4865
4265
|
}
|
|
4866
|
-
const content =
|
|
4266
|
+
const content = fs14.readFileSync(rulesPath, "utf-8");
|
|
4867
4267
|
const lines = content.split(`
|
|
4868
4268
|
`);
|
|
4869
4269
|
const patterns = [];
|
|
@@ -4914,7 +4314,7 @@ function buildBatchSuggestionPrompt(postings, context) {
|
|
|
4914
4314
|
prompt += `## Example Classification Patterns from Rules
|
|
4915
4315
|
|
|
4916
4316
|
`;
|
|
4917
|
-
const sampleSize = Math.min(
|
|
4317
|
+
const sampleSize = Math.min(EXAMPLE_PATTERN_SAMPLE_SIZE, context.existingRules.length);
|
|
4918
4318
|
for (let i2 = 0;i2 < sampleSize; i2++) {
|
|
4919
4319
|
const pattern = context.existingRules[i2];
|
|
4920
4320
|
prompt += `- If description matches "${pattern.condition}" \u2192 ${pattern.account}
|
|
@@ -5092,7 +4492,7 @@ function generateMockSuggestions(postings) {
|
|
|
5092
4492
|
});
|
|
5093
4493
|
return response;
|
|
5094
4494
|
}
|
|
5095
|
-
var suggestionCache;
|
|
4495
|
+
var EXAMPLE_PATTERN_SAMPLE_SIZE = 10, suggestionCache;
|
|
5096
4496
|
var init_accountSuggester = __esm(() => {
|
|
5097
4497
|
init_agentLoader();
|
|
5098
4498
|
suggestionCache = {};
|
|
@@ -5100,7 +4500,7 @@ var init_accountSuggester = __esm(() => {
|
|
|
5100
4500
|
|
|
5101
4501
|
// src/index.ts
|
|
5102
4502
|
init_agentLoader();
|
|
5103
|
-
import { dirname as
|
|
4503
|
+
import { dirname as dirname4, join as join12 } from "path";
|
|
5104
4504
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
5105
4505
|
|
|
5106
4506
|
// node_modules/zod/v4/classic/external.js
|
|
@@ -17425,7 +16825,7 @@ function tool(input) {
|
|
|
17425
16825
|
tool.schema = exports_external;
|
|
17426
16826
|
// src/tools/fetch-currency-prices.ts
|
|
17427
16827
|
var {$ } = globalThis.Bun;
|
|
17428
|
-
import * as
|
|
16828
|
+
import * as path4 from "path";
|
|
17429
16829
|
|
|
17430
16830
|
// src/utils/agentRestriction.ts
|
|
17431
16831
|
function checkAccountantAgent(agent, toolPrompt, additionalFields) {
|
|
@@ -17442,10 +16842,32 @@ function checkAccountantAgent(agent, toolPrompt, additionalFields) {
|
|
|
17442
16842
|
return JSON.stringify(errorResponse);
|
|
17443
16843
|
}
|
|
17444
16844
|
|
|
17445
|
-
// src/utils/
|
|
16845
|
+
// src/utils/yamlLoader.ts
|
|
17446
16846
|
init_js_yaml();
|
|
17447
|
-
import * as
|
|
16847
|
+
import * as fs2 from "fs";
|
|
17448
16848
|
import * as path from "path";
|
|
16849
|
+
function loadYamlConfig(directory, configFile, validator, notFoundMessage) {
|
|
16850
|
+
const configPath = path.join(directory, configFile);
|
|
16851
|
+
if (!fs2.existsSync(configPath)) {
|
|
16852
|
+
throw new Error(notFoundMessage || `Configuration file not found: ${configFile}. Please create this file to configure the feature.`);
|
|
16853
|
+
}
|
|
16854
|
+
let parsed;
|
|
16855
|
+
try {
|
|
16856
|
+
const content = fs2.readFileSync(configPath, "utf-8");
|
|
16857
|
+
parsed = jsYaml.load(content);
|
|
16858
|
+
} catch (err) {
|
|
16859
|
+
if (err instanceof jsYaml.YAMLException) {
|
|
16860
|
+
throw new Error(`Failed to parse ${configFile}: ${err.message}`);
|
|
16861
|
+
}
|
|
16862
|
+
throw err;
|
|
16863
|
+
}
|
|
16864
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
16865
|
+
throw new Error(`Invalid config: ${configFile} must contain a YAML object`);
|
|
16866
|
+
}
|
|
16867
|
+
return validator(parsed);
|
|
16868
|
+
}
|
|
16869
|
+
|
|
16870
|
+
// src/utils/pricesConfig.ts
|
|
17449
16871
|
var CONFIG_FILE = "config/prices.yaml";
|
|
17450
16872
|
var REQUIRED_CURRENCY_FIELDS = ["source", "pair", "file"];
|
|
17451
16873
|
function getDefaultBackfillDate() {
|
|
@@ -17471,48 +16893,84 @@ function validateCurrencyConfig(name, config2) {
|
|
|
17471
16893
|
};
|
|
17472
16894
|
}
|
|
17473
16895
|
function loadPricesConfig(directory) {
|
|
17474
|
-
|
|
17475
|
-
|
|
17476
|
-
|
|
17477
|
-
}
|
|
17478
|
-
let parsed;
|
|
17479
|
-
try {
|
|
17480
|
-
const content = fs.readFileSync(configPath, "utf-8");
|
|
17481
|
-
parsed = jsYaml.load(content);
|
|
17482
|
-
} catch (err) {
|
|
17483
|
-
if (err instanceof jsYaml.YAMLException) {
|
|
17484
|
-
throw new Error(`Failed to parse ${CONFIG_FILE}: ${err.message}`);
|
|
16896
|
+
return loadYamlConfig(directory, CONFIG_FILE, (parsedObj) => {
|
|
16897
|
+
if (!parsedObj.currencies || typeof parsedObj.currencies !== "object") {
|
|
16898
|
+
throw new Error(`Invalid config: 'currencies' section is required`);
|
|
17485
16899
|
}
|
|
17486
|
-
|
|
16900
|
+
const currenciesObj = parsedObj.currencies;
|
|
16901
|
+
if (Object.keys(currenciesObj).length === 0) {
|
|
16902
|
+
throw new Error(`Invalid config: 'currencies' section must contain at least one currency`);
|
|
16903
|
+
}
|
|
16904
|
+
const currencies = {};
|
|
16905
|
+
for (const [name, config2] of Object.entries(currenciesObj)) {
|
|
16906
|
+
currencies[name] = validateCurrencyConfig(name, config2);
|
|
16907
|
+
}
|
|
16908
|
+
return { currencies };
|
|
16909
|
+
}, `Configuration file not found: ${CONFIG_FILE}. Please refer to the plugin's GitHub repository for setup instructions.`);
|
|
16910
|
+
}
|
|
16911
|
+
|
|
16912
|
+
// src/utils/journalUtils.ts
|
|
16913
|
+
import * as fs4 from "fs";
|
|
16914
|
+
import * as path3 from "path";
|
|
16915
|
+
|
|
16916
|
+
// src/utils/fileUtils.ts
|
|
16917
|
+
import * as fs3 from "fs";
|
|
16918
|
+
import * as path2 from "path";
|
|
16919
|
+
function findCsvFiles(baseDir, options = {}) {
|
|
16920
|
+
if (!fs3.existsSync(baseDir)) {
|
|
16921
|
+
return [];
|
|
17487
16922
|
}
|
|
17488
|
-
|
|
17489
|
-
|
|
16923
|
+
let searchDir = baseDir;
|
|
16924
|
+
if (options.subdir) {
|
|
16925
|
+
searchDir = path2.join(searchDir, options.subdir);
|
|
16926
|
+
if (options.subsubdir) {
|
|
16927
|
+
searchDir = path2.join(searchDir, options.subsubdir);
|
|
16928
|
+
}
|
|
17490
16929
|
}
|
|
17491
|
-
|
|
17492
|
-
|
|
17493
|
-
throw new Error(`Invalid config: 'currencies' section is required`);
|
|
16930
|
+
if (!fs3.existsSync(searchDir)) {
|
|
16931
|
+
return [];
|
|
17494
16932
|
}
|
|
17495
|
-
const
|
|
17496
|
-
if (
|
|
17497
|
-
|
|
16933
|
+
const csvFiles = [];
|
|
16934
|
+
if (options.recursive) {
|
|
16935
|
+
let scanDirectory = function(dir) {
|
|
16936
|
+
const entries = fs3.readdirSync(dir, { withFileTypes: true });
|
|
16937
|
+
for (const entry of entries) {
|
|
16938
|
+
const fullPath = path2.join(dir, entry.name);
|
|
16939
|
+
if (entry.isDirectory()) {
|
|
16940
|
+
scanDirectory(fullPath);
|
|
16941
|
+
} else if (entry.isFile() && entry.name.toLowerCase().endsWith(".csv")) {
|
|
16942
|
+
csvFiles.push(options.fullPaths ? fullPath : entry.name);
|
|
16943
|
+
}
|
|
16944
|
+
}
|
|
16945
|
+
};
|
|
16946
|
+
scanDirectory(searchDir);
|
|
16947
|
+
} else {
|
|
16948
|
+
const entries = fs3.readdirSync(searchDir);
|
|
16949
|
+
for (const name of entries) {
|
|
16950
|
+
if (!name.toLowerCase().endsWith(".csv"))
|
|
16951
|
+
continue;
|
|
16952
|
+
const fullPath = path2.join(searchDir, name);
|
|
16953
|
+
if (fs3.statSync(fullPath).isFile()) {
|
|
16954
|
+
csvFiles.push(options.fullPaths ? fullPath : name);
|
|
16955
|
+
}
|
|
16956
|
+
}
|
|
17498
16957
|
}
|
|
17499
|
-
|
|
17500
|
-
|
|
17501
|
-
|
|
16958
|
+
return csvFiles.sort();
|
|
16959
|
+
}
|
|
16960
|
+
function ensureDirectory(dirPath) {
|
|
16961
|
+
if (!fs3.existsSync(dirPath)) {
|
|
16962
|
+
fs3.mkdirSync(dirPath, { recursive: true });
|
|
17502
16963
|
}
|
|
17503
|
-
return { currencies };
|
|
17504
16964
|
}
|
|
17505
16965
|
|
|
17506
16966
|
// src/utils/journalUtils.ts
|
|
17507
|
-
import * as fs2 from "fs";
|
|
17508
|
-
import * as path2 from "path";
|
|
17509
16967
|
function extractDateFromPriceLine(line) {
|
|
17510
16968
|
return line.split(" ")[1];
|
|
17511
16969
|
}
|
|
17512
16970
|
function updatePriceJournal(journalPath, newPriceLines) {
|
|
17513
16971
|
let existingLines = [];
|
|
17514
|
-
if (
|
|
17515
|
-
existingLines =
|
|
16972
|
+
if (fs4.existsSync(journalPath)) {
|
|
16973
|
+
existingLines = fs4.readFileSync(journalPath, "utf-8").split(`
|
|
17516
16974
|
`).filter((line) => line.trim() !== "");
|
|
17517
16975
|
}
|
|
17518
16976
|
const priceMap = new Map;
|
|
@@ -17527,54 +16985,23 @@ function updatePriceJournal(journalPath, newPriceLines) {
|
|
|
17527
16985
|
priceMap.set(date5, line);
|
|
17528
16986
|
}
|
|
17529
16987
|
const sortedLines = Array.from(priceMap.entries()).sort((a, b) => a[0].localeCompare(b[0])).map(([, line]) => line);
|
|
17530
|
-
|
|
16988
|
+
fs4.writeFileSync(journalPath, sortedLines.join(`
|
|
17531
16989
|
`) + `
|
|
17532
16990
|
`);
|
|
17533
16991
|
}
|
|
17534
|
-
function findCsvFiles(directory, provider, currency) {
|
|
17535
|
-
const csvFiles = [];
|
|
17536
|
-
if (!fs2.existsSync(directory)) {
|
|
17537
|
-
return csvFiles;
|
|
17538
|
-
}
|
|
17539
|
-
let searchPath = directory;
|
|
17540
|
-
if (provider) {
|
|
17541
|
-
searchPath = path2.join(searchPath, provider);
|
|
17542
|
-
if (currency) {
|
|
17543
|
-
searchPath = path2.join(searchPath, currency);
|
|
17544
|
-
}
|
|
17545
|
-
}
|
|
17546
|
-
if (!fs2.existsSync(searchPath)) {
|
|
17547
|
-
return csvFiles;
|
|
17548
|
-
}
|
|
17549
|
-
function scanDirectory(dir) {
|
|
17550
|
-
const entries = fs2.readdirSync(dir, { withFileTypes: true });
|
|
17551
|
-
for (const entry of entries) {
|
|
17552
|
-
const fullPath = path2.join(dir, entry.name);
|
|
17553
|
-
if (entry.isDirectory()) {
|
|
17554
|
-
scanDirectory(fullPath);
|
|
17555
|
-
} else if (entry.isFile() && entry.name.endsWith(".csv")) {
|
|
17556
|
-
csvFiles.push(fullPath);
|
|
17557
|
-
}
|
|
17558
|
-
}
|
|
17559
|
-
}
|
|
17560
|
-
scanDirectory(searchPath);
|
|
17561
|
-
return csvFiles.sort();
|
|
17562
|
-
}
|
|
17563
16992
|
function ensureYearJournalExists(directory, year) {
|
|
17564
|
-
const ledgerDir =
|
|
17565
|
-
const yearJournalPath =
|
|
17566
|
-
const mainJournalPath =
|
|
17567
|
-
|
|
17568
|
-
|
|
17569
|
-
|
|
17570
|
-
if (!fs2.existsSync(yearJournalPath)) {
|
|
17571
|
-
fs2.writeFileSync(yearJournalPath, `; ${year} transactions
|
|
16993
|
+
const ledgerDir = path3.join(directory, "ledger");
|
|
16994
|
+
const yearJournalPath = path3.join(ledgerDir, `${year}.journal`);
|
|
16995
|
+
const mainJournalPath = path3.join(directory, ".hledger.journal");
|
|
16996
|
+
ensureDirectory(ledgerDir);
|
|
16997
|
+
if (!fs4.existsSync(yearJournalPath)) {
|
|
16998
|
+
fs4.writeFileSync(yearJournalPath, `; ${year} transactions
|
|
17572
16999
|
`);
|
|
17573
17000
|
}
|
|
17574
|
-
if (!
|
|
17001
|
+
if (!fs4.existsSync(mainJournalPath)) {
|
|
17575
17002
|
throw new Error(`.hledger.journal not found at ${mainJournalPath}. Create it first with appropriate includes.`);
|
|
17576
17003
|
}
|
|
17577
|
-
const mainJournalContent =
|
|
17004
|
+
const mainJournalContent = fs4.readFileSync(mainJournalPath, "utf-8");
|
|
17578
17005
|
const includeDirective = `include ledger/${year}.journal`;
|
|
17579
17006
|
const lines = mainJournalContent.split(`
|
|
17580
17007
|
`);
|
|
@@ -17586,7 +17013,7 @@ function ensureYearJournalExists(directory, year) {
|
|
|
17586
17013
|
const newContent = mainJournalContent.trimEnd() + `
|
|
17587
17014
|
` + includeDirective + `
|
|
17588
17015
|
`;
|
|
17589
|
-
|
|
17016
|
+
fs4.writeFileSync(mainJournalPath, newContent);
|
|
17590
17017
|
}
|
|
17591
17018
|
return yearJournalPath;
|
|
17592
17019
|
}
|
|
@@ -17606,6 +17033,30 @@ function getNextDay(dateStr) {
|
|
|
17606
17033
|
return formatDateISO(date5);
|
|
17607
17034
|
}
|
|
17608
17035
|
|
|
17036
|
+
// src/utils/resultHelpers.ts
|
|
17037
|
+
function buildToolErrorResult(error45, hint, extra) {
|
|
17038
|
+
const result = {
|
|
17039
|
+
success: false,
|
|
17040
|
+
error: error45
|
|
17041
|
+
};
|
|
17042
|
+
if (hint) {
|
|
17043
|
+
result.hint = hint;
|
|
17044
|
+
}
|
|
17045
|
+
if (extra) {
|
|
17046
|
+
Object.assign(result, extra);
|
|
17047
|
+
}
|
|
17048
|
+
return JSON.stringify(result);
|
|
17049
|
+
}
|
|
17050
|
+
function buildToolSuccessResult(data) {
|
|
17051
|
+
const result = {
|
|
17052
|
+
success: true
|
|
17053
|
+
};
|
|
17054
|
+
if (data) {
|
|
17055
|
+
Object.assign(result, data);
|
|
17056
|
+
}
|
|
17057
|
+
return JSON.stringify(result);
|
|
17058
|
+
}
|
|
17059
|
+
|
|
17609
17060
|
// src/tools/fetch-currency-prices.ts
|
|
17610
17061
|
async function defaultPriceFetcher(cmdArgs) {
|
|
17611
17062
|
const result = await $`pricehist ${cmdArgs}`.quiet();
|
|
@@ -17629,10 +17080,10 @@ function buildPricehistArgs(startDate, endDate, currencyConfig) {
|
|
|
17629
17080
|
return cmdArgs;
|
|
17630
17081
|
}
|
|
17631
17082
|
function buildErrorResult(error45) {
|
|
17632
|
-
return
|
|
17083
|
+
return buildToolErrorResult(error45);
|
|
17633
17084
|
}
|
|
17634
17085
|
function buildSuccessResult(results, endDate, backfill) {
|
|
17635
|
-
return
|
|
17086
|
+
return buildToolSuccessResult({
|
|
17636
17087
|
success: results.every((r) => !("error" in r)),
|
|
17637
17088
|
endDate,
|
|
17638
17089
|
backfill,
|
|
@@ -17648,7 +17099,7 @@ function parsePriceLine(line) {
|
|
|
17648
17099
|
formattedLine: line
|
|
17649
17100
|
};
|
|
17650
17101
|
}
|
|
17651
|
-
function
|
|
17102
|
+
function filterAndSortPriceLinesByDateRange(priceLines, startDate, endDate) {
|
|
17652
17103
|
return priceLines.map(parsePriceLine).filter((parsed) => {
|
|
17653
17104
|
if (!parsed)
|
|
17654
17105
|
return false;
|
|
@@ -17684,7 +17135,7 @@ async function fetchCurrencyPrices(directory, agent, backfill, priceFetcher = de
|
|
|
17684
17135
|
});
|
|
17685
17136
|
continue;
|
|
17686
17137
|
}
|
|
17687
|
-
const priceLines =
|
|
17138
|
+
const priceLines = filterAndSortPriceLinesByDateRange(rawPriceLines, startDate, endDate);
|
|
17688
17139
|
if (priceLines.length === 0) {
|
|
17689
17140
|
results.push({
|
|
17690
17141
|
ticker,
|
|
@@ -17692,7 +17143,7 @@ async function fetchCurrencyPrices(directory, agent, backfill, priceFetcher = de
|
|
|
17692
17143
|
});
|
|
17693
17144
|
continue;
|
|
17694
17145
|
}
|
|
17695
|
-
const journalPath =
|
|
17146
|
+
const journalPath = path4.join(directory, "ledger", "currencies", currencyConfig.file);
|
|
17696
17147
|
updatePriceJournal(journalPath, priceLines);
|
|
17697
17148
|
const latestPriceLine = priceLines[priceLines.length - 1];
|
|
17698
17149
|
results.push({
|
|
@@ -17721,13 +17172,10 @@ var fetch_currency_prices_default = tool({
|
|
|
17721
17172
|
}
|
|
17722
17173
|
});
|
|
17723
17174
|
// src/tools/classify-statements.ts
|
|
17724
|
-
import * as
|
|
17175
|
+
import * as fs6 from "fs";
|
|
17725
17176
|
import * as path6 from "path";
|
|
17726
17177
|
|
|
17727
17178
|
// src/utils/importConfig.ts
|
|
17728
|
-
init_js_yaml();
|
|
17729
|
-
import * as fs3 from "fs";
|
|
17730
|
-
import * as path4 from "path";
|
|
17731
17179
|
var CONFIG_FILE2 = "config/import/providers.yaml";
|
|
17732
17180
|
var REQUIRED_PATH_FIELDS = [
|
|
17733
17181
|
"import",
|
|
@@ -17850,43 +17298,27 @@ function validateProviderConfig(name, config2) {
|
|
|
17850
17298
|
return { detect, currencies };
|
|
17851
17299
|
}
|
|
17852
17300
|
function loadImportConfig(directory) {
|
|
17853
|
-
|
|
17854
|
-
|
|
17855
|
-
|
|
17856
|
-
}
|
|
17857
|
-
let parsed;
|
|
17858
|
-
try {
|
|
17859
|
-
const content = fs3.readFileSync(configPath, "utf-8");
|
|
17860
|
-
parsed = jsYaml.load(content);
|
|
17861
|
-
} catch (err) {
|
|
17862
|
-
if (err instanceof jsYaml.YAMLException) {
|
|
17863
|
-
throw new Error(`Failed to parse ${CONFIG_FILE2}: ${err.message}`);
|
|
17301
|
+
return loadYamlConfig(directory, CONFIG_FILE2, (parsedObj) => {
|
|
17302
|
+
if (!parsedObj.paths) {
|
|
17303
|
+
throw new Error("Invalid config: 'paths' section is required");
|
|
17864
17304
|
}
|
|
17865
|
-
|
|
17866
|
-
|
|
17867
|
-
|
|
17868
|
-
|
|
17869
|
-
|
|
17870
|
-
|
|
17871
|
-
|
|
17872
|
-
|
|
17873
|
-
|
|
17874
|
-
|
|
17875
|
-
|
|
17876
|
-
|
|
17877
|
-
|
|
17878
|
-
|
|
17879
|
-
|
|
17880
|
-
|
|
17881
|
-
}
|
|
17882
|
-
const providers = {};
|
|
17883
|
-
for (const [name, config2] of Object.entries(providersObj)) {
|
|
17884
|
-
providers[name] = validateProviderConfig(name, config2);
|
|
17885
|
-
}
|
|
17886
|
-
if (!paths.logs) {
|
|
17887
|
-
paths.logs = ".memory";
|
|
17888
|
-
}
|
|
17889
|
-
return { paths, providers };
|
|
17305
|
+
const paths = validatePaths(parsedObj.paths);
|
|
17306
|
+
if (!parsedObj.providers || typeof parsedObj.providers !== "object") {
|
|
17307
|
+
throw new Error("Invalid config: 'providers' section is required");
|
|
17308
|
+
}
|
|
17309
|
+
const providersObj = parsedObj.providers;
|
|
17310
|
+
if (Object.keys(providersObj).length === 0) {
|
|
17311
|
+
throw new Error("Invalid config: 'providers' section must contain at least one provider");
|
|
17312
|
+
}
|
|
17313
|
+
const providers = {};
|
|
17314
|
+
for (const [name, config2] of Object.entries(providersObj)) {
|
|
17315
|
+
providers[name] = validateProviderConfig(name, config2);
|
|
17316
|
+
}
|
|
17317
|
+
if (!paths.logs) {
|
|
17318
|
+
paths.logs = ".memory";
|
|
17319
|
+
}
|
|
17320
|
+
return { paths, providers };
|
|
17321
|
+
}, `Configuration file not found: ${CONFIG_FILE2}. Please create this file to configure statement imports.`);
|
|
17890
17322
|
}
|
|
17891
17323
|
|
|
17892
17324
|
// src/utils/providerDetector.ts
|
|
@@ -17994,28 +17426,85 @@ function detectProvider(filename, content, config2) {
|
|
|
17994
17426
|
return null;
|
|
17995
17427
|
}
|
|
17996
17428
|
|
|
17997
|
-
// src/utils/
|
|
17998
|
-
import * as
|
|
17429
|
+
// src/utils/importContext.ts
|
|
17430
|
+
import * as fs5 from "fs";
|
|
17999
17431
|
import * as path5 from "path";
|
|
18000
|
-
|
|
18001
|
-
|
|
18002
|
-
|
|
17432
|
+
import { randomUUID } from "crypto";
|
|
17433
|
+
function getContextPath(directory, contextId) {
|
|
17434
|
+
return path5.join(directory, ".memory", `${contextId}.json`);
|
|
17435
|
+
}
|
|
17436
|
+
function ensureMemoryDir(directory) {
|
|
17437
|
+
ensureDirectory(path5.join(directory, ".memory"));
|
|
17438
|
+
}
|
|
17439
|
+
function createContext(directory, params) {
|
|
17440
|
+
const now = new Date().toISOString();
|
|
17441
|
+
const context = {
|
|
17442
|
+
id: randomUUID(),
|
|
17443
|
+
createdAt: now,
|
|
17444
|
+
updatedAt: now,
|
|
17445
|
+
filename: params.filename,
|
|
17446
|
+
filePath: params.filePath,
|
|
17447
|
+
provider: params.provider,
|
|
17448
|
+
currency: params.currency,
|
|
17449
|
+
accountNumber: params.accountNumber,
|
|
17450
|
+
originalFilename: params.originalFilename,
|
|
17451
|
+
fromDate: params.fromDate,
|
|
17452
|
+
untilDate: params.untilDate,
|
|
17453
|
+
openingBalance: params.openingBalance,
|
|
17454
|
+
closingBalance: params.closingBalance,
|
|
17455
|
+
account: params.account
|
|
17456
|
+
};
|
|
17457
|
+
ensureMemoryDir(directory);
|
|
17458
|
+
const contextPath = getContextPath(directory, context.id);
|
|
17459
|
+
fs5.writeFileSync(contextPath, JSON.stringify(context, null, 2), "utf-8");
|
|
17460
|
+
return context;
|
|
17461
|
+
}
|
|
17462
|
+
function validateContext(context, contextId) {
|
|
17463
|
+
const requiredFields = [
|
|
17464
|
+
"id",
|
|
17465
|
+
"filename",
|
|
17466
|
+
"filePath",
|
|
17467
|
+
"provider",
|
|
17468
|
+
"currency"
|
|
17469
|
+
];
|
|
17470
|
+
for (const field of requiredFields) {
|
|
17471
|
+
if (!context[field]) {
|
|
17472
|
+
throw new Error(`Invalid context ${contextId}: missing required field '${field}'`);
|
|
17473
|
+
}
|
|
18003
17474
|
}
|
|
18004
|
-
return fs4.readdirSync(importsDir).filter((file2) => file2.toLowerCase().endsWith(".csv")).filter((file2) => {
|
|
18005
|
-
const fullPath = path5.join(importsDir, file2);
|
|
18006
|
-
return fs4.statSync(fullPath).isFile();
|
|
18007
|
-
});
|
|
18008
17475
|
}
|
|
18009
|
-
function
|
|
18010
|
-
|
|
18011
|
-
|
|
17476
|
+
function loadContext(directory, contextId) {
|
|
17477
|
+
const contextPath = getContextPath(directory, contextId);
|
|
17478
|
+
if (!fs5.existsSync(contextPath)) {
|
|
17479
|
+
throw new Error(`Context not found: ${contextId}`);
|
|
18012
17480
|
}
|
|
17481
|
+
const content = fs5.readFileSync(contextPath, "utf-8");
|
|
17482
|
+
let context;
|
|
17483
|
+
try {
|
|
17484
|
+
context = JSON.parse(content);
|
|
17485
|
+
} catch (err) {
|
|
17486
|
+
throw new Error(`Malformed context file ${contextId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
17487
|
+
}
|
|
17488
|
+
validateContext(context, contextId);
|
|
17489
|
+
return context;
|
|
17490
|
+
}
|
|
17491
|
+
function updateContext(directory, contextId, updates) {
|
|
17492
|
+
const context = loadContext(directory, contextId);
|
|
17493
|
+
const updatedContext = {
|
|
17494
|
+
...context,
|
|
17495
|
+
...updates,
|
|
17496
|
+
id: context.id,
|
|
17497
|
+
createdAt: context.createdAt,
|
|
17498
|
+
updatedAt: new Date().toISOString()
|
|
17499
|
+
};
|
|
17500
|
+
const contextPath = getContextPath(directory, contextId);
|
|
17501
|
+
fs5.writeFileSync(contextPath, JSON.stringify(updatedContext, null, 2), "utf-8");
|
|
17502
|
+
return updatedContext;
|
|
18013
17503
|
}
|
|
18014
17504
|
|
|
18015
17505
|
// src/tools/classify-statements.ts
|
|
18016
17506
|
function buildSuccessResult2(classified, unrecognized, message) {
|
|
18017
|
-
return
|
|
18018
|
-
success: true,
|
|
17507
|
+
return buildToolSuccessResult({
|
|
18019
17508
|
classified,
|
|
18020
17509
|
unrecognized,
|
|
18021
17510
|
message,
|
|
@@ -18027,19 +17516,14 @@ function buildSuccessResult2(classified, unrecognized, message) {
|
|
|
18027
17516
|
});
|
|
18028
17517
|
}
|
|
18029
17518
|
function buildErrorResult2(error45, hint) {
|
|
18030
|
-
return
|
|
18031
|
-
success: false,
|
|
18032
|
-
error: error45,
|
|
18033
|
-
hint,
|
|
17519
|
+
return buildToolErrorResult(error45, hint, {
|
|
18034
17520
|
classified: [],
|
|
18035
17521
|
unrecognized: []
|
|
18036
17522
|
});
|
|
18037
17523
|
}
|
|
18038
17524
|
function buildCollisionError(collisions) {
|
|
18039
17525
|
const error45 = `Cannot classify: ${collisions.length} file(s) would overwrite existing pending files.`;
|
|
18040
|
-
return
|
|
18041
|
-
success: false,
|
|
18042
|
-
error: error45,
|
|
17526
|
+
return buildToolErrorResult(error45, undefined, {
|
|
18043
17527
|
collisions,
|
|
18044
17528
|
classified: [],
|
|
18045
17529
|
unrecognized: []
|
|
@@ -18050,7 +17534,7 @@ function planMoves(csvFiles, importsDir, pendingDir, unrecognizedDir, config2) {
|
|
|
18050
17534
|
const collisions = [];
|
|
18051
17535
|
for (const filename of csvFiles) {
|
|
18052
17536
|
const sourcePath = path6.join(importsDir, filename);
|
|
18053
|
-
const content =
|
|
17537
|
+
const content = fs6.readFileSync(sourcePath, "utf-8");
|
|
18054
17538
|
const detection = detectProvider(filename, content, config2);
|
|
18055
17539
|
let targetPath;
|
|
18056
17540
|
let targetFilename;
|
|
@@ -18062,7 +17546,7 @@ function planMoves(csvFiles, importsDir, pendingDir, unrecognizedDir, config2) {
|
|
|
18062
17546
|
targetFilename = filename;
|
|
18063
17547
|
targetPath = path6.join(unrecognizedDir, filename);
|
|
18064
17548
|
}
|
|
18065
|
-
if (
|
|
17549
|
+
if (fs6.existsSync(targetPath)) {
|
|
18066
17550
|
collisions.push({
|
|
18067
17551
|
filename,
|
|
18068
17552
|
existingPath: targetPath
|
|
@@ -18078,24 +17562,52 @@ function planMoves(csvFiles, importsDir, pendingDir, unrecognizedDir, config2) {
|
|
|
18078
17562
|
}
|
|
18079
17563
|
return { plannedMoves, collisions };
|
|
18080
17564
|
}
|
|
18081
|
-
function
|
|
17565
|
+
function extractMetadata2(detection) {
|
|
17566
|
+
const metadata = detection.metadata;
|
|
17567
|
+
if (!metadata) {
|
|
17568
|
+
return {};
|
|
17569
|
+
}
|
|
17570
|
+
return {
|
|
17571
|
+
accountNumber: metadata["account-number"],
|
|
17572
|
+
fromDate: metadata["from-date"],
|
|
17573
|
+
untilDate: metadata["until-date"],
|
|
17574
|
+
openingBalance: metadata["opening-balance"],
|
|
17575
|
+
closingBalance: metadata["closing-balance"]
|
|
17576
|
+
};
|
|
17577
|
+
}
|
|
17578
|
+
function executeMoves(plannedMoves, config2, unrecognizedDir, directory) {
|
|
18082
17579
|
const classified = [];
|
|
18083
17580
|
const unrecognized = [];
|
|
18084
17581
|
for (const move of plannedMoves) {
|
|
18085
17582
|
if (move.detection) {
|
|
18086
17583
|
const targetDir = path6.dirname(move.targetPath);
|
|
18087
17584
|
ensureDirectory(targetDir);
|
|
18088
|
-
|
|
17585
|
+
fs6.renameSync(move.sourcePath, move.targetPath);
|
|
17586
|
+
const targetPath = path6.join(config2.paths.pending, move.detection.provider, move.detection.currency, move.targetFilename);
|
|
17587
|
+
const metadata = extractMetadata2(move.detection);
|
|
17588
|
+
const context = createContext(directory, {
|
|
17589
|
+
filename: move.targetFilename,
|
|
17590
|
+
filePath: targetPath,
|
|
17591
|
+
provider: move.detection.provider,
|
|
17592
|
+
currency: move.detection.currency,
|
|
17593
|
+
originalFilename: move.detection.outputFilename ? move.filename : undefined,
|
|
17594
|
+
accountNumber: metadata.accountNumber,
|
|
17595
|
+
fromDate: metadata.fromDate,
|
|
17596
|
+
untilDate: metadata.untilDate,
|
|
17597
|
+
openingBalance: metadata.openingBalance,
|
|
17598
|
+
closingBalance: metadata.closingBalance
|
|
17599
|
+
});
|
|
18089
17600
|
classified.push({
|
|
18090
17601
|
filename: move.targetFilename,
|
|
18091
17602
|
originalFilename: move.detection.outputFilename ? move.filename : undefined,
|
|
18092
17603
|
provider: move.detection.provider,
|
|
18093
17604
|
currency: move.detection.currency,
|
|
18094
|
-
targetPath
|
|
17605
|
+
targetPath,
|
|
17606
|
+
contextId: context.id
|
|
18095
17607
|
});
|
|
18096
17608
|
} else {
|
|
18097
17609
|
ensureDirectory(unrecognizedDir);
|
|
18098
|
-
|
|
17610
|
+
fs6.renameSync(move.sourcePath, move.targetPath);
|
|
18099
17611
|
unrecognized.push({
|
|
18100
17612
|
filename: move.filename,
|
|
18101
17613
|
targetPath: path6.join(config2.paths.unrecognized, move.filename)
|
|
@@ -18122,7 +17634,7 @@ async function classifyStatements(directory, agent, configLoader = loadImportCon
|
|
|
18122
17634
|
const importsDir = path6.join(directory, config2.paths.import);
|
|
18123
17635
|
const pendingDir = path6.join(directory, config2.paths.pending);
|
|
18124
17636
|
const unrecognizedDir = path6.join(directory, config2.paths.unrecognized);
|
|
18125
|
-
const csvFiles =
|
|
17637
|
+
const csvFiles = findCsvFiles(importsDir);
|
|
18126
17638
|
if (csvFiles.length === 0) {
|
|
18127
17639
|
return buildSuccessResult2([], [], `No CSV files found in ${config2.paths.import}`);
|
|
18128
17640
|
}
|
|
@@ -18130,11 +17642,19 @@ async function classifyStatements(directory, agent, configLoader = loadImportCon
|
|
|
18130
17642
|
if (collisions.length > 0) {
|
|
18131
17643
|
return buildCollisionError(collisions);
|
|
18132
17644
|
}
|
|
18133
|
-
const { classified, unrecognized } = executeMoves(plannedMoves, config2, unrecognizedDir);
|
|
17645
|
+
const { classified, unrecognized } = executeMoves(plannedMoves, config2, unrecognizedDir, directory);
|
|
18134
17646
|
return buildSuccessResult2(classified, unrecognized);
|
|
18135
17647
|
}
|
|
18136
17648
|
var classify_statements_default = tool({
|
|
18137
|
-
description:
|
|
17649
|
+
description: `ACCOUNTANT AGENT ONLY: Classifies bank statement CSV files from the imports directory by detecting their provider and currency, then moves them to the appropriate pending import directories.
|
|
17650
|
+
|
|
17651
|
+
For each CSV file:
|
|
17652
|
+
- Detects the provider and currency using rules from providers.yaml
|
|
17653
|
+
- Creates an import context (.memory/{uuid}.json) to track the file through the pipeline
|
|
17654
|
+
- Extracts metadata (account number, dates, balances) from CSV headers
|
|
17655
|
+
- Moves the file to import/pending/{provider}/{currency}/
|
|
17656
|
+
- Checks for file collisions before any moves (atomic: all-or-nothing)
|
|
17657
|
+
- Unrecognized files are moved to import/unrecognized/`,
|
|
18138
17658
|
args: {},
|
|
18139
17659
|
async execute(_params, context) {
|
|
18140
17660
|
const { directory, agent } = context;
|
|
@@ -18142,7 +17662,7 @@ var classify_statements_default = tool({
|
|
|
18142
17662
|
}
|
|
18143
17663
|
});
|
|
18144
17664
|
// src/tools/import-statements.ts
|
|
18145
|
-
import * as
|
|
17665
|
+
import * as fs10 from "fs";
|
|
18146
17666
|
import * as path9 from "path";
|
|
18147
17667
|
|
|
18148
17668
|
// node_modules/minimatch/dist/esm/index.js
|
|
@@ -20474,7 +19994,7 @@ class LRUCache {
|
|
|
20474
19994
|
// node_modules/path-scurry/dist/esm/index.js
|
|
20475
19995
|
import { posix, win32 } from "path";
|
|
20476
19996
|
import { fileURLToPath } from "url";
|
|
20477
|
-
import { lstatSync, readdir as readdirCB, readdirSync as
|
|
19997
|
+
import { lstatSync, readdir as readdirCB, readdirSync as readdirSync2, readlinkSync, realpathSync as rps } from "fs";
|
|
20478
19998
|
import * as actualFS from "fs";
|
|
20479
19999
|
import { lstat, readdir, readlink, realpath } from "fs/promises";
|
|
20480
20000
|
|
|
@@ -21146,7 +20666,7 @@ var realpathSync = rps.native;
|
|
|
21146
20666
|
var defaultFS = {
|
|
21147
20667
|
lstatSync,
|
|
21148
20668
|
readdir: readdirCB,
|
|
21149
|
-
readdirSync:
|
|
20669
|
+
readdirSync: readdirSync2,
|
|
21150
20670
|
readlinkSync,
|
|
21151
20671
|
realpathSync,
|
|
21152
20672
|
promises: {
|
|
@@ -21927,8 +21447,8 @@ class PathScurryBase {
|
|
|
21927
21447
|
#children;
|
|
21928
21448
|
nocase;
|
|
21929
21449
|
#fs;
|
|
21930
|
-
constructor(cwd = process.cwd(), pathImpl, sep2, { nocase, childrenCacheSize = 16 * 1024, fs:
|
|
21931
|
-
this.#fs = fsFromOption(
|
|
21450
|
+
constructor(cwd = process.cwd(), pathImpl, sep2, { nocase, childrenCacheSize = 16 * 1024, fs: fs7 = defaultFS } = {}) {
|
|
21451
|
+
this.#fs = fsFromOption(fs7);
|
|
21932
21452
|
if (cwd instanceof URL || cwd.startsWith("file://")) {
|
|
21933
21453
|
cwd = fileURLToPath(cwd);
|
|
21934
21454
|
}
|
|
@@ -22404,8 +21924,8 @@ class PathScurryWin32 extends PathScurryBase {
|
|
|
22404
21924
|
parseRootPath(dir) {
|
|
22405
21925
|
return win32.parse(dir).root.toUpperCase();
|
|
22406
21926
|
}
|
|
22407
|
-
newRoot(
|
|
22408
|
-
return new PathWin32(this.rootPath, IFDIR, undefined, this.roots, this.nocase, this.childrenCache(), { fs:
|
|
21927
|
+
newRoot(fs7) {
|
|
21928
|
+
return new PathWin32(this.rootPath, IFDIR, undefined, this.roots, this.nocase, this.childrenCache(), { fs: fs7 });
|
|
22409
21929
|
}
|
|
22410
21930
|
isAbsolute(p) {
|
|
22411
21931
|
return p.startsWith("/") || p.startsWith("\\") || /^[a-z]:(\/|\\)/i.test(p);
|
|
@@ -22422,8 +21942,8 @@ class PathScurryPosix extends PathScurryBase {
|
|
|
22422
21942
|
parseRootPath(_dir) {
|
|
22423
21943
|
return "/";
|
|
22424
21944
|
}
|
|
22425
|
-
newRoot(
|
|
22426
|
-
return new PathPosix(this.rootPath, IFDIR, undefined, this.roots, this.nocase, this.childrenCache(), { fs:
|
|
21945
|
+
newRoot(fs7) {
|
|
21946
|
+
return new PathPosix(this.rootPath, IFDIR, undefined, this.roots, this.nocase, this.childrenCache(), { fs: fs7 });
|
|
22427
21947
|
}
|
|
22428
21948
|
isAbsolute(p) {
|
|
22429
21949
|
return p.startsWith("/");
|
|
@@ -23425,7 +22945,7 @@ var glob = Object.assign(glob_, {
|
|
|
23425
22945
|
glob.glob = glob;
|
|
23426
22946
|
|
|
23427
22947
|
// src/utils/rulesMatcher.ts
|
|
23428
|
-
import * as
|
|
22948
|
+
import * as fs7 from "fs";
|
|
23429
22949
|
import * as path8 from "path";
|
|
23430
22950
|
function parseSourceDirective(content) {
|
|
23431
22951
|
const match2 = content.match(/^source\s+([^\n#]+)/m);
|
|
@@ -23443,20 +22963,25 @@ function resolveSourcePath(sourcePath, rulesFilePath) {
|
|
|
23443
22963
|
}
|
|
23444
22964
|
function loadRulesMapping(rulesDir) {
|
|
23445
22965
|
const mapping = {};
|
|
23446
|
-
if (!
|
|
22966
|
+
if (!fs7.existsSync(rulesDir)) {
|
|
23447
22967
|
return mapping;
|
|
23448
22968
|
}
|
|
23449
|
-
const files =
|
|
22969
|
+
const files = fs7.readdirSync(rulesDir);
|
|
23450
22970
|
for (const file2 of files) {
|
|
23451
22971
|
if (!file2.endsWith(".rules")) {
|
|
23452
22972
|
continue;
|
|
23453
22973
|
}
|
|
23454
22974
|
const rulesFilePath = path8.join(rulesDir, file2);
|
|
23455
|
-
const stat =
|
|
22975
|
+
const stat = fs7.statSync(rulesFilePath);
|
|
23456
22976
|
if (!stat.isFile()) {
|
|
23457
22977
|
continue;
|
|
23458
22978
|
}
|
|
23459
|
-
|
|
22979
|
+
let content;
|
|
22980
|
+
try {
|
|
22981
|
+
content = fs7.readFileSync(rulesFilePath, "utf-8");
|
|
22982
|
+
} catch {
|
|
22983
|
+
continue;
|
|
22984
|
+
}
|
|
23460
22985
|
const sourcePath = parseSourceDirective(content);
|
|
23461
22986
|
if (!sourcePath) {
|
|
23462
22987
|
continue;
|
|
@@ -23499,18 +23024,30 @@ function findRulesForCsv(csvPath, mapping) {
|
|
|
23499
23024
|
|
|
23500
23025
|
// src/utils/hledgerExecutor.ts
|
|
23501
23026
|
var {$: $2 } = globalThis.Bun;
|
|
23027
|
+
var STDERR_TRUNCATE_LENGTH = 500;
|
|
23502
23028
|
async function defaultHledgerExecutor(cmdArgs) {
|
|
23503
23029
|
try {
|
|
23504
23030
|
const result = await $2`hledger ${cmdArgs}`.quiet().nothrow();
|
|
23031
|
+
const stdout = result.stdout.toString();
|
|
23032
|
+
const stderr = result.stderr.toString();
|
|
23033
|
+
if (result.exitCode !== 0 && stderr) {
|
|
23034
|
+
process.stderr.write(`[hledger] command failed (exit ${result.exitCode}): hledger ${cmdArgs.join(" ")}
|
|
23035
|
+
`);
|
|
23036
|
+
process.stderr.write(`[hledger] stderr: ${stderr.slice(0, STDERR_TRUNCATE_LENGTH)}
|
|
23037
|
+
`);
|
|
23038
|
+
}
|
|
23505
23039
|
return {
|
|
23506
|
-
stdout
|
|
23507
|
-
stderr
|
|
23040
|
+
stdout,
|
|
23041
|
+
stderr,
|
|
23508
23042
|
exitCode: result.exitCode
|
|
23509
23043
|
};
|
|
23510
23044
|
} catch (error45) {
|
|
23045
|
+
const errorMessage = error45 instanceof Error ? error45.message : String(error45);
|
|
23046
|
+
process.stderr.write(`[hledger] exception: ${errorMessage}
|
|
23047
|
+
`);
|
|
23511
23048
|
return {
|
|
23512
23049
|
stdout: "",
|
|
23513
|
-
stderr:
|
|
23050
|
+
stderr: errorMessage,
|
|
23514
23051
|
exitCode: 1
|
|
23515
23052
|
};
|
|
23516
23053
|
}
|
|
@@ -23616,7 +23153,7 @@ async function getAccountBalance(mainJournalPath, account, asOfDate, executor =
|
|
|
23616
23153
|
}
|
|
23617
23154
|
|
|
23618
23155
|
// src/utils/rulesParser.ts
|
|
23619
|
-
import * as
|
|
23156
|
+
import * as fs8 from "fs";
|
|
23620
23157
|
function parseSkipRows(rulesContent) {
|
|
23621
23158
|
const match2 = rulesContent.match(/^skip\s+(\d+)/m);
|
|
23622
23159
|
return match2 ? parseInt(match2[1], 10) : 0;
|
|
@@ -23682,7 +23219,7 @@ function parseAccount1(rulesContent) {
|
|
|
23682
23219
|
}
|
|
23683
23220
|
function getAccountFromRulesFile(rulesFilePath) {
|
|
23684
23221
|
try {
|
|
23685
|
-
const content =
|
|
23222
|
+
const content = fs8.readFileSync(rulesFilePath, "utf-8");
|
|
23686
23223
|
return parseAccount1(content);
|
|
23687
23224
|
} catch {
|
|
23688
23225
|
return null;
|
|
@@ -23701,8 +23238,8 @@ function parseRulesFile(rulesContent) {
|
|
|
23701
23238
|
}
|
|
23702
23239
|
|
|
23703
23240
|
// src/utils/csvParser.ts
|
|
23704
|
-
var
|
|
23705
|
-
import * as
|
|
23241
|
+
var import_papaparse2 = __toESM(require_papaparse(), 1);
|
|
23242
|
+
import * as fs9 from "fs";
|
|
23706
23243
|
|
|
23707
23244
|
// src/utils/balanceUtils.ts
|
|
23708
23245
|
function parseAmountValue(amountStr) {
|
|
@@ -23750,30 +23287,35 @@ function balancesMatch(balance1, balance2) {
|
|
|
23750
23287
|
}
|
|
23751
23288
|
|
|
23752
23289
|
// src/utils/csvParser.ts
|
|
23290
|
+
var AMOUNT_MATCH_TOLERANCE = 0.001;
|
|
23753
23291
|
function parseCsvFile(csvPath, config2) {
|
|
23754
|
-
const csvContent =
|
|
23292
|
+
const csvContent = fs9.readFileSync(csvPath, "utf-8");
|
|
23755
23293
|
const lines = csvContent.split(`
|
|
23756
23294
|
`);
|
|
23757
23295
|
const headerIndex = config2.skipRows;
|
|
23758
23296
|
if (headerIndex >= lines.length) {
|
|
23759
23297
|
return [];
|
|
23760
23298
|
}
|
|
23761
|
-
const
|
|
23762
|
-
const dataLines = lines.slice(headerIndex + 1).filter((line) => line.trim() !== "");
|
|
23763
|
-
const csvWithHeader = [headerLine, ...dataLines].join(`
|
|
23299
|
+
const csvWithHeader = lines.slice(headerIndex).join(`
|
|
23764
23300
|
`);
|
|
23765
|
-
const
|
|
23766
|
-
const
|
|
23767
|
-
|
|
23768
|
-
|
|
23769
|
-
|
|
23770
|
-
|
|
23771
|
-
|
|
23772
|
-
|
|
23773
|
-
|
|
23774
|
-
|
|
23301
|
+
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
|
+
if (useFieldNames) {
|
|
23308
|
+
const rawRows = result.data;
|
|
23309
|
+
const dataRows = rawRows.slice(1);
|
|
23310
|
+
return dataRows.map((values) => {
|
|
23311
|
+
const row = {};
|
|
23312
|
+
for (let i2 = 0;i2 < config2.fieldNames.length && i2 < values.length; i2++) {
|
|
23313
|
+
row[config2.fieldNames[i2]] = values[i2];
|
|
23314
|
+
}
|
|
23315
|
+
return row;
|
|
23316
|
+
});
|
|
23775
23317
|
}
|
|
23776
|
-
return
|
|
23318
|
+
return result.data;
|
|
23777
23319
|
}
|
|
23778
23320
|
function getRowAmount(row, amountFields) {
|
|
23779
23321
|
if (amountFields.single) {
|
|
@@ -23851,7 +23393,7 @@ function findMatchingCsvRow(posting, csvRows, config2) {
|
|
|
23851
23393
|
const rowAmount = getRowAmount(row, config2.amountFields);
|
|
23852
23394
|
if (rowDate !== posting.date)
|
|
23853
23395
|
return false;
|
|
23854
|
-
if (Math.abs(rowAmount - postingAmount) >
|
|
23396
|
+
if (Math.abs(rowAmount - postingAmount) > AMOUNT_MATCH_TOLERANCE)
|
|
23855
23397
|
return false;
|
|
23856
23398
|
return true;
|
|
23857
23399
|
});
|
|
@@ -23885,31 +23427,26 @@ function findMatchingCsvRow(posting, csvRows, config2) {
|
|
|
23885
23427
|
|
|
23886
23428
|
// src/tools/import-statements.ts
|
|
23887
23429
|
function buildErrorResult3(error45, hint) {
|
|
23888
|
-
return
|
|
23889
|
-
|
|
23890
|
-
|
|
23891
|
-
|
|
23430
|
+
return buildToolErrorResult(error45, hint, {
|
|
23431
|
+
files: [],
|
|
23432
|
+
summary: {
|
|
23433
|
+
filesProcessed: 0,
|
|
23434
|
+
filesWithErrors: 0,
|
|
23435
|
+
filesWithoutRules: 0,
|
|
23436
|
+
totalTransactions: 0,
|
|
23437
|
+
matched: 0,
|
|
23438
|
+
unknown: 0
|
|
23439
|
+
}
|
|
23892
23440
|
});
|
|
23893
23441
|
}
|
|
23894
23442
|
function buildErrorResultWithDetails(error45, files, summary, hint) {
|
|
23895
|
-
return
|
|
23896
|
-
success: false,
|
|
23897
|
-
error: error45,
|
|
23898
|
-
hint,
|
|
23899
|
-
files,
|
|
23900
|
-
summary
|
|
23901
|
-
});
|
|
23443
|
+
return buildToolErrorResult(error45, hint, { files, summary });
|
|
23902
23444
|
}
|
|
23903
23445
|
function buildSuccessResult3(files, summary, message) {
|
|
23904
|
-
return
|
|
23905
|
-
success: true,
|
|
23906
|
-
files,
|
|
23907
|
-
summary,
|
|
23908
|
-
message
|
|
23909
|
-
});
|
|
23446
|
+
return buildToolSuccessResult({ files, summary, message });
|
|
23910
23447
|
}
|
|
23911
23448
|
function findCsvFromRulesFile(rulesFile) {
|
|
23912
|
-
const content =
|
|
23449
|
+
const content = fs10.readFileSync(rulesFile, "utf-8");
|
|
23913
23450
|
const match2 = content.match(/^source\s+([^\n#]+)/m);
|
|
23914
23451
|
if (!match2) {
|
|
23915
23452
|
return null;
|
|
@@ -23922,8 +23459,8 @@ function findCsvFromRulesFile(rulesFile) {
|
|
|
23922
23459
|
return null;
|
|
23923
23460
|
}
|
|
23924
23461
|
matches.sort((a, b) => {
|
|
23925
|
-
const aStat =
|
|
23926
|
-
const bStat =
|
|
23462
|
+
const aStat = fs10.statSync(a);
|
|
23463
|
+
const bStat = fs10.statSync(b);
|
|
23927
23464
|
return bStat.mtime.getTime() - aStat.mtime.getTime();
|
|
23928
23465
|
});
|
|
23929
23466
|
return matches[0];
|
|
@@ -23976,10 +23513,8 @@ async function executeImports(fileResults, directory, pendingDir, doneDir, hledg
|
|
|
23976
23513
|
const relativePath = path9.relative(pendingDir, csvFile);
|
|
23977
23514
|
const destPath = path9.join(doneDir, relativePath);
|
|
23978
23515
|
const destDir = path9.dirname(destPath);
|
|
23979
|
-
|
|
23980
|
-
|
|
23981
|
-
}
|
|
23982
|
-
fs9.renameSync(csvFile, destPath);
|
|
23516
|
+
ensureDirectory(destDir);
|
|
23517
|
+
fs10.renameSync(csvFile, destPath);
|
|
23983
23518
|
}
|
|
23984
23519
|
return {
|
|
23985
23520
|
success: true,
|
|
@@ -24027,7 +23562,7 @@ async function processCsvFile(csvFile, rulesMapping, directory, hledgerExecutor)
|
|
|
24027
23562
|
const transactionYear = years.size === 1 ? Array.from(years)[0] : undefined;
|
|
24028
23563
|
if (unknownPostings.length > 0) {
|
|
24029
23564
|
try {
|
|
24030
|
-
const rulesContent =
|
|
23565
|
+
const rulesContent = fs10.readFileSync(rulesFile, "utf-8");
|
|
24031
23566
|
const rulesConfig = parseRulesFile(rulesContent);
|
|
24032
23567
|
const csvRows = parseCsvFile(csvFile, rulesConfig);
|
|
24033
23568
|
for (const posting of unknownPostings) {
|
|
@@ -24068,17 +23603,12 @@ async function importStatements(directory, agent, options, configLoader = loadIm
|
|
|
24068
23603
|
const rulesDir = path9.join(directory, config2.paths.rules);
|
|
24069
23604
|
const doneDir = path9.join(directory, config2.paths.done);
|
|
24070
23605
|
const rulesMapping = loadRulesMapping(rulesDir);
|
|
24071
|
-
const
|
|
24072
|
-
|
|
24073
|
-
|
|
24074
|
-
|
|
24075
|
-
filesWithErrors: 0,
|
|
24076
|
-
filesWithoutRules: 0,
|
|
24077
|
-
totalTransactions: 0,
|
|
24078
|
-
matched: 0,
|
|
24079
|
-
unknown: 0
|
|
24080
|
-
}, "No CSV files found to process");
|
|
23606
|
+
const importContext = loadContext(directory, options.contextId);
|
|
23607
|
+
const csvPath = path9.join(directory, importContext.filePath);
|
|
23608
|
+
if (!fs10.existsSync(csvPath)) {
|
|
23609
|
+
return buildErrorResult3(`CSV file not found: ${importContext.filePath}`, "The file may have been moved or deleted");
|
|
24081
23610
|
}
|
|
23611
|
+
const csvFiles = [csvPath];
|
|
24082
23612
|
const fileResults = [];
|
|
24083
23613
|
let totalTransactions = 0;
|
|
24084
23614
|
let totalMatched = 0;
|
|
@@ -24110,8 +23640,8 @@ async function importStatements(directory, agent, options, configLoader = loadIm
|
|
|
24110
23640
|
}
|
|
24111
23641
|
for (const [_rulesFile, matchingCSVs] of rulesFileToCSVs.entries()) {
|
|
24112
23642
|
matchingCSVs.sort((a, b) => {
|
|
24113
|
-
const aStat =
|
|
24114
|
-
const bStat =
|
|
23643
|
+
const aStat = fs10.statSync(a);
|
|
23644
|
+
const bStat = fs10.statSync(b);
|
|
24115
23645
|
return bStat.mtime.getTime() - aStat.mtime.getTime();
|
|
24116
23646
|
});
|
|
24117
23647
|
const newestCSV = matchingCSVs[0];
|
|
@@ -24169,6 +23699,16 @@ async function importStatements(directory, agent, options, configLoader = loadIm
|
|
|
24169
23699
|
unknown: totalUnknown
|
|
24170
23700
|
}, importResult.hint);
|
|
24171
23701
|
}
|
|
23702
|
+
if (fileResults.length > 0) {
|
|
23703
|
+
const firstResult = fileResults[0];
|
|
23704
|
+
const newFilePath = path9.join(config2.paths.done, path9.relative(pendingDir, path9.join(directory, firstResult.csv)));
|
|
23705
|
+
updateContext(directory, options.contextId, {
|
|
23706
|
+
filePath: newFilePath,
|
|
23707
|
+
rulesFile: firstResult.rulesFile || undefined,
|
|
23708
|
+
yearJournal: firstResult.transactionYear ? `ledger/${firstResult.transactionYear}.journal` : undefined,
|
|
23709
|
+
transactionCount: firstResult.totalTransactions
|
|
23710
|
+
});
|
|
23711
|
+
}
|
|
24172
23712
|
return buildSuccessResult3(fileResults.map((f) => ({
|
|
24173
23713
|
...f,
|
|
24174
23714
|
imported: true
|
|
@@ -24204,26 +23744,29 @@ This tool processes CSV files in the pending import directory and uses hledger's
|
|
|
24204
23744
|
|
|
24205
23745
|
Note: This tool is typically called via import-pipeline for the full workflow.`,
|
|
24206
23746
|
args: {
|
|
24207
|
-
|
|
24208
|
-
currency: tool.schema.string().optional().describe('Filter by currency (e.g., "chf", "eur"). If omitted, process all currencies for the provider.'),
|
|
23747
|
+
contextId: tool.schema.string().describe("Context ID from classify step. Used to locate the specific CSV file to process."),
|
|
24209
23748
|
checkOnly: tool.schema.boolean().optional().describe("If true (default), only check for unknown accounts without importing. Set to false to perform actual import.")
|
|
24210
23749
|
},
|
|
24211
23750
|
async execute(params, context) {
|
|
24212
23751
|
const { directory, agent } = context;
|
|
24213
23752
|
return importStatements(directory, agent, {
|
|
24214
|
-
|
|
24215
|
-
currency: params.currency,
|
|
23753
|
+
contextId: params.contextId,
|
|
24216
23754
|
checkOnly: params.checkOnly
|
|
24217
23755
|
});
|
|
24218
23756
|
}
|
|
24219
23757
|
});
|
|
24220
23758
|
// src/tools/reconcile-statement.ts
|
|
24221
|
-
import * as
|
|
23759
|
+
import * as fs11 from "fs";
|
|
24222
23760
|
import * as path10 from "path";
|
|
24223
23761
|
function buildErrorResult4(params) {
|
|
24224
|
-
return
|
|
24225
|
-
|
|
24226
|
-
|
|
23762
|
+
return buildToolErrorResult(params.error, params.hint, {
|
|
23763
|
+
account: params.account ?? "",
|
|
23764
|
+
expectedBalance: params.expectedBalance ?? "",
|
|
23765
|
+
actualBalance: params.actualBalance ?? "",
|
|
23766
|
+
lastTransactionDate: params.lastTransactionDate ?? "",
|
|
23767
|
+
csvFile: params.csvFile ?? "",
|
|
23768
|
+
difference: params.difference,
|
|
23769
|
+
metadata: params.metadata
|
|
24227
23770
|
});
|
|
24228
23771
|
}
|
|
24229
23772
|
function loadConfiguration(directory, configLoader) {
|
|
@@ -24239,69 +23782,87 @@ function loadConfiguration(directory, configLoader) {
|
|
|
24239
23782
|
};
|
|
24240
23783
|
}
|
|
24241
23784
|
}
|
|
24242
|
-
function
|
|
24243
|
-
const
|
|
24244
|
-
if (
|
|
24245
|
-
const providerFilter = options.provider ? ` --provider=${options.provider}` : "";
|
|
24246
|
-
const currencyFilter = options.currency ? ` --currency=${options.currency}` : "";
|
|
23785
|
+
function verifyCsvExists(directory, importContext) {
|
|
23786
|
+
const csvFile = path10.join(directory, importContext.filePath);
|
|
23787
|
+
if (!fs11.existsSync(csvFile)) {
|
|
24247
23788
|
return {
|
|
24248
23789
|
error: buildErrorResult4({
|
|
24249
|
-
error: `
|
|
24250
|
-
hint: `
|
|
23790
|
+
error: `CSV file not found: ${importContext.filePath}`,
|
|
23791
|
+
hint: `The file may have been moved or deleted. Context ID: ${importContext.id}`
|
|
24251
23792
|
})
|
|
24252
23793
|
};
|
|
24253
23794
|
}
|
|
24254
|
-
|
|
24255
|
-
|
|
24256
|
-
|
|
23795
|
+
return { csvFile, relativePath: importContext.filePath };
|
|
23796
|
+
}
|
|
23797
|
+
function getBalanceFromContext(importContext) {
|
|
23798
|
+
if (!importContext.closingBalance)
|
|
23799
|
+
return;
|
|
23800
|
+
let balance = importContext.closingBalance;
|
|
23801
|
+
if (importContext.currency && !balance.includes(importContext.currency.toUpperCase())) {
|
|
23802
|
+
balance = `${importContext.currency.toUpperCase()} ${balance}`;
|
|
23803
|
+
}
|
|
23804
|
+
return balance;
|
|
23805
|
+
}
|
|
23806
|
+
function getBalanceFromCsvMetadata(metadata) {
|
|
23807
|
+
if (!metadata?.["closing-balance"])
|
|
23808
|
+
return;
|
|
23809
|
+
let balance = metadata["closing-balance"];
|
|
23810
|
+
const currency = metadata.currency;
|
|
23811
|
+
if (currency && balance && !balance.includes(currency)) {
|
|
23812
|
+
balance = `${currency} ${balance}`;
|
|
23813
|
+
}
|
|
23814
|
+
return balance;
|
|
24257
23815
|
}
|
|
24258
|
-
function
|
|
24259
|
-
|
|
23816
|
+
function getBalanceFromCsvAnalysis(csvFile, rulesDir) {
|
|
23817
|
+
const csvAnalysis = tryExtractClosingBalanceFromCSV(csvFile, rulesDir);
|
|
23818
|
+
if (csvAnalysis && csvAnalysis.confidence === "high") {
|
|
23819
|
+
return csvAnalysis.balance;
|
|
23820
|
+
}
|
|
23821
|
+
return;
|
|
23822
|
+
}
|
|
23823
|
+
function extractCsvMetadata(csvFile, config2) {
|
|
24260
23824
|
try {
|
|
24261
|
-
const content =
|
|
23825
|
+
const content = fs11.readFileSync(csvFile, "utf-8");
|
|
24262
23826
|
const filename = path10.basename(csvFile);
|
|
24263
23827
|
const detectionResult = detectProvider(filename, content, config2);
|
|
24264
|
-
|
|
24265
|
-
|
|
24266
|
-
|
|
24267
|
-
|
|
24268
|
-
|
|
24269
|
-
|
|
24270
|
-
|
|
24271
|
-
|
|
24272
|
-
|
|
24273
|
-
|
|
24274
|
-
closingBalance = `${currency} ${closingBalance}`;
|
|
24275
|
-
}
|
|
24276
|
-
}
|
|
24277
|
-
if (!closingBalance) {
|
|
24278
|
-
const csvAnalysis = tryExtractClosingBalanceFromCSV(csvFile, rulesDir);
|
|
24279
|
-
if (csvAnalysis && csvAnalysis.confidence === "high") {
|
|
24280
|
-
closingBalance = csvAnalysis.balance;
|
|
24281
|
-
return { closingBalance, metadata, fromCSVAnalysis: true };
|
|
23828
|
+
if (detectionResult?.metadata) {
|
|
23829
|
+
const m = detectionResult.metadata;
|
|
23830
|
+
return {
|
|
23831
|
+
"from-date": m["from-date"],
|
|
23832
|
+
"until-date": m["until-date"],
|
|
23833
|
+
"opening-balance": m["opening-balance"],
|
|
23834
|
+
"closing-balance": m["closing-balance"],
|
|
23835
|
+
currency: m["currency"],
|
|
23836
|
+
"account-number": m["account-number"]
|
|
23837
|
+
};
|
|
24282
23838
|
}
|
|
24283
|
-
}
|
|
24284
|
-
|
|
24285
|
-
const retryCmd = buildRetryCommand(options, "CHF 2324.79", options.account);
|
|
24286
|
-
return {
|
|
24287
|
-
error: buildErrorResult4({
|
|
24288
|
-
csvFile: relativeCsvPath,
|
|
24289
|
-
error: "No closing balance found in CSV metadata or data",
|
|
24290
|
-
hint: `Provide closingBalance parameter manually. Example retry: ${retryCmd}`,
|
|
24291
|
-
metadata
|
|
24292
|
-
})
|
|
24293
|
-
};
|
|
24294
|
-
}
|
|
24295
|
-
return { closingBalance, metadata };
|
|
23839
|
+
} catch {}
|
|
23840
|
+
return;
|
|
24296
23841
|
}
|
|
24297
|
-
function
|
|
24298
|
-
const
|
|
24299
|
-
|
|
24300
|
-
|
|
23842
|
+
function determineClosingBalance(csvFile, config2, importContext, manualClosingBalance, relativeCsvPath, rulesDir) {
|
|
23843
|
+
const metadata = extractCsvMetadata(csvFile, config2);
|
|
23844
|
+
const closingBalance = manualClosingBalance || getBalanceFromContext(importContext) || getBalanceFromCsvMetadata(metadata);
|
|
23845
|
+
if (closingBalance) {
|
|
23846
|
+
return { closingBalance, metadata };
|
|
24301
23847
|
}
|
|
24302
|
-
|
|
24303
|
-
|
|
23848
|
+
const analysisBalance = getBalanceFromCsvAnalysis(csvFile, rulesDir);
|
|
23849
|
+
if (analysisBalance) {
|
|
23850
|
+
return { closingBalance: analysisBalance, metadata, fromCSVAnalysis: true };
|
|
24304
23851
|
}
|
|
23852
|
+
const currency = importContext.currency?.toUpperCase() || "CHF";
|
|
23853
|
+
const exampleBalance = `${currency} <amount>`;
|
|
23854
|
+
const retryCmd = buildRetryCommand(importContext.id, exampleBalance);
|
|
23855
|
+
return {
|
|
23856
|
+
error: buildErrorResult4({
|
|
23857
|
+
csvFile: relativeCsvPath,
|
|
23858
|
+
error: "No closing balance found in CSV metadata or data",
|
|
23859
|
+
hint: `Provide closingBalance parameter manually. Example retry: ${retryCmd}`,
|
|
23860
|
+
metadata
|
|
23861
|
+
})
|
|
23862
|
+
};
|
|
23863
|
+
}
|
|
23864
|
+
function buildRetryCommand(contextId, closingBalance, account) {
|
|
23865
|
+
const parts = ["reconcile-statement", `--contextId ${contextId}`];
|
|
24305
23866
|
if (closingBalance) {
|
|
24306
23867
|
parts.push(`--closingBalance "${closingBalance}"`);
|
|
24307
23868
|
}
|
|
@@ -24310,19 +23871,17 @@ function buildRetryCommand(options, closingBalance, account) {
|
|
|
24310
23871
|
}
|
|
24311
23872
|
return parts.join(" ");
|
|
24312
23873
|
}
|
|
24313
|
-
function determineAccount(csvFile, rulesDir,
|
|
24314
|
-
let account =
|
|
23874
|
+
function determineAccount(csvFile, rulesDir, importContext, manualAccount, relativeCsvPath, metadata) {
|
|
23875
|
+
let account = manualAccount;
|
|
23876
|
+
const rulesMapping = loadRulesMapping(rulesDir);
|
|
23877
|
+
const rulesFile = findRulesForCsv(csvFile, rulesMapping);
|
|
24315
23878
|
if (!account) {
|
|
24316
|
-
const rulesMapping = loadRulesMapping(rulesDir);
|
|
24317
|
-
const rulesFile = findRulesForCsv(csvFile, rulesMapping);
|
|
24318
23879
|
if (rulesFile) {
|
|
24319
23880
|
account = getAccountFromRulesFile(rulesFile) ?? undefined;
|
|
24320
23881
|
}
|
|
24321
23882
|
}
|
|
24322
23883
|
if (!account) {
|
|
24323
|
-
const
|
|
24324
|
-
const rulesFile = findRulesForCsv(csvFile, rulesMapping);
|
|
24325
|
-
const rulesHint = rulesFile ? `Add 'account1 assets:bank:...' to ${rulesFile} or retry with: ${buildRetryCommand(options, undefined, "assets:bank:...")}` : `Create a rules file in ${rulesDir} with 'account1' directive or retry with: ${buildRetryCommand(options, undefined, "assets:bank:...")}`;
|
|
23884
|
+
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:...")}`;
|
|
24326
23885
|
return {
|
|
24327
23886
|
error: buildErrorResult4({
|
|
24328
23887
|
csvFile: relativeCsvPath,
|
|
@@ -24341,7 +23900,7 @@ function tryExtractClosingBalanceFromCSV(csvFile, rulesDir) {
|
|
|
24341
23900
|
if (!rulesFile) {
|
|
24342
23901
|
return null;
|
|
24343
23902
|
}
|
|
24344
|
-
const rulesContent =
|
|
23903
|
+
const rulesContent = fs11.readFileSync(rulesFile, "utf-8");
|
|
24345
23904
|
const rulesConfig = parseRulesFile(rulesContent);
|
|
24346
23905
|
const csvRows = parseCsvFile(csvFile, rulesConfig);
|
|
24347
23906
|
if (csvRows.length === 0) {
|
|
@@ -24403,25 +23962,33 @@ async function reconcileStatement(directory, agent, options, configLoader = load
|
|
|
24403
23962
|
if (restrictionError) {
|
|
24404
23963
|
return restrictionError;
|
|
24405
23964
|
}
|
|
23965
|
+
let importContext;
|
|
23966
|
+
try {
|
|
23967
|
+
importContext = loadContext(directory, options.contextId);
|
|
23968
|
+
} catch {
|
|
23969
|
+
return buildErrorResult4({
|
|
23970
|
+
error: `Failed to load import context: ${options.contextId}`,
|
|
23971
|
+
hint: "Ensure the context ID is valid and the context file exists in .memory/"
|
|
23972
|
+
});
|
|
23973
|
+
}
|
|
24406
23974
|
const configResult = loadConfiguration(directory, configLoader);
|
|
24407
23975
|
if ("error" in configResult) {
|
|
24408
23976
|
return configResult.error;
|
|
24409
23977
|
}
|
|
24410
23978
|
const { config: config2 } = configResult;
|
|
24411
|
-
const doneDir = path10.join(directory, config2.paths.done);
|
|
24412
23979
|
const rulesDir = path10.join(directory, config2.paths.rules);
|
|
24413
23980
|
const mainJournalPath = path10.join(directory, ".hledger.journal");
|
|
24414
|
-
const csvResult =
|
|
23981
|
+
const csvResult = verifyCsvExists(directory, importContext);
|
|
24415
23982
|
if ("error" in csvResult) {
|
|
24416
23983
|
return csvResult.error;
|
|
24417
23984
|
}
|
|
24418
23985
|
const { csvFile, relativePath: relativeCsvPath } = csvResult;
|
|
24419
|
-
const balanceResult = determineClosingBalance(csvFile, config2, options, relativeCsvPath, rulesDir);
|
|
23986
|
+
const balanceResult = determineClosingBalance(csvFile, config2, importContext, options.closingBalance, relativeCsvPath, rulesDir);
|
|
24420
23987
|
if ("error" in balanceResult) {
|
|
24421
23988
|
return balanceResult.error;
|
|
24422
23989
|
}
|
|
24423
23990
|
const { closingBalance, metadata, fromCSVAnalysis } = balanceResult;
|
|
24424
|
-
const accountResult = determineAccount(csvFile, rulesDir, options, relativeCsvPath, metadata);
|
|
23991
|
+
const accountResult = determineAccount(csvFile, rulesDir, importContext, options.account, relativeCsvPath, metadata);
|
|
24425
23992
|
if ("error" in accountResult) {
|
|
24426
23993
|
return accountResult.error;
|
|
24427
23994
|
}
|
|
@@ -24508,30 +24075,30 @@ var reconcile_statement_default = tool({
|
|
|
24508
24075
|
This tool validates that the imported transactions result in the correct closing balance.
|
|
24509
24076
|
|
|
24510
24077
|
**Workflow:**
|
|
24511
|
-
1.
|
|
24512
|
-
2. Extracts closing balance from CSV metadata (or uses manual override)
|
|
24078
|
+
1. Loads import context to find the CSV file
|
|
24079
|
+
2. Extracts closing balance from context/CSV metadata (or uses manual override)
|
|
24513
24080
|
3. Determines the account from the matching rules file (or uses manual override)
|
|
24514
24081
|
4. Queries hledger for the actual balance as of the last transaction date
|
|
24515
24082
|
5. Compares expected vs actual balance
|
|
24516
24083
|
|
|
24517
24084
|
**Balance Sources:**
|
|
24518
|
-
- Automatic:
|
|
24085
|
+
- Automatic: From import context or CSV header metadata (e.g., UBS files have "Closing balance:" row)
|
|
24519
24086
|
- Manual: Provided via closingBalance parameter (required for providers like Revolut)
|
|
24520
24087
|
|
|
24521
24088
|
**Account Detection:**
|
|
24522
24089
|
- Automatic: Parsed from account1 directive in matching rules file
|
|
24523
|
-
- Manual: Provided via account parameter
|
|
24090
|
+
- Manual: Provided via account parameter
|
|
24091
|
+
|
|
24092
|
+
Note: This tool requires a contextId from a prior classify/import step.`,
|
|
24524
24093
|
args: {
|
|
24525
|
-
|
|
24526
|
-
currency: tool.schema.string().optional().describe('Filter by currency (e.g., "chf", "eur")'),
|
|
24094
|
+
contextId: tool.schema.string().describe("Context ID from classify/import step (required)"),
|
|
24527
24095
|
closingBalance: tool.schema.string().optional().describe('Manual closing balance (e.g., "CHF 2324.79"). Required if not in CSV metadata.'),
|
|
24528
24096
|
account: tool.schema.string().optional().describe('Manual account (e.g., "assets:bank:ubs:checking"). Auto-detected from rules file if not provided.')
|
|
24529
24097
|
},
|
|
24530
24098
|
async execute(params, context) {
|
|
24531
24099
|
const { directory, agent } = context;
|
|
24532
24100
|
return reconcileStatement(directory, agent, {
|
|
24533
|
-
|
|
24534
|
-
currency: params.currency,
|
|
24101
|
+
contextId: params.contextId,
|
|
24535
24102
|
closingBalance: params.closingBalance,
|
|
24536
24103
|
account: params.account
|
|
24537
24104
|
});
|
|
@@ -24541,13 +24108,13 @@ This tool validates that the imported transactions result in the correct closing
|
|
|
24541
24108
|
import * as path12 from "path";
|
|
24542
24109
|
|
|
24543
24110
|
// src/utils/accountDeclarations.ts
|
|
24544
|
-
import * as
|
|
24111
|
+
import * as fs12 from "fs";
|
|
24545
24112
|
function extractAccountsFromRulesFile(rulesPath) {
|
|
24546
24113
|
const accounts = new Set;
|
|
24547
|
-
if (!
|
|
24114
|
+
if (!fs12.existsSync(rulesPath)) {
|
|
24548
24115
|
return accounts;
|
|
24549
24116
|
}
|
|
24550
|
-
const content =
|
|
24117
|
+
const content = fs12.readFileSync(rulesPath, "utf-8");
|
|
24551
24118
|
const lines = content.split(`
|
|
24552
24119
|
`);
|
|
24553
24120
|
for (const line of lines) {
|
|
@@ -24582,10 +24149,10 @@ function sortAccountDeclarations(accounts) {
|
|
|
24582
24149
|
return Array.from(accounts).sort((a, b) => a.localeCompare(b));
|
|
24583
24150
|
}
|
|
24584
24151
|
function ensureAccountDeclarations(yearJournalPath, accounts) {
|
|
24585
|
-
if (!
|
|
24152
|
+
if (!fs12.existsSync(yearJournalPath)) {
|
|
24586
24153
|
throw new Error(`Year journal not found: ${yearJournalPath}`);
|
|
24587
24154
|
}
|
|
24588
|
-
const content =
|
|
24155
|
+
const content = fs12.readFileSync(yearJournalPath, "utf-8");
|
|
24589
24156
|
const lines = content.split(`
|
|
24590
24157
|
`);
|
|
24591
24158
|
const existingAccounts = new Set;
|
|
@@ -24647,7 +24214,7 @@ function ensureAccountDeclarations(yearJournalPath, accounts) {
|
|
|
24647
24214
|
newContent.push("");
|
|
24648
24215
|
}
|
|
24649
24216
|
newContent.push(...otherLines);
|
|
24650
|
-
|
|
24217
|
+
fs12.writeFileSync(yearJournalPath, newContent.join(`
|
|
24651
24218
|
`));
|
|
24652
24219
|
return {
|
|
24653
24220
|
added: Array.from(missingAccounts).sort(),
|
|
@@ -24656,8 +24223,9 @@ function ensureAccountDeclarations(yearJournalPath, accounts) {
|
|
|
24656
24223
|
}
|
|
24657
24224
|
|
|
24658
24225
|
// src/utils/logger.ts
|
|
24659
|
-
import
|
|
24226
|
+
import fs13 from "fs/promises";
|
|
24660
24227
|
import path11 from "path";
|
|
24228
|
+
var LOG_LINE_LIMIT = 50;
|
|
24661
24229
|
|
|
24662
24230
|
class MarkdownLogger {
|
|
24663
24231
|
buffer = [];
|
|
@@ -24665,6 +24233,7 @@ class MarkdownLogger {
|
|
|
24665
24233
|
context = {};
|
|
24666
24234
|
autoFlush;
|
|
24667
24235
|
sectionDepth = 0;
|
|
24236
|
+
pendingFlush = null;
|
|
24668
24237
|
constructor(config2) {
|
|
24669
24238
|
this.autoFlush = config2.autoFlush ?? true;
|
|
24670
24239
|
this.context = config2.context || {};
|
|
@@ -24739,9 +24308,9 @@ class MarkdownLogger {
|
|
|
24739
24308
|
this.buffer.push("");
|
|
24740
24309
|
const lines = output.trim().split(`
|
|
24741
24310
|
`);
|
|
24742
|
-
if (lines.length >
|
|
24743
|
-
this.buffer.push(...lines.slice(0,
|
|
24744
|
-
this.buffer.push(`... (${lines.length -
|
|
24311
|
+
if (lines.length > LOG_LINE_LIMIT) {
|
|
24312
|
+
this.buffer.push(...lines.slice(0, LOG_LINE_LIMIT));
|
|
24313
|
+
this.buffer.push(`... (${lines.length - LOG_LINE_LIMIT} more lines omitted)`);
|
|
24745
24314
|
} else {
|
|
24746
24315
|
this.buffer.push(output.trim());
|
|
24747
24316
|
}
|
|
@@ -24763,11 +24332,14 @@ class MarkdownLogger {
|
|
|
24763
24332
|
this.context[key] = value;
|
|
24764
24333
|
}
|
|
24765
24334
|
async flush() {
|
|
24335
|
+
if (this.pendingFlush) {
|
|
24336
|
+
await this.pendingFlush;
|
|
24337
|
+
}
|
|
24766
24338
|
if (this.buffer.length === 0)
|
|
24767
24339
|
return;
|
|
24768
24340
|
try {
|
|
24769
|
-
await
|
|
24770
|
-
await
|
|
24341
|
+
await fs13.mkdir(path11.dirname(this.logPath), { recursive: true });
|
|
24342
|
+
await fs13.writeFile(this.logPath, this.buffer.join(`
|
|
24771
24343
|
`), "utf-8");
|
|
24772
24344
|
} catch {}
|
|
24773
24345
|
}
|
|
@@ -24775,7 +24347,7 @@ class MarkdownLogger {
|
|
|
24775
24347
|
return this.logPath;
|
|
24776
24348
|
}
|
|
24777
24349
|
flushAsync() {
|
|
24778
|
-
this.flush().catch(() => {});
|
|
24350
|
+
this.pendingFlush = this.flush().catch(() => {});
|
|
24779
24351
|
}
|
|
24780
24352
|
getTimestamp() {
|
|
24781
24353
|
return new Date().toISOString().replace(/:/g, "-").split(".")[0];
|
|
@@ -24821,12 +24393,12 @@ function buildStepResult(success2, message, details) {
|
|
|
24821
24393
|
}
|
|
24822
24394
|
return result;
|
|
24823
24395
|
}
|
|
24824
|
-
function
|
|
24396
|
+
function buildPipelineSuccessResult(result, summary) {
|
|
24825
24397
|
result.success = true;
|
|
24826
24398
|
result.summary = summary;
|
|
24827
24399
|
return JSON.stringify(result);
|
|
24828
24400
|
}
|
|
24829
|
-
function
|
|
24401
|
+
function buildPipelineErrorResult(result, error45, hint) {
|
|
24830
24402
|
result.success = false;
|
|
24831
24403
|
result.error = error45;
|
|
24832
24404
|
if (hint) {
|
|
@@ -24841,16 +24413,28 @@ async function executeClassifyStep(context, logger) {
|
|
|
24841
24413
|
logger?.info("Classification skipped (skipClassify: true)");
|
|
24842
24414
|
context.result.steps.classify = buildStepResult(true, "Classification skipped (skipClassify: true)");
|
|
24843
24415
|
logger?.endSection();
|
|
24844
|
-
return;
|
|
24416
|
+
return [];
|
|
24845
24417
|
}
|
|
24846
24418
|
const classifyResult = await classifyStatements(context.directory, context.agent, context.configLoader);
|
|
24847
24419
|
const classifyParsed = JSON.parse(classifyResult);
|
|
24848
24420
|
const success2 = classifyParsed.success !== false;
|
|
24421
|
+
const contextIds = [];
|
|
24422
|
+
if (classifyParsed.classified?.length > 0) {
|
|
24423
|
+
for (const file2 of classifyParsed.classified) {
|
|
24424
|
+
if (file2.contextId) {
|
|
24425
|
+
contextIds.push(file2.contextId);
|
|
24426
|
+
}
|
|
24427
|
+
}
|
|
24428
|
+
}
|
|
24849
24429
|
let message = success2 ? "Classification complete" : "Classification had issues";
|
|
24850
24430
|
if (classifyParsed.unrecognized?.length > 0) {
|
|
24851
24431
|
message = `Classification complete with ${classifyParsed.unrecognized.length} unrecognized file(s)`;
|
|
24852
24432
|
logger?.warn(`${classifyParsed.unrecognized.length} unrecognized file(s)`);
|
|
24853
24433
|
}
|
|
24434
|
+
if (contextIds.length > 0) {
|
|
24435
|
+
message += ` (${contextIds.length} context(s) created)`;
|
|
24436
|
+
logger?.info(`Created ${contextIds.length} import context(s)`);
|
|
24437
|
+
}
|
|
24854
24438
|
logger?.logStep("Classify", success2 ? "success" : "error", message);
|
|
24855
24439
|
const details = {
|
|
24856
24440
|
success: success2,
|
|
@@ -24858,23 +24442,18 @@ async function executeClassifyStep(context, logger) {
|
|
|
24858
24442
|
classified: classifyParsed
|
|
24859
24443
|
};
|
|
24860
24444
|
context.result.steps.classify = buildStepResult(success2, message, details);
|
|
24445
|
+
context.result.contexts = contextIds;
|
|
24861
24446
|
logger?.endSection();
|
|
24447
|
+
return contextIds;
|
|
24862
24448
|
}
|
|
24863
|
-
async function executeAccountDeclarationsStep(context, logger) {
|
|
24449
|
+
async function executeAccountDeclarationsStep(context, contextId, logger) {
|
|
24864
24450
|
logger?.startSection("Step 2: Check Account Declarations");
|
|
24865
24451
|
logger?.logStep("Check Accounts", "start");
|
|
24866
24452
|
const config2 = context.configLoader(context.directory);
|
|
24867
|
-
const pendingDir = path12.join(context.directory, config2.paths.pending);
|
|
24868
24453
|
const rulesDir = path12.join(context.directory, config2.paths.rules);
|
|
24869
|
-
const
|
|
24870
|
-
|
|
24871
|
-
|
|
24872
|
-
accountsAdded: [],
|
|
24873
|
-
journalUpdated: "",
|
|
24874
|
-
rulesScanned: []
|
|
24875
|
-
});
|
|
24876
|
-
return;
|
|
24877
|
-
}
|
|
24454
|
+
const importCtx = loadContext(context.directory, contextId);
|
|
24455
|
+
const csvPath = path12.join(context.directory, importCtx.filePath);
|
|
24456
|
+
const csvFiles = [csvPath];
|
|
24878
24457
|
const rulesMapping = loadRulesMapping(rulesDir);
|
|
24879
24458
|
const matchedRulesFiles = new Set;
|
|
24880
24459
|
for (const csvFile of csvFiles) {
|
|
@@ -24949,12 +24528,11 @@ async function executeAccountDeclarationsStep(context, logger) {
|
|
|
24949
24528
|
});
|
|
24950
24529
|
logger?.endSection();
|
|
24951
24530
|
}
|
|
24952
|
-
async function executeDryRunStep(context, logger) {
|
|
24531
|
+
async function executeDryRunStep(context, contextId, logger) {
|
|
24953
24532
|
logger?.startSection("Step 3: Dry Run Import");
|
|
24954
24533
|
logger?.logStep("Dry Run", "start");
|
|
24955
24534
|
const dryRunResult = await importStatements(context.directory, context.agent, {
|
|
24956
|
-
|
|
24957
|
-
currency: context.options.currency,
|
|
24535
|
+
contextId,
|
|
24958
24536
|
checkOnly: true
|
|
24959
24537
|
}, context.configLoader, context.hledgerExecutor);
|
|
24960
24538
|
const dryRunParsed = JSON.parse(dryRunResult);
|
|
@@ -24979,35 +24557,30 @@ async function executeDryRunStep(context, logger) {
|
|
|
24979
24557
|
extractRulePatternsFromFile: extractRulePatternsFromFile2
|
|
24980
24558
|
} = await Promise.resolve().then(() => (init_accountSuggester(), exports_accountSuggester));
|
|
24981
24559
|
const config2 = context.configLoader(context.directory);
|
|
24982
|
-
const pendingDir = path12.join(context.directory, config2.paths.pending);
|
|
24983
24560
|
const rulesDir = path12.join(context.directory, config2.paths.rules);
|
|
24984
|
-
const
|
|
24561
|
+
const importCtx = loadContext(context.directory, contextId);
|
|
24562
|
+
const csvPath = path12.join(context.directory, importCtx.filePath);
|
|
24985
24563
|
const rulesMapping = loadRulesMapping(rulesDir);
|
|
24986
24564
|
let yearJournalPath;
|
|
24987
24565
|
let firstRulesFile;
|
|
24988
|
-
|
|
24989
|
-
|
|
24990
|
-
|
|
24991
|
-
|
|
24992
|
-
|
|
24993
|
-
|
|
24994
|
-
|
|
24995
|
-
|
|
24996
|
-
|
|
24997
|
-
|
|
24998
|
-
yearJournalPath = ensureYearJournalExists(context.directory, transactionYear);
|
|
24999
|
-
break;
|
|
25000
|
-
}
|
|
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);
|
|
25001
24576
|
}
|
|
25002
|
-
} catch {
|
|
25003
|
-
continue;
|
|
25004
24577
|
}
|
|
25005
|
-
}
|
|
24578
|
+
} catch {}
|
|
25006
24579
|
}
|
|
25007
24580
|
const suggestionContext = {
|
|
25008
|
-
existingAccounts: yearJournalPath ?
|
|
24581
|
+
existingAccounts: yearJournalPath ? loadExistingAccounts2(yearJournalPath) : [],
|
|
25009
24582
|
rulesFilePath: firstRulesFile,
|
|
25010
|
-
existingRules: firstRulesFile ?
|
|
24583
|
+
existingRules: firstRulesFile ? extractRulePatternsFromFile2(firstRulesFile) : undefined,
|
|
25011
24584
|
yearJournalPath,
|
|
25012
24585
|
logger
|
|
25013
24586
|
};
|
|
@@ -25075,12 +24648,12 @@ function formatUnknownPostingsLog(postings) {
|
|
|
25075
24648
|
`;
|
|
25076
24649
|
return log;
|
|
25077
24650
|
}
|
|
25078
|
-
async function executeImportStep(context, logger) {
|
|
25079
|
-
|
|
24651
|
+
async function executeImportStep(context, contextId, logger) {
|
|
24652
|
+
const importContext = loadContext(context.directory, contextId);
|
|
24653
|
+
logger?.startSection(`Step 4: Import Transactions (${importContext.accountNumber || contextId})`);
|
|
25080
24654
|
logger?.logStep("Import", "start");
|
|
25081
24655
|
const importResult = await importStatements(context.directory, context.agent, {
|
|
25082
|
-
|
|
25083
|
-
currency: context.options.currency,
|
|
24656
|
+
contextId,
|
|
25084
24657
|
checkOnly: false
|
|
25085
24658
|
}, context.configLoader, context.hledgerExecutor);
|
|
25086
24659
|
const importParsed = JSON.parse(importResult);
|
|
@@ -25091,6 +24664,13 @@ async function executeImportStep(context, logger) {
|
|
|
25091
24664
|
summary: importParsed.summary,
|
|
25092
24665
|
error: importParsed.error
|
|
25093
24666
|
});
|
|
24667
|
+
if (importParsed.success) {
|
|
24668
|
+
updateContext(context.directory, contextId, {
|
|
24669
|
+
rulesFile: importParsed.files?.[0]?.rulesFile,
|
|
24670
|
+
yearJournal: importParsed.files?.[0]?.yearJournal,
|
|
24671
|
+
transactionCount: importParsed.summary?.totalTransactions
|
|
24672
|
+
});
|
|
24673
|
+
}
|
|
25094
24674
|
if (!importParsed.success) {
|
|
25095
24675
|
logger?.error("Import failed", new Error(importParsed.error || "Unknown error"));
|
|
25096
24676
|
logger?.endSection();
|
|
@@ -25099,12 +24679,12 @@ async function executeImportStep(context, logger) {
|
|
|
25099
24679
|
}
|
|
25100
24680
|
logger?.endSection();
|
|
25101
24681
|
}
|
|
25102
|
-
async function executeReconcileStep(context, logger) {
|
|
25103
|
-
|
|
24682
|
+
async function executeReconcileStep(context, contextId, logger) {
|
|
24683
|
+
const importContext = loadContext(context.directory, contextId);
|
|
24684
|
+
logger?.startSection(`Step 5: Reconcile Balance (${importContext.accountNumber || contextId})`);
|
|
25104
24685
|
logger?.logStep("Reconcile", "start");
|
|
25105
24686
|
const reconcileResult = await reconcileStatement(context.directory, context.agent, {
|
|
25106
|
-
|
|
25107
|
-
currency: context.options.currency,
|
|
24687
|
+
contextId,
|
|
25108
24688
|
closingBalance: context.options.closingBalance,
|
|
25109
24689
|
account: context.options.account
|
|
25110
24690
|
}, context.configLoader, context.hledgerExecutor);
|
|
@@ -25122,6 +24702,14 @@ async function executeReconcileStep(context, logger) {
|
|
|
25122
24702
|
metadata: reconcileParsed.metadata,
|
|
25123
24703
|
error: reconcileParsed.error
|
|
25124
24704
|
});
|
|
24705
|
+
if (reconcileParsed.success) {
|
|
24706
|
+
updateContext(context.directory, contextId, {
|
|
24707
|
+
reconciledAccount: reconcileParsed.account,
|
|
24708
|
+
actualBalance: reconcileParsed.actualBalance,
|
|
24709
|
+
lastTransactionDate: reconcileParsed.lastTransactionDate,
|
|
24710
|
+
reconciled: true
|
|
24711
|
+
});
|
|
24712
|
+
}
|
|
25125
24713
|
if (!reconcileParsed.success) {
|
|
25126
24714
|
logger?.error("Reconciliation failed", new Error(reconcileParsed.error || "Balance mismatch"));
|
|
25127
24715
|
logger?.endSection();
|
|
@@ -25134,7 +24722,7 @@ async function executeReconcileStep(context, logger) {
|
|
|
25134
24722
|
function handleNoTransactions(result) {
|
|
25135
24723
|
result.steps.import = buildStepResult(true, "No transactions to import");
|
|
25136
24724
|
result.steps.reconcile = buildStepResult(true, "Reconciliation skipped (no transactions)");
|
|
25137
|
-
return
|
|
24725
|
+
return buildPipelineSuccessResult(result, "No transactions found to import");
|
|
25138
24726
|
}
|
|
25139
24727
|
async function importPipeline(directory, agent, options, configLoader = loadImportConfig, hledgerExecutor = defaultHledgerExecutor) {
|
|
25140
24728
|
const restrictionError = checkAccountantAgent(agent, "import pipeline");
|
|
@@ -25160,21 +24748,31 @@ async function importPipeline(directory, agent, options, configLoader = loadImpo
|
|
|
25160
24748
|
result
|
|
25161
24749
|
};
|
|
25162
24750
|
try {
|
|
25163
|
-
await executeClassifyStep(context, logger);
|
|
25164
|
-
|
|
25165
|
-
|
|
25166
|
-
|
|
25167
|
-
|
|
25168
|
-
|
|
24751
|
+
const contextIds = await executeClassifyStep(context, logger);
|
|
24752
|
+
if (contextIds.length === 0) {
|
|
24753
|
+
logger.info("No files classified, nothing to import");
|
|
24754
|
+
return buildPipelineSuccessResult(result, "No files to import");
|
|
24755
|
+
}
|
|
24756
|
+
let totalTransactions = 0;
|
|
24757
|
+
for (const contextId of contextIds) {
|
|
24758
|
+
const importContext = loadContext(context.directory, contextId);
|
|
24759
|
+
logger.info(`Processing: ${importContext.filename} (${importContext.accountNumber || "unknown account"})`);
|
|
24760
|
+
await executeAccountDeclarationsStep(context, contextId, logger);
|
|
24761
|
+
await executeDryRunStep(context, contextId, logger);
|
|
24762
|
+
await executeImportStep(context, contextId, logger);
|
|
24763
|
+
await executeReconcileStep(context, contextId, logger);
|
|
24764
|
+
totalTransactions += context.result.steps.import?.details?.summary?.totalTransactions || 0;
|
|
24765
|
+
}
|
|
25169
24766
|
logger.startSection("Summary");
|
|
25170
24767
|
logger.info(`Import completed successfully`);
|
|
25171
|
-
logger.info(`
|
|
24768
|
+
logger.info(`Contexts processed: ${contextIds.length}`);
|
|
24769
|
+
logger.info(`Total transactions imported: ${totalTransactions}`);
|
|
25172
24770
|
if (context.result.steps.reconcile?.details?.actualBalance) {
|
|
25173
24771
|
logger.info(`Balance: ${context.result.steps.reconcile.details.actualBalance}`);
|
|
25174
24772
|
}
|
|
25175
24773
|
logger.info(`Log file: ${logger.getLogPath()}`);
|
|
25176
24774
|
logger.endSection();
|
|
25177
|
-
return
|
|
24775
|
+
return buildPipelineSuccessResult(result, `Successfully imported ${totalTransactions} transaction(s) from ${contextIds.length} file(s)`);
|
|
25178
24776
|
} catch (error45) {
|
|
25179
24777
|
logger.error("Pipeline step failed", error45);
|
|
25180
24778
|
logger.info(`Log file: ${logger.getLogPath()}`);
|
|
@@ -25184,7 +24782,7 @@ async function importPipeline(directory, agent, options, configLoader = loadImpo
|
|
|
25184
24782
|
if (!result.error) {
|
|
25185
24783
|
result.error = error45 instanceof Error ? error45.message : String(error45);
|
|
25186
24784
|
}
|
|
25187
|
-
return
|
|
24785
|
+
return buildPipelineErrorResult(result, result.error, result.hint);
|
|
25188
24786
|
} finally {
|
|
25189
24787
|
logger.endSection();
|
|
25190
24788
|
await logger.flush();
|
|
@@ -25235,7 +24833,7 @@ This tool orchestrates the full import workflow:
|
|
|
25235
24833
|
}
|
|
25236
24834
|
});
|
|
25237
24835
|
// src/tools/init-directories.ts
|
|
25238
|
-
import * as
|
|
24836
|
+
import * as fs15 from "fs";
|
|
25239
24837
|
import * as path13 from "path";
|
|
25240
24838
|
async function initDirectories(directory) {
|
|
25241
24839
|
try {
|
|
@@ -25243,8 +24841,8 @@ async function initDirectories(directory) {
|
|
|
25243
24841
|
const directoriesCreated = [];
|
|
25244
24842
|
const gitkeepFiles = [];
|
|
25245
24843
|
const importBase = path13.join(directory, "import");
|
|
25246
|
-
if (!
|
|
25247
|
-
|
|
24844
|
+
if (!fs15.existsSync(importBase)) {
|
|
24845
|
+
fs15.mkdirSync(importBase, { recursive: true });
|
|
25248
24846
|
directoriesCreated.push("import");
|
|
25249
24847
|
}
|
|
25250
24848
|
const pathsToCreate = [
|
|
@@ -25255,19 +24853,19 @@ async function initDirectories(directory) {
|
|
|
25255
24853
|
];
|
|
25256
24854
|
for (const { path: dirPath } of pathsToCreate) {
|
|
25257
24855
|
const fullPath = path13.join(directory, dirPath);
|
|
25258
|
-
if (!
|
|
25259
|
-
|
|
24856
|
+
if (!fs15.existsSync(fullPath)) {
|
|
24857
|
+
fs15.mkdirSync(fullPath, { recursive: true });
|
|
25260
24858
|
directoriesCreated.push(dirPath);
|
|
25261
24859
|
}
|
|
25262
24860
|
const gitkeepPath = path13.join(fullPath, ".gitkeep");
|
|
25263
|
-
if (!
|
|
25264
|
-
|
|
24861
|
+
if (!fs15.existsSync(gitkeepPath)) {
|
|
24862
|
+
fs15.writeFileSync(gitkeepPath, "");
|
|
25265
24863
|
gitkeepFiles.push(path13.join(dirPath, ".gitkeep"));
|
|
25266
24864
|
}
|
|
25267
24865
|
}
|
|
25268
24866
|
const gitignorePath = path13.join(importBase, ".gitignore");
|
|
25269
24867
|
let gitignoreCreated = false;
|
|
25270
|
-
if (!
|
|
24868
|
+
if (!fs15.existsSync(gitignorePath)) {
|
|
25271
24869
|
const gitignoreContent = `# Ignore CSV/PDF files in temporary directories
|
|
25272
24870
|
/incoming/*.csv
|
|
25273
24871
|
/incoming/*.pdf
|
|
@@ -25285,7 +24883,7 @@ async function initDirectories(directory) {
|
|
|
25285
24883
|
.DS_Store
|
|
25286
24884
|
Thumbs.db
|
|
25287
24885
|
`;
|
|
25288
|
-
|
|
24886
|
+
fs15.writeFileSync(gitignorePath, gitignoreContent);
|
|
25289
24887
|
gitignoreCreated = true;
|
|
25290
24888
|
}
|
|
25291
24889
|
const parts = [];
|
|
@@ -25361,7 +24959,7 @@ You can now drop CSV files into import/incoming/ and run import-pipeline.`);
|
|
|
25361
24959
|
}
|
|
25362
24960
|
});
|
|
25363
24961
|
// src/index.ts
|
|
25364
|
-
var __dirname2 =
|
|
24962
|
+
var __dirname2 = dirname4(fileURLToPath3(import.meta.url));
|
|
25365
24963
|
var AGENT_FILE = join12(__dirname2, "..", "agent", "accountant.md");
|
|
25366
24964
|
var AccountantPlugin = async () => {
|
|
25367
24965
|
const agent = loadAgent(AGENT_FILE);
|