@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/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 { existsSync, readFileSync } from "fs";
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 fs13 from "fs";
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
- async function loadExistingAccounts(yearJournalPath) {
4844
- if (!fs13.existsSync(yearJournalPath)) {
4243
+ function loadExistingAccounts(yearJournalPath) {
4244
+ if (!fs14.existsSync(yearJournalPath)) {
4845
4245
  return [];
4846
4246
  }
4847
- const content = fs13.readFileSync(yearJournalPath, "utf-8");
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
- async function extractRulePatternsFromFile(rulesPath) {
4863
- if (!fs13.existsSync(rulesPath)) {
4262
+ function extractRulePatternsFromFile(rulesPath) {
4263
+ if (!fs14.existsSync(rulesPath)) {
4864
4264
  return [];
4865
4265
  }
4866
- const content = fs13.readFileSync(rulesPath, "utf-8");
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(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
- import * as fs from "fs";
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
- 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 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
- 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 (!fs3.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 = 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
- 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 (!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 (fs2.existsSync(journalPath)) {
17515
- existingLines = fs2.readFileSync(journalPath, "utf-8").split(`
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
- fs2.writeFileSync(journalPath, sortedLines.join(`
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 = 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
+ ensureDirectory(ledgerDir);
16997
+ if (!fs4.existsSync(yearJournalPath)) {
16998
+ fs4.writeFileSync(yearJournalPath, `; ${year} transactions
17572
16999
  `);
17573
17000
  }
17574
- if (!fs2.existsSync(mainJournalPath)) {
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 = fs2.readFileSync(mainJournalPath, "utf-8");
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
- fs2.writeFileSync(mainJournalPath, newContent);
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 JSON.stringify({ error: error45 });
17083
+ return buildToolErrorResult(error45);
17633
17084
  }
17634
17085
  function buildSuccessResult(results, endDate, backfill) {
17635
- return JSON.stringify({
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 filterPriceLinesByDateRange(priceLines, startDate, endDate) {
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 = filterPriceLinesByDateRange(rawPriceLines, startDate, endDate);
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 = path3.join(directory, "ledger", "currencies", currencyConfig.file);
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 fs5 from "fs";
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
- 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}`);
17301
+ return loadYamlConfig(directory, CONFIG_FILE2, (parsedObj) => {
17302
+ if (!parsedObj.paths) {
17303
+ throw new Error("Invalid config: 'paths' section is required");
17864
17304
  }
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 };
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/fileUtils.ts
17998
- import * as fs4 from "fs";
17429
+ // src/utils/importContext.ts
17430
+ import * as fs5 from "fs";
17999
17431
  import * as path5 from "path";
18000
- function findCSVFiles(importsDir) {
18001
- if (!fs4.existsSync(importsDir)) {
18002
- return [];
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 ensureDirectory(dirPath) {
18010
- if (!fs4.existsSync(dirPath)) {
18011
- fs4.mkdirSync(dirPath, { recursive: true });
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 JSON.stringify({
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 JSON.stringify({
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 JSON.stringify({
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 = fs5.readFileSync(sourcePath, "utf-8");
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 (fs5.existsSync(targetPath)) {
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 executeMoves(plannedMoves, config2, unrecognizedDir) {
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
- fs5.renameSync(move.sourcePath, move.targetPath);
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: path6.join(config2.paths.pending, move.detection.provider, move.detection.currency, move.targetFilename)
17605
+ targetPath,
17606
+ contextId: context.id
18095
17607
  });
18096
17608
  } else {
18097
17609
  ensureDirectory(unrecognizedDir);
18098
- fs5.renameSync(move.sourcePath, move.targetPath);
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 = findCSVFiles(importsDir);
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: "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.",
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 fs9 from "fs";
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 readdirSync3, readlinkSync, realpathSync as rps } from "fs";
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: readdirSync3,
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: fs6 = defaultFS } = {}) {
21931
- this.#fs = fsFromOption(fs6);
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(fs6) {
22408
- return new PathWin32(this.rootPath, IFDIR, undefined, this.roots, this.nocase, this.childrenCache(), { fs: fs6 });
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(fs6) {
22426
- return new PathPosix(this.rootPath, IFDIR, undefined, this.roots, this.nocase, this.childrenCache(), { fs: fs6 });
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 fs6 from "fs";
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 (!fs6.existsSync(rulesDir)) {
22966
+ if (!fs7.existsSync(rulesDir)) {
23447
22967
  return mapping;
23448
22968
  }
23449
- const files = fs6.readdirSync(rulesDir);
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 = fs6.statSync(rulesFilePath);
22975
+ const stat = fs7.statSync(rulesFilePath);
23456
22976
  if (!stat.isFile()) {
23457
22977
  continue;
23458
22978
  }
23459
- const content = fs6.readFileSync(rulesFilePath, "utf-8");
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: result.stdout.toString(),
23507
- stderr: result.stderr.toString(),
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: error45 instanceof Error ? error45.message : String(error45),
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 fs7 from "fs";
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 = fs7.readFileSync(rulesFilePath, "utf-8");
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 import_convert_csv_to_json = __toESM(require_convert_csv_to_json(), 1);
23705
- import * as fs8 from "fs";
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 = fs8.readFileSync(csvPath, "utf-8");
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 headerLine = lines[headerIndex];
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 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);
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 mappedRows;
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) > 0.001)
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 JSON.stringify({
23889
- success: false,
23890
- error: error45,
23891
- hint
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 JSON.stringify({
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 JSON.stringify({
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 = fs9.readFileSync(rulesFile, "utf-8");
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 = fs9.statSync(a);
23926
- const bStat = fs9.statSync(b);
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
- if (!fs9.existsSync(destDir)) {
23980
- fs9.mkdirSync(destDir, { recursive: true });
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 = fs9.readFileSync(rulesFile, "utf-8");
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 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");
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 = fs9.statSync(a);
24114
- const bStat = fs9.statSync(b);
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
- 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.'),
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
- provider: params.provider,
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 fs10 from "fs";
23759
+ import * as fs11 from "fs";
24222
23760
  import * as path10 from "path";
24223
23761
  function buildErrorResult4(params) {
24224
- return JSON.stringify({
24225
- success: false,
24226
- ...params
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 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}` : "";
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: `No CSV files found in ${doneDir}`,
24250
- hint: `Run: import-statements${providerFilter}${currencyFilter}`
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
- const csvFile = csvFiles[csvFiles.length - 1];
24255
- const relativePath = path10.relative(path10.dirname(path10.dirname(doneDir)), csvFile);
24256
- return { csvFile, relativePath };
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 determineClosingBalance(csvFile, config2, options, relativeCsvPath, rulesDir) {
24259
- let metadata;
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 = fs10.readFileSync(csvFile, "utf-8");
23825
+ const content = fs11.readFileSync(csvFile, "utf-8");
24262
23826
  const filename = path10.basename(csvFile);
24263
23827
  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 };
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
- 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 };
23839
+ } catch {}
23840
+ return;
24296
23841
  }
24297
- function buildRetryCommand(options, closingBalance, account) {
24298
- const parts = ["import-pipeline"];
24299
- if (options.provider) {
24300
- parts.push(`--provider ${options.provider}`);
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
- if (options.currency) {
24303
- parts.push(`--currency ${options.currency}`);
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, options, relativeCsvPath, metadata) {
24314
- let account = options.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 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:...")}`;
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 = fs10.readFileSync(rulesFile, "utf-8");
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 = findCsvToReconcile(doneDir, options);
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. Finds the most recently imported CSV in the done directory
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: Extracted from CSV header metadata (e.g., UBS files have "Closing balance:" row)
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
- 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")'),
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
- provider: params.provider,
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 fs11 from "fs";
24111
+ import * as fs12 from "fs";
24545
24112
  function extractAccountsFromRulesFile(rulesPath) {
24546
24113
  const accounts = new Set;
24547
- if (!fs11.existsSync(rulesPath)) {
24114
+ if (!fs12.existsSync(rulesPath)) {
24548
24115
  return accounts;
24549
24116
  }
24550
- const content = fs11.readFileSync(rulesPath, "utf-8");
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 (!fs11.existsSync(yearJournalPath)) {
24152
+ if (!fs12.existsSync(yearJournalPath)) {
24586
24153
  throw new Error(`Year journal not found: ${yearJournalPath}`);
24587
24154
  }
24588
- const content = fs11.readFileSync(yearJournalPath, "utf-8");
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
- fs11.writeFileSync(yearJournalPath, newContent.join(`
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 fs12 from "fs/promises";
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 > 50) {
24743
- this.buffer.push(...lines.slice(0, 50));
24744
- this.buffer.push(`... (${lines.length - 50} more lines omitted)`);
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 fs12.mkdir(path11.dirname(this.logPath), { recursive: true });
24770
- await fs12.writeFile(this.logPath, this.buffer.join(`
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 buildSuccessResult4(result, summary) {
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 buildErrorResult5(result, error45, hint) {
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 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
- }
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
- provider: context.options.provider,
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 csvFiles = findCsvFiles(pendingDir, context.options.provider, context.options.currency);
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
- 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
- }
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 ? await loadExistingAccounts2(yearJournalPath) : [],
24581
+ existingAccounts: yearJournalPath ? loadExistingAccounts2(yearJournalPath) : [],
25009
24582
  rulesFilePath: firstRulesFile,
25010
- existingRules: firstRulesFile ? await extractRulePatternsFromFile2(firstRulesFile) : undefined,
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
- logger?.startSection("Step 4: Import Transactions");
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
- provider: context.options.provider,
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
- logger?.startSection("Step 5: Reconcile Balance");
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
- provider: context.options.provider,
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 buildSuccessResult4(result, "No transactions found to import");
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
- 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;
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(`Total transactions imported: ${transactionCount}`);
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 buildSuccessResult4(result, `Successfully imported ${transactionCount} transaction(s)`);
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 buildErrorResult5(result, result.error, result.hint);
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 fs14 from "fs";
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 (!fs14.existsSync(importBase)) {
25247
- fs14.mkdirSync(importBase, { recursive: true });
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 (!fs14.existsSync(fullPath)) {
25259
- fs14.mkdirSync(fullPath, { recursive: true });
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 (!fs14.existsSync(gitkeepPath)) {
25264
- fs14.writeFileSync(gitkeepPath, "");
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 (!fs14.existsSync(gitignorePath)) {
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
- fs14.writeFileSync(gitignorePath, gitignoreContent);
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 = dirname5(fileURLToPath3(import.meta.url));
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);