@fuzzle/opencode-accountant 0.4.6 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -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, {
@@ -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(10, context.existingRules.length);
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 dirname5, join as join12 } from "path";
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 path3 from "path";
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/pricesConfig.ts
16845
+ // src/utils/yamlLoader.ts
17446
16846
  init_js_yaml();
17447
16847
  import * as fs 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 (!fs.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 = fs.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
- const configPath = path.join(directory, CONFIG_FILE);
17475
- if (!fs.existsSync(configPath)) {
17476
- throw new Error(`Configuration file not found: ${CONFIG_FILE}. Please refer to the plugin's GitHub repository for setup instructions.`);
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
- throw err;
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 fs3 from "fs";
16914
+ import * as path3 from "path";
16915
+
16916
+ // src/utils/fileUtils.ts
16917
+ import * as fs2 from "fs";
16918
+ import * as path2 from "path";
16919
+ function findCsvFiles(baseDir, options = {}) {
16920
+ if (!fs2.existsSync(baseDir)) {
16921
+ return [];
17487
16922
  }
17488
- if (typeof parsed !== "object" || parsed === null) {
17489
- throw new Error(`Invalid config: ${CONFIG_FILE} must contain a YAML object`);
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
- const parsedObj = parsed;
17492
- if (!parsedObj.currencies || typeof parsedObj.currencies !== "object") {
17493
- throw new Error(`Invalid config: 'currencies' section is required`);
16930
+ if (!fs2.existsSync(searchDir)) {
16931
+ return [];
17494
16932
  }
17495
- const currenciesObj = parsedObj.currencies;
17496
- if (Object.keys(currenciesObj).length === 0) {
17497
- throw new Error(`Invalid config: 'currencies' section must contain at least one currency`);
16933
+ const csvFiles = [];
16934
+ if (options.recursive) {
16935
+ let scanDirectory = function(dir) {
16936
+ const entries = fs2.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 = fs2.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 (fs2.statSync(fullPath).isFile()) {
16954
+ csvFiles.push(options.fullPaths ? fullPath : name);
16955
+ }
16956
+ }
17498
16957
  }
17499
- const currencies = {};
17500
- for (const [name, config2] of Object.entries(currenciesObj)) {
17501
- currencies[name] = validateCurrencyConfig(name, config2);
16958
+ return csvFiles.sort();
16959
+ }
16960
+ function ensureDirectory(dirPath) {
16961
+ if (!fs2.existsSync(dirPath)) {
16962
+ fs2.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 (fs2.existsSync(journalPath)) {
17515
- existingLines = fs2.readFileSync(journalPath, "utf-8").split(`
16972
+ if (fs3.existsSync(journalPath)) {
16973
+ existingLines = fs3.readFileSync(journalPath, "utf-8").split(`
17516
16974
  `).filter((line) => line.trim() !== "");
17517
16975
  }
17518
16976
  const priceMap = new Map;
@@ -17527,54 +16985,25 @@ 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
- fs2.writeFileSync(journalPath, sortedLines.join(`
16988
+ fs3.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 = path2.join(directory, "ledger");
17565
- const yearJournalPath = path2.join(ledgerDir, `${year}.journal`);
17566
- const mainJournalPath = path2.join(directory, ".hledger.journal");
17567
- if (!fs2.existsSync(ledgerDir)) {
17568
- fs2.mkdirSync(ledgerDir, { recursive: true });
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
+ if (!fs3.existsSync(ledgerDir)) {
16997
+ fs3.mkdirSync(ledgerDir, { recursive: true });
16998
+ }
16999
+ if (!fs3.existsSync(yearJournalPath)) {
17000
+ fs3.writeFileSync(yearJournalPath, `; ${year} transactions
17572
17001
  `);
17573
17002
  }
17574
- if (!fs2.existsSync(mainJournalPath)) {
17003
+ if (!fs3.existsSync(mainJournalPath)) {
17575
17004
  throw new Error(`.hledger.journal not found at ${mainJournalPath}. Create it first with appropriate includes.`);
17576
17005
  }
17577
- const mainJournalContent = fs2.readFileSync(mainJournalPath, "utf-8");
17006
+ const mainJournalContent = fs3.readFileSync(mainJournalPath, "utf-8");
17578
17007
  const includeDirective = `include ledger/${year}.journal`;
17579
17008
  const lines = mainJournalContent.split(`
17580
17009
  `);
@@ -17586,7 +17015,7 @@ function ensureYearJournalExists(directory, year) {
17586
17015
  const newContent = mainJournalContent.trimEnd() + `
17587
17016
  ` + includeDirective + `
17588
17017
  `;
17589
- fs2.writeFileSync(mainJournalPath, newContent);
17018
+ fs3.writeFileSync(mainJournalPath, newContent);
17590
17019
  }
17591
17020
  return yearJournalPath;
17592
17021
  }
@@ -17648,7 +17077,7 @@ function parsePriceLine(line) {
17648
17077
  formattedLine: line
17649
17078
  };
17650
17079
  }
17651
- function filterPriceLinesByDateRange(priceLines, startDate, endDate) {
17080
+ function filterAndSortPriceLinesByDateRange(priceLines, startDate, endDate) {
17652
17081
  return priceLines.map(parsePriceLine).filter((parsed) => {
17653
17082
  if (!parsed)
17654
17083
  return false;
@@ -17684,7 +17113,7 @@ async function fetchCurrencyPrices(directory, agent, backfill, priceFetcher = de
17684
17113
  });
17685
17114
  continue;
17686
17115
  }
17687
- const priceLines = filterPriceLinesByDateRange(rawPriceLines, startDate, endDate);
17116
+ const priceLines = filterAndSortPriceLinesByDateRange(rawPriceLines, startDate, endDate);
17688
17117
  if (priceLines.length === 0) {
17689
17118
  results.push({
17690
17119
  ticker,
@@ -17692,7 +17121,7 @@ async function fetchCurrencyPrices(directory, agent, backfill, priceFetcher = de
17692
17121
  });
17693
17122
  continue;
17694
17123
  }
17695
- const journalPath = path3.join(directory, "ledger", "currencies", currencyConfig.file);
17124
+ const journalPath = path4.join(directory, "ledger", "currencies", currencyConfig.file);
17696
17125
  updatePriceJournal(journalPath, priceLines);
17697
17126
  const latestPriceLine = priceLines[priceLines.length - 1];
17698
17127
  results.push({
@@ -17725,9 +17154,6 @@ import * as fs5 from "fs";
17725
17154
  import * as path6 from "path";
17726
17155
 
17727
17156
  // src/utils/importConfig.ts
17728
- init_js_yaml();
17729
- import * as fs3 from "fs";
17730
- import * as path4 from "path";
17731
17157
  var CONFIG_FILE2 = "config/import/providers.yaml";
17732
17158
  var REQUIRED_PATH_FIELDS = [
17733
17159
  "import",
@@ -17850,43 +17276,27 @@ function validateProviderConfig(name, config2) {
17850
17276
  return { detect, currencies };
17851
17277
  }
17852
17278
  function loadImportConfig(directory) {
17853
- const configPath = path4.join(directory, CONFIG_FILE2);
17854
- if (!fs3.existsSync(configPath)) {
17855
- throw new Error(`Configuration file not found: ${CONFIG_FILE2}. Please create this file to configure statement imports.`);
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}`);
17279
+ return loadYamlConfig(directory, CONFIG_FILE2, (parsedObj) => {
17280
+ if (!parsedObj.paths) {
17281
+ throw new Error("Invalid config: 'paths' section is required");
17864
17282
  }
17865
- throw err;
17866
- }
17867
- if (typeof parsed !== "object" || parsed === null) {
17868
- throw new Error(`Invalid config: ${CONFIG_FILE2} must contain a YAML object`);
17869
- }
17870
- const parsedObj = parsed;
17871
- if (!parsedObj.paths) {
17872
- throw new Error("Invalid config: 'paths' section is required");
17873
- }
17874
- const paths = validatePaths(parsedObj.paths);
17875
- if (!parsedObj.providers || typeof parsedObj.providers !== "object") {
17876
- throw new Error("Invalid config: 'providers' section is required");
17877
- }
17878
- const providersObj = parsedObj.providers;
17879
- if (Object.keys(providersObj).length === 0) {
17880
- throw new Error("Invalid config: 'providers' section must contain at least one provider");
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 };
17283
+ const paths = validatePaths(parsedObj.paths);
17284
+ if (!parsedObj.providers || typeof parsedObj.providers !== "object") {
17285
+ throw new Error("Invalid config: 'providers' section is required");
17286
+ }
17287
+ const providersObj = parsedObj.providers;
17288
+ if (Object.keys(providersObj).length === 0) {
17289
+ throw new Error("Invalid config: 'providers' section must contain at least one provider");
17290
+ }
17291
+ const providers = {};
17292
+ for (const [name, config2] of Object.entries(providersObj)) {
17293
+ providers[name] = validateProviderConfig(name, config2);
17294
+ }
17295
+ if (!paths.logs) {
17296
+ paths.logs = ".memory";
17297
+ }
17298
+ return { paths, providers };
17299
+ }, `Configuration file not found: ${CONFIG_FILE2}. Please create this file to configure statement imports.`);
17890
17300
  }
17891
17301
 
17892
17302
  // src/utils/providerDetector.ts
@@ -17994,23 +17404,84 @@ function detectProvider(filename, content, config2) {
17994
17404
  return null;
17995
17405
  }
17996
17406
 
17997
- // src/utils/fileUtils.ts
17407
+ // src/utils/importContext.ts
17998
17408
  import * as fs4 from "fs";
17999
17409
  import * as path5 from "path";
18000
- function findCSVFiles(importsDir) {
18001
- if (!fs4.existsSync(importsDir)) {
18002
- return [];
17410
+ import { randomUUID } from "crypto";
17411
+ function getContextPath(directory, contextId) {
17412
+ return path5.join(directory, ".memory", `${contextId}.json`);
17413
+ }
17414
+ function ensureMemoryDir(directory) {
17415
+ const memoryDir = path5.join(directory, ".memory");
17416
+ if (!fs4.existsSync(memoryDir)) {
17417
+ fs4.mkdirSync(memoryDir, { recursive: true });
18003
17418
  }
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
17419
  }
18009
- function ensureDirectory(dirPath) {
18010
- if (!fs4.existsSync(dirPath)) {
18011
- fs4.mkdirSync(dirPath, { recursive: true });
17420
+ function createContext(directory, params) {
17421
+ const now = new Date().toISOString();
17422
+ const context = {
17423
+ id: randomUUID(),
17424
+ createdAt: now,
17425
+ updatedAt: now,
17426
+ filename: params.filename,
17427
+ filePath: params.filePath,
17428
+ provider: params.provider,
17429
+ currency: params.currency,
17430
+ accountNumber: params.accountNumber,
17431
+ originalFilename: params.originalFilename,
17432
+ fromDate: params.fromDate,
17433
+ untilDate: params.untilDate,
17434
+ openingBalance: params.openingBalance,
17435
+ closingBalance: params.closingBalance,
17436
+ account: params.account
17437
+ };
17438
+ ensureMemoryDir(directory);
17439
+ const contextPath = getContextPath(directory, context.id);
17440
+ fs4.writeFileSync(contextPath, JSON.stringify(context, null, 2), "utf-8");
17441
+ return context;
17442
+ }
17443
+ function validateContext(context, contextId) {
17444
+ const requiredFields = [
17445
+ "id",
17446
+ "filename",
17447
+ "filePath",
17448
+ "provider",
17449
+ "currency"
17450
+ ];
17451
+ for (const field of requiredFields) {
17452
+ if (!context[field]) {
17453
+ throw new Error(`Invalid context ${contextId}: missing required field '${field}'`);
17454
+ }
18012
17455
  }
18013
17456
  }
17457
+ function loadContext(directory, contextId) {
17458
+ const contextPath = getContextPath(directory, contextId);
17459
+ if (!fs4.existsSync(contextPath)) {
17460
+ throw new Error(`Context not found: ${contextId}`);
17461
+ }
17462
+ const content = fs4.readFileSync(contextPath, "utf-8");
17463
+ let context;
17464
+ try {
17465
+ context = JSON.parse(content);
17466
+ } catch (err) {
17467
+ throw new Error(`Malformed context file ${contextId}: ${err instanceof Error ? err.message : String(err)}`);
17468
+ }
17469
+ validateContext(context, contextId);
17470
+ return context;
17471
+ }
17472
+ function updateContext(directory, contextId, updates) {
17473
+ const context = loadContext(directory, contextId);
17474
+ const updatedContext = {
17475
+ ...context,
17476
+ ...updates,
17477
+ id: context.id,
17478
+ createdAt: context.createdAt,
17479
+ updatedAt: new Date().toISOString()
17480
+ };
17481
+ const contextPath = getContextPath(directory, contextId);
17482
+ fs4.writeFileSync(contextPath, JSON.stringify(updatedContext, null, 2), "utf-8");
17483
+ return updatedContext;
17484
+ }
18014
17485
 
18015
17486
  // src/tools/classify-statements.ts
18016
17487
  function buildSuccessResult2(classified, unrecognized, message) {
@@ -18078,7 +17549,20 @@ function planMoves(csvFiles, importsDir, pendingDir, unrecognizedDir, config2) {
18078
17549
  }
18079
17550
  return { plannedMoves, collisions };
18080
17551
  }
18081
- function executeMoves(plannedMoves, config2, unrecognizedDir) {
17552
+ function extractMetadata2(detection) {
17553
+ const metadata = detection.metadata;
17554
+ if (!metadata) {
17555
+ return {};
17556
+ }
17557
+ return {
17558
+ accountNumber: metadata["account-number"],
17559
+ fromDate: metadata["from-date"],
17560
+ untilDate: metadata["until-date"],
17561
+ openingBalance: metadata["opening-balance"],
17562
+ closingBalance: metadata["closing-balance"]
17563
+ };
17564
+ }
17565
+ function executeMoves(plannedMoves, config2, unrecognizedDir, directory) {
18082
17566
  const classified = [];
18083
17567
  const unrecognized = [];
18084
17568
  for (const move of plannedMoves) {
@@ -18086,12 +17570,27 @@ function executeMoves(plannedMoves, config2, unrecognizedDir) {
18086
17570
  const targetDir = path6.dirname(move.targetPath);
18087
17571
  ensureDirectory(targetDir);
18088
17572
  fs5.renameSync(move.sourcePath, move.targetPath);
17573
+ const targetPath = path6.join(config2.paths.pending, move.detection.provider, move.detection.currency, move.targetFilename);
17574
+ const metadata = extractMetadata2(move.detection);
17575
+ const context = createContext(directory, {
17576
+ filename: move.targetFilename,
17577
+ filePath: targetPath,
17578
+ provider: move.detection.provider,
17579
+ currency: move.detection.currency,
17580
+ originalFilename: move.detection.outputFilename ? move.filename : undefined,
17581
+ accountNumber: metadata.accountNumber,
17582
+ fromDate: metadata.fromDate,
17583
+ untilDate: metadata.untilDate,
17584
+ openingBalance: metadata.openingBalance,
17585
+ closingBalance: metadata.closingBalance
17586
+ });
18089
17587
  classified.push({
18090
17588
  filename: move.targetFilename,
18091
17589
  originalFilename: move.detection.outputFilename ? move.filename : undefined,
18092
17590
  provider: move.detection.provider,
18093
17591
  currency: move.detection.currency,
18094
- targetPath: path6.join(config2.paths.pending, move.detection.provider, move.detection.currency, move.targetFilename)
17592
+ targetPath,
17593
+ contextId: context.id
18095
17594
  });
18096
17595
  } else {
18097
17596
  ensureDirectory(unrecognizedDir);
@@ -18122,7 +17621,7 @@ async function classifyStatements(directory, agent, configLoader = loadImportCon
18122
17621
  const importsDir = path6.join(directory, config2.paths.import);
18123
17622
  const pendingDir = path6.join(directory, config2.paths.pending);
18124
17623
  const unrecognizedDir = path6.join(directory, config2.paths.unrecognized);
18125
- const csvFiles = findCSVFiles(importsDir);
17624
+ const csvFiles = findCsvFiles(importsDir);
18126
17625
  if (csvFiles.length === 0) {
18127
17626
  return buildSuccessResult2([], [], `No CSV files found in ${config2.paths.import}`);
18128
17627
  }
@@ -18130,11 +17629,19 @@ async function classifyStatements(directory, agent, configLoader = loadImportCon
18130
17629
  if (collisions.length > 0) {
18131
17630
  return buildCollisionError(collisions);
18132
17631
  }
18133
- const { classified, unrecognized } = executeMoves(plannedMoves, config2, unrecognizedDir);
17632
+ const { classified, unrecognized } = executeMoves(plannedMoves, config2, unrecognizedDir, directory);
18134
17633
  return buildSuccessResult2(classified, unrecognized);
18135
17634
  }
18136
17635
  var classify_statements_default = tool({
18137
- 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.",
17636
+ 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.
17637
+
17638
+ For each CSV file:
17639
+ - Detects the provider and currency using rules from providers.yaml
17640
+ - Creates an import context (.memory/{uuid}.json) to track the file through the pipeline
17641
+ - Extracts metadata (account number, dates, balances) from CSV headers
17642
+ - Moves the file to import/pending/{provider}/{currency}/
17643
+ - Checks for file collisions before any moves (atomic: all-or-nothing)
17644
+ - Unrecognized files are moved to import/unrecognized/`,
18138
17645
  args: {},
18139
17646
  async execute(_params, context) {
18140
17647
  const { directory, agent } = context;
@@ -20474,7 +19981,7 @@ class LRUCache {
20474
19981
  // node_modules/path-scurry/dist/esm/index.js
20475
19982
  import { posix, win32 } from "path";
20476
19983
  import { fileURLToPath } from "url";
20477
- import { lstatSync, readdir as readdirCB, readdirSync as readdirSync3, readlinkSync, realpathSync as rps } from "fs";
19984
+ import { lstatSync, readdir as readdirCB, readdirSync as readdirSync2, readlinkSync, realpathSync as rps } from "fs";
20478
19985
  import * as actualFS from "fs";
20479
19986
  import { lstat, readdir, readlink, realpath } from "fs/promises";
20480
19987
 
@@ -21146,7 +20653,7 @@ var realpathSync = rps.native;
21146
20653
  var defaultFS = {
21147
20654
  lstatSync,
21148
20655
  readdir: readdirCB,
21149
- readdirSync: readdirSync3,
20656
+ readdirSync: readdirSync2,
21150
20657
  readlinkSync,
21151
20658
  realpathSync,
21152
20659
  promises: {
@@ -23456,7 +22963,12 @@ function loadRulesMapping(rulesDir) {
23456
22963
  if (!stat.isFile()) {
23457
22964
  continue;
23458
22965
  }
23459
- const content = fs6.readFileSync(rulesFilePath, "utf-8");
22966
+ let content;
22967
+ try {
22968
+ content = fs6.readFileSync(rulesFilePath, "utf-8");
22969
+ } catch {
22970
+ continue;
22971
+ }
23460
22972
  const sourcePath = parseSourceDirective(content);
23461
22973
  if (!sourcePath) {
23462
22974
  continue;
@@ -23499,18 +23011,30 @@ function findRulesForCsv(csvPath, mapping) {
23499
23011
 
23500
23012
  // src/utils/hledgerExecutor.ts
23501
23013
  var {$: $2 } = globalThis.Bun;
23014
+ var STDERR_TRUNCATE_LENGTH = 500;
23502
23015
  async function defaultHledgerExecutor(cmdArgs) {
23503
23016
  try {
23504
23017
  const result = await $2`hledger ${cmdArgs}`.quiet().nothrow();
23018
+ const stdout = result.stdout.toString();
23019
+ const stderr = result.stderr.toString();
23020
+ if (result.exitCode !== 0 && stderr) {
23021
+ process.stderr.write(`[hledger] command failed (exit ${result.exitCode}): hledger ${cmdArgs.join(" ")}
23022
+ `);
23023
+ process.stderr.write(`[hledger] stderr: ${stderr.slice(0, STDERR_TRUNCATE_LENGTH)}
23024
+ `);
23025
+ }
23505
23026
  return {
23506
- stdout: result.stdout.toString(),
23507
- stderr: result.stderr.toString(),
23027
+ stdout,
23028
+ stderr,
23508
23029
  exitCode: result.exitCode
23509
23030
  };
23510
23031
  } catch (error45) {
23032
+ const errorMessage = error45 instanceof Error ? error45.message : String(error45);
23033
+ process.stderr.write(`[hledger] exception: ${errorMessage}
23034
+ `);
23511
23035
  return {
23512
23036
  stdout: "",
23513
- stderr: error45 instanceof Error ? error45.message : String(error45),
23037
+ stderr: errorMessage,
23514
23038
  exitCode: 1
23515
23039
  };
23516
23040
  }
@@ -23701,7 +23225,7 @@ function parseRulesFile(rulesContent) {
23701
23225
  }
23702
23226
 
23703
23227
  // src/utils/csvParser.ts
23704
- var import_convert_csv_to_json = __toESM(require_convert_csv_to_json(), 1);
23228
+ var import_papaparse2 = __toESM(require_papaparse(), 1);
23705
23229
  import * as fs8 from "fs";
23706
23230
 
23707
23231
  // src/utils/balanceUtils.ts
@@ -23750,6 +23274,7 @@ function balancesMatch(balance1, balance2) {
23750
23274
  }
23751
23275
 
23752
23276
  // src/utils/csvParser.ts
23277
+ var AMOUNT_MATCH_TOLERANCE = 0.001;
23753
23278
  function parseCsvFile(csvPath, config2) {
23754
23279
  const csvContent = fs8.readFileSync(csvPath, "utf-8");
23755
23280
  const lines = csvContent.split(`
@@ -23758,22 +23283,26 @@ function parseCsvFile(csvPath, config2) {
23758
23283
  if (headerIndex >= lines.length) {
23759
23284
  return [];
23760
23285
  }
23761
- const headerLine = lines[headerIndex];
23762
- const dataLines = lines.slice(headerIndex + 1).filter((line) => line.trim() !== "");
23763
- const csvWithHeader = [headerLine, ...dataLines].join(`
23286
+ const csvWithHeader = lines.slice(headerIndex).join(`
23764
23287
  `);
23765
- const rawRows = import_convert_csv_to_json.default.indexHeader(0).fieldDelimiter(config2.separator).supportQuotedField(true).csvStringToJson(csvWithHeader);
23766
- const fieldNames = config2.fieldNames.length > 0 ? config2.fieldNames : Object.keys(rawRows[0] || {});
23767
- const mappedRows = [];
23768
- for (const parsedRow of rawRows) {
23769
- const row = {};
23770
- const values = Object.values(parsedRow);
23771
- for (let i2 = 0;i2 < fieldNames.length && i2 < values.length; i2++) {
23772
- row[fieldNames[i2]] = values[i2];
23773
- }
23774
- mappedRows.push(row);
23288
+ const useFieldNames = config2.fieldNames.length > 0;
23289
+ const result = import_papaparse2.default.parse(csvWithHeader, {
23290
+ header: !useFieldNames,
23291
+ delimiter: config2.separator,
23292
+ skipEmptyLines: true
23293
+ });
23294
+ if (useFieldNames) {
23295
+ const rawRows = result.data;
23296
+ const dataRows = rawRows.slice(1);
23297
+ return dataRows.map((values) => {
23298
+ const row = {};
23299
+ for (let i2 = 0;i2 < config2.fieldNames.length && i2 < values.length; i2++) {
23300
+ row[config2.fieldNames[i2]] = values[i2];
23301
+ }
23302
+ return row;
23303
+ });
23775
23304
  }
23776
- return mappedRows;
23305
+ return result.data;
23777
23306
  }
23778
23307
  function getRowAmount(row, amountFields) {
23779
23308
  if (amountFields.single) {
@@ -23851,7 +23380,7 @@ function findMatchingCsvRow(posting, csvRows, config2) {
23851
23380
  const rowAmount = getRowAmount(row, config2.amountFields);
23852
23381
  if (rowDate !== posting.date)
23853
23382
  return false;
23854
- if (Math.abs(rowAmount - postingAmount) > 0.001)
23383
+ if (Math.abs(rowAmount - postingAmount) > AMOUNT_MATCH_TOLERANCE)
23855
23384
  return false;
23856
23385
  return true;
23857
23386
  });
@@ -23885,11 +23414,21 @@ function findMatchingCsvRow(posting, csvRows, config2) {
23885
23414
 
23886
23415
  // src/tools/import-statements.ts
23887
23416
  function buildErrorResult3(error45, hint) {
23888
- return JSON.stringify({
23417
+ const result = {
23889
23418
  success: false,
23890
23419
  error: error45,
23891
- hint
23892
- });
23420
+ hint,
23421
+ files: [],
23422
+ summary: {
23423
+ filesProcessed: 0,
23424
+ filesWithErrors: 0,
23425
+ filesWithoutRules: 0,
23426
+ totalTransactions: 0,
23427
+ matched: 0,
23428
+ unknown: 0
23429
+ }
23430
+ };
23431
+ return JSON.stringify(result);
23893
23432
  }
23894
23433
  function buildErrorResultWithDetails(error45, files, summary, hint) {
23895
23434
  return JSON.stringify({
@@ -24068,17 +23607,12 @@ async function importStatements(directory, agent, options, configLoader = loadIm
24068
23607
  const rulesDir = path9.join(directory, config2.paths.rules);
24069
23608
  const doneDir = path9.join(directory, config2.paths.done);
24070
23609
  const rulesMapping = loadRulesMapping(rulesDir);
24071
- const csvFiles = findCsvFiles(pendingDir, options.provider, options.currency);
24072
- if (csvFiles.length === 0) {
24073
- return buildSuccessResult3([], {
24074
- filesProcessed: 0,
24075
- filesWithErrors: 0,
24076
- filesWithoutRules: 0,
24077
- totalTransactions: 0,
24078
- matched: 0,
24079
- unknown: 0
24080
- }, "No CSV files found to process");
23610
+ const importContext = loadContext(directory, options.contextId);
23611
+ const csvPath = path9.join(directory, importContext.filePath);
23612
+ if (!fs9.existsSync(csvPath)) {
23613
+ return buildErrorResult3(`CSV file not found: ${importContext.filePath}`, "The file may have been moved or deleted");
24081
23614
  }
23615
+ const csvFiles = [csvPath];
24082
23616
  const fileResults = [];
24083
23617
  let totalTransactions = 0;
24084
23618
  let totalMatched = 0;
@@ -24169,6 +23703,16 @@ async function importStatements(directory, agent, options, configLoader = loadIm
24169
23703
  unknown: totalUnknown
24170
23704
  }, importResult.hint);
24171
23705
  }
23706
+ if (fileResults.length > 0) {
23707
+ const firstResult = fileResults[0];
23708
+ const newFilePath = path9.join(config2.paths.done, path9.relative(pendingDir, path9.join(directory, firstResult.csv)));
23709
+ updateContext(directory, options.contextId, {
23710
+ filePath: newFilePath,
23711
+ rulesFile: firstResult.rulesFile || undefined,
23712
+ yearJournal: firstResult.transactionYear ? `ledger/${firstResult.transactionYear}.journal` : undefined,
23713
+ transactionCount: firstResult.totalTransactions
23714
+ });
23715
+ }
24172
23716
  return buildSuccessResult3(fileResults.map((f) => ({
24173
23717
  ...f,
24174
23718
  imported: true
@@ -24204,15 +23748,13 @@ This tool processes CSV files in the pending import directory and uses hledger's
24204
23748
 
24205
23749
  Note: This tool is typically called via import-pipeline for the full workflow.`,
24206
23750
  args: {
24207
- provider: tool.schema.string().optional().describe('Filter by provider (e.g., "revolut", "ubs"). If omitted, process all providers.'),
24208
- currency: tool.schema.string().optional().describe('Filter by currency (e.g., "chf", "eur"). If omitted, process all currencies for the provider.'),
23751
+ contextId: tool.schema.string().describe("Context ID from classify step. Used to locate the specific CSV file to process."),
24209
23752
  checkOnly: tool.schema.boolean().optional().describe("If true (default), only check for unknown accounts without importing. Set to false to perform actual import.")
24210
23753
  },
24211
23754
  async execute(params, context) {
24212
23755
  const { directory, agent } = context;
24213
23756
  return importStatements(directory, agent, {
24214
- provider: params.provider,
24215
- currency: params.currency,
23757
+ contextId: params.contextId,
24216
23758
  checkOnly: params.checkOnly
24217
23759
  });
24218
23760
  }
@@ -24221,10 +23763,19 @@ Note: This tool is typically called via import-pipeline for the full workflow.`,
24221
23763
  import * as fs10 from "fs";
24222
23764
  import * as path10 from "path";
24223
23765
  function buildErrorResult4(params) {
24224
- return JSON.stringify({
23766
+ const result = {
24225
23767
  success: false,
24226
- ...params
24227
- });
23768
+ account: params.account ?? "",
23769
+ expectedBalance: params.expectedBalance ?? "",
23770
+ actualBalance: params.actualBalance ?? "",
23771
+ lastTransactionDate: params.lastTransactionDate ?? "",
23772
+ csvFile: params.csvFile ?? "",
23773
+ difference: params.difference,
23774
+ metadata: params.metadata,
23775
+ error: params.error,
23776
+ hint: params.hint
23777
+ };
23778
+ return JSON.stringify(result);
24228
23779
  }
24229
23780
  function loadConfiguration(directory, configLoader) {
24230
23781
  try {
@@ -24239,69 +23790,87 @@ function loadConfiguration(directory, configLoader) {
24239
23790
  };
24240
23791
  }
24241
23792
  }
24242
- function findCsvToReconcile(doneDir, options) {
24243
- const csvFiles = findCsvFiles(doneDir, options.provider, options.currency);
24244
- if (csvFiles.length === 0) {
24245
- const providerFilter = options.provider ? ` --provider=${options.provider}` : "";
24246
- const currencyFilter = options.currency ? ` --currency=${options.currency}` : "";
23793
+ function verifyCsvExists(directory, importContext) {
23794
+ const csvFile = path10.join(directory, importContext.filePath);
23795
+ if (!fs10.existsSync(csvFile)) {
24247
23796
  return {
24248
23797
  error: buildErrorResult4({
24249
- error: `No CSV files found in ${doneDir}`,
24250
- hint: `Run: import-statements${providerFilter}${currencyFilter}`
23798
+ error: `CSV file not found: ${importContext.filePath}`,
23799
+ hint: `The file may have been moved or deleted. Context ID: ${importContext.id}`
24251
23800
  })
24252
23801
  };
24253
23802
  }
24254
- const csvFile = csvFiles[csvFiles.length - 1];
24255
- const relativePath = path10.relative(path10.dirname(path10.dirname(doneDir)), csvFile);
24256
- return { csvFile, relativePath };
23803
+ return { csvFile, relativePath: importContext.filePath };
23804
+ }
23805
+ function getBalanceFromContext(importContext) {
23806
+ if (!importContext.closingBalance)
23807
+ return;
23808
+ let balance = importContext.closingBalance;
23809
+ if (importContext.currency && !balance.includes(importContext.currency.toUpperCase())) {
23810
+ balance = `${importContext.currency.toUpperCase()} ${balance}`;
23811
+ }
23812
+ return balance;
23813
+ }
23814
+ function getBalanceFromCsvMetadata(metadata) {
23815
+ if (!metadata?.["closing-balance"])
23816
+ return;
23817
+ let balance = metadata["closing-balance"];
23818
+ const currency = metadata.currency;
23819
+ if (currency && balance && !balance.includes(currency)) {
23820
+ balance = `${currency} ${balance}`;
23821
+ }
23822
+ return balance;
23823
+ }
23824
+ function getBalanceFromCsvAnalysis(csvFile, rulesDir) {
23825
+ const csvAnalysis = tryExtractClosingBalanceFromCSV(csvFile, rulesDir);
23826
+ if (csvAnalysis && csvAnalysis.confidence === "high") {
23827
+ return csvAnalysis.balance;
23828
+ }
23829
+ return;
24257
23830
  }
24258
- function determineClosingBalance(csvFile, config2, options, relativeCsvPath, rulesDir) {
24259
- let metadata;
23831
+ function extractCsvMetadata(csvFile, config2) {
24260
23832
  try {
24261
23833
  const content = fs10.readFileSync(csvFile, "utf-8");
24262
23834
  const filename = path10.basename(csvFile);
24263
23835
  const detectionResult = detectProvider(filename, content, config2);
24264
- metadata = detectionResult?.metadata;
24265
- } catch {
24266
- metadata = undefined;
24267
- }
24268
- let closingBalance = options.closingBalance;
24269
- if (!closingBalance && metadata?.["closing-balance"]) {
24270
- const closingBalanceValue = metadata["closing-balance"];
24271
- const currency = metadata.currency;
24272
- closingBalance = closingBalanceValue;
24273
- if (currency && closingBalance && !closingBalance.includes(currency)) {
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 };
23836
+ if (detectionResult?.metadata) {
23837
+ const m = detectionResult.metadata;
23838
+ return {
23839
+ "from-date": m["from-date"],
23840
+ "until-date": m["until-date"],
23841
+ "opening-balance": m["opening-balance"],
23842
+ "closing-balance": m["closing-balance"],
23843
+ currency: m["currency"],
23844
+ "account-number": m["account-number"]
23845
+ };
24282
23846
  }
24283
- }
24284
- if (!closingBalance) {
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 };
23847
+ } catch {}
23848
+ return;
24296
23849
  }
24297
- function buildRetryCommand(options, closingBalance, account) {
24298
- const parts = ["import-pipeline"];
24299
- if (options.provider) {
24300
- parts.push(`--provider ${options.provider}`);
23850
+ function determineClosingBalance(csvFile, config2, importContext, manualClosingBalance, relativeCsvPath, rulesDir) {
23851
+ const metadata = extractCsvMetadata(csvFile, config2);
23852
+ const closingBalance = manualClosingBalance || getBalanceFromContext(importContext) || getBalanceFromCsvMetadata(metadata);
23853
+ if (closingBalance) {
23854
+ return { closingBalance, metadata };
24301
23855
  }
24302
- if (options.currency) {
24303
- parts.push(`--currency ${options.currency}`);
23856
+ const analysisBalance = getBalanceFromCsvAnalysis(csvFile, rulesDir);
23857
+ if (analysisBalance) {
23858
+ return { closingBalance: analysisBalance, metadata, fromCSVAnalysis: true };
24304
23859
  }
23860
+ const currency = importContext.currency?.toUpperCase() || "CHF";
23861
+ const exampleBalance = `${currency} <amount>`;
23862
+ const retryCmd = buildRetryCommand(importContext.id, exampleBalance);
23863
+ return {
23864
+ error: buildErrorResult4({
23865
+ csvFile: relativeCsvPath,
23866
+ error: "No closing balance found in CSV metadata or data",
23867
+ hint: `Provide closingBalance parameter manually. Example retry: ${retryCmd}`,
23868
+ metadata
23869
+ })
23870
+ };
23871
+ }
23872
+ function buildRetryCommand(contextId, closingBalance, account) {
23873
+ const parts = ["reconcile-statement", `--contextId ${contextId}`];
24305
23874
  if (closingBalance) {
24306
23875
  parts.push(`--closingBalance "${closingBalance}"`);
24307
23876
  }
@@ -24310,19 +23879,17 @@ function buildRetryCommand(options, closingBalance, account) {
24310
23879
  }
24311
23880
  return parts.join(" ");
24312
23881
  }
24313
- function determineAccount(csvFile, rulesDir, options, relativeCsvPath, metadata) {
24314
- let account = options.account;
23882
+ function determineAccount(csvFile, rulesDir, importContext, manualAccount, relativeCsvPath, metadata) {
23883
+ let account = manualAccount;
23884
+ const rulesMapping = loadRulesMapping(rulesDir);
23885
+ const rulesFile = findRulesForCsv(csvFile, rulesMapping);
24315
23886
  if (!account) {
24316
- const rulesMapping = loadRulesMapping(rulesDir);
24317
- const rulesFile = findRulesForCsv(csvFile, rulesMapping);
24318
23887
  if (rulesFile) {
24319
23888
  account = getAccountFromRulesFile(rulesFile) ?? undefined;
24320
23889
  }
24321
23890
  }
24322
23891
  if (!account) {
24323
- const rulesMapping = loadRulesMapping(rulesDir);
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:...")}`;
23892
+ 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
23893
  return {
24327
23894
  error: buildErrorResult4({
24328
23895
  csvFile: relativeCsvPath,
@@ -24403,25 +23970,33 @@ async function reconcileStatement(directory, agent, options, configLoader = load
24403
23970
  if (restrictionError) {
24404
23971
  return restrictionError;
24405
23972
  }
23973
+ let importContext;
23974
+ try {
23975
+ importContext = loadContext(directory, options.contextId);
23976
+ } catch {
23977
+ return buildErrorResult4({
23978
+ error: `Failed to load import context: ${options.contextId}`,
23979
+ hint: "Ensure the context ID is valid and the context file exists in .memory/"
23980
+ });
23981
+ }
24406
23982
  const configResult = loadConfiguration(directory, configLoader);
24407
23983
  if ("error" in configResult) {
24408
23984
  return configResult.error;
24409
23985
  }
24410
23986
  const { config: config2 } = configResult;
24411
- const doneDir = path10.join(directory, config2.paths.done);
24412
23987
  const rulesDir = path10.join(directory, config2.paths.rules);
24413
23988
  const mainJournalPath = path10.join(directory, ".hledger.journal");
24414
- const csvResult = findCsvToReconcile(doneDir, options);
23989
+ const csvResult = verifyCsvExists(directory, importContext);
24415
23990
  if ("error" in csvResult) {
24416
23991
  return csvResult.error;
24417
23992
  }
24418
23993
  const { csvFile, relativePath: relativeCsvPath } = csvResult;
24419
- const balanceResult = determineClosingBalance(csvFile, config2, options, relativeCsvPath, rulesDir);
23994
+ const balanceResult = determineClosingBalance(csvFile, config2, importContext, options.closingBalance, relativeCsvPath, rulesDir);
24420
23995
  if ("error" in balanceResult) {
24421
23996
  return balanceResult.error;
24422
23997
  }
24423
23998
  const { closingBalance, metadata, fromCSVAnalysis } = balanceResult;
24424
- const accountResult = determineAccount(csvFile, rulesDir, options, relativeCsvPath, metadata);
23999
+ const accountResult = determineAccount(csvFile, rulesDir, importContext, options.account, relativeCsvPath, metadata);
24425
24000
  if ("error" in accountResult) {
24426
24001
  return accountResult.error;
24427
24002
  }
@@ -24508,30 +24083,30 @@ var reconcile_statement_default = tool({
24508
24083
  This tool validates that the imported transactions result in the correct closing balance.
24509
24084
 
24510
24085
  **Workflow:**
24511
- 1. Finds the most recently imported CSV in the done directory
24512
- 2. Extracts closing balance from CSV metadata (or uses manual override)
24086
+ 1. Loads import context to find the CSV file
24087
+ 2. Extracts closing balance from context/CSV metadata (or uses manual override)
24513
24088
  3. Determines the account from the matching rules file (or uses manual override)
24514
24089
  4. Queries hledger for the actual balance as of the last transaction date
24515
24090
  5. Compares expected vs actual balance
24516
24091
 
24517
24092
  **Balance Sources:**
24518
- - Automatic: Extracted from CSV header metadata (e.g., UBS files have "Closing balance:" row)
24093
+ - Automatic: From import context or CSV header metadata (e.g., UBS files have "Closing balance:" row)
24519
24094
  - Manual: Provided via closingBalance parameter (required for providers like Revolut)
24520
24095
 
24521
24096
  **Account Detection:**
24522
24097
  - Automatic: Parsed from account1 directive in matching rules file
24523
- - Manual: Provided via account parameter`,
24098
+ - Manual: Provided via account parameter
24099
+
24100
+ Note: This tool requires a contextId from a prior classify/import step.`,
24524
24101
  args: {
24525
- provider: tool.schema.string().optional().describe('Filter by provider (e.g., "ubs", "revolut")'),
24526
- currency: tool.schema.string().optional().describe('Filter by currency (e.g., "chf", "eur")'),
24102
+ contextId: tool.schema.string().describe("Context ID from classify/import step (required)"),
24527
24103
  closingBalance: tool.schema.string().optional().describe('Manual closing balance (e.g., "CHF 2324.79"). Required if not in CSV metadata.'),
24528
24104
  account: tool.schema.string().optional().describe('Manual account (e.g., "assets:bank:ubs:checking"). Auto-detected from rules file if not provided.')
24529
24105
  },
24530
24106
  async execute(params, context) {
24531
24107
  const { directory, agent } = context;
24532
24108
  return reconcileStatement(directory, agent, {
24533
- provider: params.provider,
24534
- currency: params.currency,
24109
+ contextId: params.contextId,
24535
24110
  closingBalance: params.closingBalance,
24536
24111
  account: params.account
24537
24112
  });
@@ -24658,6 +24233,7 @@ function ensureAccountDeclarations(yearJournalPath, accounts) {
24658
24233
  // src/utils/logger.ts
24659
24234
  import fs12 from "fs/promises";
24660
24235
  import path11 from "path";
24236
+ var LOG_LINE_LIMIT = 50;
24661
24237
 
24662
24238
  class MarkdownLogger {
24663
24239
  buffer = [];
@@ -24665,6 +24241,7 @@ class MarkdownLogger {
24665
24241
  context = {};
24666
24242
  autoFlush;
24667
24243
  sectionDepth = 0;
24244
+ pendingFlush = null;
24668
24245
  constructor(config2) {
24669
24246
  this.autoFlush = config2.autoFlush ?? true;
24670
24247
  this.context = config2.context || {};
@@ -24739,9 +24316,9 @@ class MarkdownLogger {
24739
24316
  this.buffer.push("");
24740
24317
  const lines = output.trim().split(`
24741
24318
  `);
24742
- if (lines.length > 50) {
24743
- this.buffer.push(...lines.slice(0, 50));
24744
- this.buffer.push(`... (${lines.length - 50} more lines omitted)`);
24319
+ if (lines.length > LOG_LINE_LIMIT) {
24320
+ this.buffer.push(...lines.slice(0, LOG_LINE_LIMIT));
24321
+ this.buffer.push(`... (${lines.length - LOG_LINE_LIMIT} more lines omitted)`);
24745
24322
  } else {
24746
24323
  this.buffer.push(output.trim());
24747
24324
  }
@@ -24763,6 +24340,9 @@ class MarkdownLogger {
24763
24340
  this.context[key] = value;
24764
24341
  }
24765
24342
  async flush() {
24343
+ if (this.pendingFlush) {
24344
+ await this.pendingFlush;
24345
+ }
24766
24346
  if (this.buffer.length === 0)
24767
24347
  return;
24768
24348
  try {
@@ -24775,7 +24355,7 @@ class MarkdownLogger {
24775
24355
  return this.logPath;
24776
24356
  }
24777
24357
  flushAsync() {
24778
- this.flush().catch(() => {});
24358
+ this.pendingFlush = this.flush().catch(() => {});
24779
24359
  }
24780
24360
  getTimestamp() {
24781
24361
  return new Date().toISOString().replace(/:/g, "-").split(".")[0];
@@ -24841,16 +24421,28 @@ async function executeClassifyStep(context, logger) {
24841
24421
  logger?.info("Classification skipped (skipClassify: true)");
24842
24422
  context.result.steps.classify = buildStepResult(true, "Classification skipped (skipClassify: true)");
24843
24423
  logger?.endSection();
24844
- return;
24424
+ return [];
24845
24425
  }
24846
24426
  const classifyResult = await classifyStatements(context.directory, context.agent, context.configLoader);
24847
24427
  const classifyParsed = JSON.parse(classifyResult);
24848
24428
  const success2 = classifyParsed.success !== false;
24429
+ const contextIds = [];
24430
+ if (classifyParsed.classified?.length > 0) {
24431
+ for (const file2 of classifyParsed.classified) {
24432
+ if (file2.contextId) {
24433
+ contextIds.push(file2.contextId);
24434
+ }
24435
+ }
24436
+ }
24849
24437
  let message = success2 ? "Classification complete" : "Classification had issues";
24850
24438
  if (classifyParsed.unrecognized?.length > 0) {
24851
24439
  message = `Classification complete with ${classifyParsed.unrecognized.length} unrecognized file(s)`;
24852
24440
  logger?.warn(`${classifyParsed.unrecognized.length} unrecognized file(s)`);
24853
24441
  }
24442
+ if (contextIds.length > 0) {
24443
+ message += ` (${contextIds.length} context(s) created)`;
24444
+ logger?.info(`Created ${contextIds.length} import context(s)`);
24445
+ }
24854
24446
  logger?.logStep("Classify", success2 ? "success" : "error", message);
24855
24447
  const details = {
24856
24448
  success: success2,
@@ -24858,23 +24450,18 @@ async function executeClassifyStep(context, logger) {
24858
24450
  classified: classifyParsed
24859
24451
  };
24860
24452
  context.result.steps.classify = buildStepResult(success2, message, details);
24453
+ context.result.contexts = contextIds;
24861
24454
  logger?.endSection();
24455
+ return contextIds;
24862
24456
  }
24863
- async function executeAccountDeclarationsStep(context, logger) {
24457
+ async function executeAccountDeclarationsStep(context, contextId, logger) {
24864
24458
  logger?.startSection("Step 2: Check Account Declarations");
24865
24459
  logger?.logStep("Check Accounts", "start");
24866
24460
  const config2 = context.configLoader(context.directory);
24867
- const pendingDir = path12.join(context.directory, config2.paths.pending);
24868
24461
  const rulesDir = path12.join(context.directory, config2.paths.rules);
24869
- const csvFiles = findCsvFiles(pendingDir, context.options.provider, context.options.currency);
24870
- if (csvFiles.length === 0) {
24871
- context.result.steps.accountDeclarations = buildStepResult(true, "No CSV files to process", {
24872
- accountsAdded: [],
24873
- journalUpdated: "",
24874
- rulesScanned: []
24875
- });
24876
- return;
24877
- }
24462
+ const importCtx = loadContext(context.directory, contextId);
24463
+ const csvPath = path12.join(context.directory, importCtx.filePath);
24464
+ const csvFiles = [csvPath];
24878
24465
  const rulesMapping = loadRulesMapping(rulesDir);
24879
24466
  const matchedRulesFiles = new Set;
24880
24467
  for (const csvFile of csvFiles) {
@@ -24949,12 +24536,11 @@ async function executeAccountDeclarationsStep(context, logger) {
24949
24536
  });
24950
24537
  logger?.endSection();
24951
24538
  }
24952
- async function executeDryRunStep(context, logger) {
24539
+ async function executeDryRunStep(context, contextId, logger) {
24953
24540
  logger?.startSection("Step 3: Dry Run Import");
24954
24541
  logger?.logStep("Dry Run", "start");
24955
24542
  const dryRunResult = await importStatements(context.directory, context.agent, {
24956
- provider: context.options.provider,
24957
- currency: context.options.currency,
24543
+ contextId,
24958
24544
  checkOnly: true
24959
24545
  }, context.configLoader, context.hledgerExecutor);
24960
24546
  const dryRunParsed = JSON.parse(dryRunResult);
@@ -24979,30 +24565,25 @@ async function executeDryRunStep(context, logger) {
24979
24565
  extractRulePatternsFromFile: extractRulePatternsFromFile2
24980
24566
  } = await Promise.resolve().then(() => (init_accountSuggester(), exports_accountSuggester));
24981
24567
  const config2 = context.configLoader(context.directory);
24982
- const pendingDir = path12.join(context.directory, config2.paths.pending);
24983
24568
  const rulesDir = path12.join(context.directory, config2.paths.rules);
24984
- const csvFiles = findCsvFiles(pendingDir, context.options.provider, context.options.currency);
24569
+ const importCtx = loadContext(context.directory, contextId);
24570
+ const csvPath = path12.join(context.directory, importCtx.filePath);
24985
24571
  const rulesMapping = loadRulesMapping(rulesDir);
24986
24572
  let yearJournalPath;
24987
24573
  let firstRulesFile;
24988
- for (const csvFile of csvFiles) {
24989
- const rulesFile = findRulesForCsv(csvFile, rulesMapping);
24990
- if (rulesFile) {
24991
- firstRulesFile = rulesFile;
24992
- try {
24993
- const result = await context.hledgerExecutor(["print", "-f", rulesFile]);
24994
- if (result.exitCode === 0) {
24995
- const years = extractTransactionYears(result.stdout);
24996
- if (years.size > 0) {
24997
- const transactionYear = Array.from(years)[0];
24998
- yearJournalPath = ensureYearJournalExists(context.directory, transactionYear);
24999
- break;
25000
- }
24574
+ const rulesFile = findRulesForCsv(csvPath, rulesMapping);
24575
+ if (rulesFile) {
24576
+ firstRulesFile = rulesFile;
24577
+ try {
24578
+ const result = await context.hledgerExecutor(["print", "-f", rulesFile]);
24579
+ if (result.exitCode === 0) {
24580
+ const years = extractTransactionYears(result.stdout);
24581
+ if (years.size > 0) {
24582
+ const transactionYear = Array.from(years)[0];
24583
+ yearJournalPath = ensureYearJournalExists(context.directory, transactionYear);
25001
24584
  }
25002
- } catch {
25003
- continue;
25004
24585
  }
25005
- }
24586
+ } catch {}
25006
24587
  }
25007
24588
  const suggestionContext = {
25008
24589
  existingAccounts: yearJournalPath ? await loadExistingAccounts2(yearJournalPath) : [],
@@ -25075,12 +24656,12 @@ function formatUnknownPostingsLog(postings) {
25075
24656
  `;
25076
24657
  return log;
25077
24658
  }
25078
- async function executeImportStep(context, logger) {
25079
- logger?.startSection("Step 4: Import Transactions");
24659
+ async function executeImportStep(context, contextId, logger) {
24660
+ const importContext = loadContext(context.directory, contextId);
24661
+ logger?.startSection(`Step 4: Import Transactions (${importContext.accountNumber || contextId})`);
25080
24662
  logger?.logStep("Import", "start");
25081
24663
  const importResult = await importStatements(context.directory, context.agent, {
25082
- provider: context.options.provider,
25083
- currency: context.options.currency,
24664
+ contextId,
25084
24665
  checkOnly: false
25085
24666
  }, context.configLoader, context.hledgerExecutor);
25086
24667
  const importParsed = JSON.parse(importResult);
@@ -25091,6 +24672,13 @@ async function executeImportStep(context, logger) {
25091
24672
  summary: importParsed.summary,
25092
24673
  error: importParsed.error
25093
24674
  });
24675
+ if (importParsed.success) {
24676
+ updateContext(context.directory, contextId, {
24677
+ rulesFile: importParsed.files?.[0]?.rulesFile,
24678
+ yearJournal: importParsed.files?.[0]?.yearJournal,
24679
+ transactionCount: importParsed.summary?.totalTransactions
24680
+ });
24681
+ }
25094
24682
  if (!importParsed.success) {
25095
24683
  logger?.error("Import failed", new Error(importParsed.error || "Unknown error"));
25096
24684
  logger?.endSection();
@@ -25099,12 +24687,12 @@ async function executeImportStep(context, logger) {
25099
24687
  }
25100
24688
  logger?.endSection();
25101
24689
  }
25102
- async function executeReconcileStep(context, logger) {
25103
- logger?.startSection("Step 5: Reconcile Balance");
24690
+ async function executeReconcileStep(context, contextId, logger) {
24691
+ const importContext = loadContext(context.directory, contextId);
24692
+ logger?.startSection(`Step 5: Reconcile Balance (${importContext.accountNumber || contextId})`);
25104
24693
  logger?.logStep("Reconcile", "start");
25105
24694
  const reconcileResult = await reconcileStatement(context.directory, context.agent, {
25106
- provider: context.options.provider,
25107
- currency: context.options.currency,
24695
+ contextId,
25108
24696
  closingBalance: context.options.closingBalance,
25109
24697
  account: context.options.account
25110
24698
  }, context.configLoader, context.hledgerExecutor);
@@ -25122,6 +24710,14 @@ async function executeReconcileStep(context, logger) {
25122
24710
  metadata: reconcileParsed.metadata,
25123
24711
  error: reconcileParsed.error
25124
24712
  });
24713
+ if (reconcileParsed.success) {
24714
+ updateContext(context.directory, contextId, {
24715
+ reconciledAccount: reconcileParsed.account,
24716
+ actualBalance: reconcileParsed.actualBalance,
24717
+ lastTransactionDate: reconcileParsed.lastTransactionDate,
24718
+ reconciled: true
24719
+ });
24720
+ }
25125
24721
  if (!reconcileParsed.success) {
25126
24722
  logger?.error("Reconciliation failed", new Error(reconcileParsed.error || "Balance mismatch"));
25127
24723
  logger?.endSection();
@@ -25160,21 +24756,31 @@ async function importPipeline(directory, agent, options, configLoader = loadImpo
25160
24756
  result
25161
24757
  };
25162
24758
  try {
25163
- await executeClassifyStep(context, logger);
25164
- await executeAccountDeclarationsStep(context, logger);
25165
- await executeDryRunStep(context, logger);
25166
- await executeImportStep(context, logger);
25167
- await executeReconcileStep(context, logger);
25168
- const transactionCount = context.result.steps.import?.details?.summary?.totalTransactions || 0;
24759
+ const contextIds = await executeClassifyStep(context, logger);
24760
+ if (contextIds.length === 0) {
24761
+ logger.info("No files classified, nothing to import");
24762
+ return buildSuccessResult4(result, "No files to import");
24763
+ }
24764
+ let totalTransactions = 0;
24765
+ for (const contextId of contextIds) {
24766
+ const importContext = loadContext(context.directory, contextId);
24767
+ logger.info(`Processing: ${importContext.filename} (${importContext.accountNumber || "unknown account"})`);
24768
+ await executeAccountDeclarationsStep(context, contextId, logger);
24769
+ await executeDryRunStep(context, contextId, logger);
24770
+ await executeImportStep(context, contextId, logger);
24771
+ await executeReconcileStep(context, contextId, logger);
24772
+ totalTransactions += context.result.steps.import?.details?.summary?.totalTransactions || 0;
24773
+ }
25169
24774
  logger.startSection("Summary");
25170
24775
  logger.info(`Import completed successfully`);
25171
- logger.info(`Total transactions imported: ${transactionCount}`);
24776
+ logger.info(`Contexts processed: ${contextIds.length}`);
24777
+ logger.info(`Total transactions imported: ${totalTransactions}`);
25172
24778
  if (context.result.steps.reconcile?.details?.actualBalance) {
25173
24779
  logger.info(`Balance: ${context.result.steps.reconcile.details.actualBalance}`);
25174
24780
  }
25175
24781
  logger.info(`Log file: ${logger.getLogPath()}`);
25176
24782
  logger.endSection();
25177
- return buildSuccessResult4(result, `Successfully imported ${transactionCount} transaction(s)`);
24783
+ return buildSuccessResult4(result, `Successfully imported ${totalTransactions} transaction(s) from ${contextIds.length} file(s)`);
25178
24784
  } catch (error45) {
25179
24785
  logger.error("Pipeline step failed", error45);
25180
24786
  logger.info(`Log file: ${logger.getLogPath()}`);
@@ -25361,7 +24967,7 @@ You can now drop CSV files into import/incoming/ and run import-pipeline.`);
25361
24967
  }
25362
24968
  });
25363
24969
  // src/index.ts
25364
- var __dirname2 = dirname5(fileURLToPath3(import.meta.url));
24970
+ var __dirname2 = dirname4(fileURLToPath3(import.meta.url));
25365
24971
  var AGENT_FILE = join12(__dirname2, "..", "agent", "accountant.md");
25366
24972
  var AccountantPlugin = async () => {
25367
24973
  const agent = loadAgent(AGENT_FILE);