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