@fuzzle/opencode-accountant 0.4.6 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -9
- package/agent/accountant.md +27 -87
- package/dist/index.js +502 -896
- package/docs/architecture/import-context.md +674 -0
- package/docs/tools/classify-statements.md +84 -7
- package/docs/tools/import-pipeline.md +611 -0
- package/docs/tools/import-statements.md +43 -5
- package/docs/tools/reconcile-statement.md +529 -0
- package/package.json +3 -4
package/dist/index.js
CHANGED
|
@@ -4222,606 +4222,6 @@ var require_brace_expansion = __commonJS((exports, module) => {
|
|
|
4222
4222
|
}
|
|
4223
4223
|
});
|
|
4224
4224
|
|
|
4225
|
-
// node_modules/convert-csv-to-json/src/util/fileUtils.js
|
|
4226
|
-
var require_fileUtils = __commonJS((exports, module) => {
|
|
4227
|
-
var fs8 = __require("fs");
|
|
4228
|
-
|
|
4229
|
-
class FileUtils {
|
|
4230
|
-
readFile(fileInputName, encoding) {
|
|
4231
|
-
return fs8.readFileSync(fileInputName, encoding).toString();
|
|
4232
|
-
}
|
|
4233
|
-
readFileAsync(fileInputName, encoding = "utf8") {
|
|
4234
|
-
if (fs8.promises && typeof fs8.promises.readFile === "function") {
|
|
4235
|
-
return fs8.promises.readFile(fileInputName, encoding).then((buf) => buf.toString());
|
|
4236
|
-
}
|
|
4237
|
-
return new Promise((resolve2, reject) => {
|
|
4238
|
-
fs8.readFile(fileInputName, encoding, (err, data) => {
|
|
4239
|
-
if (err) {
|
|
4240
|
-
reject(err);
|
|
4241
|
-
return;
|
|
4242
|
-
}
|
|
4243
|
-
resolve2(data.toString());
|
|
4244
|
-
});
|
|
4245
|
-
});
|
|
4246
|
-
}
|
|
4247
|
-
writeFile(json3, fileOutputName) {
|
|
4248
|
-
fs8.writeFile(fileOutputName, json3, function(err) {
|
|
4249
|
-
if (err) {
|
|
4250
|
-
throw err;
|
|
4251
|
-
} else {
|
|
4252
|
-
console.log("File saved: " + fileOutputName);
|
|
4253
|
-
}
|
|
4254
|
-
});
|
|
4255
|
-
}
|
|
4256
|
-
writeFileAsync(json3, fileOutputName) {
|
|
4257
|
-
if (fs8.promises && typeof fs8.promises.writeFile === "function") {
|
|
4258
|
-
return fs8.promises.writeFile(fileOutputName, json3);
|
|
4259
|
-
}
|
|
4260
|
-
return new Promise((resolve2, reject) => {
|
|
4261
|
-
fs8.writeFile(fileOutputName, json3, (err) => {
|
|
4262
|
-
if (err)
|
|
4263
|
-
return reject(err);
|
|
4264
|
-
resolve2();
|
|
4265
|
-
});
|
|
4266
|
-
});
|
|
4267
|
-
}
|
|
4268
|
-
}
|
|
4269
|
-
module.exports = new FileUtils;
|
|
4270
|
-
});
|
|
4271
|
-
|
|
4272
|
-
// node_modules/convert-csv-to-json/src/util/stringUtils.js
|
|
4273
|
-
var require_stringUtils = __commonJS((exports, module) => {
|
|
4274
|
-
class StringUtils {
|
|
4275
|
-
static PATTERNS = {
|
|
4276
|
-
INTEGER: /^-?\d+$/,
|
|
4277
|
-
FLOAT: /^-?\d*\.\d+$/,
|
|
4278
|
-
WHITESPACE: /\s/g
|
|
4279
|
-
};
|
|
4280
|
-
static BOOLEAN_VALUES = {
|
|
4281
|
-
TRUE: "true",
|
|
4282
|
-
FALSE: "false"
|
|
4283
|
-
};
|
|
4284
|
-
trimPropertyName(shouldTrimAll, propertyName) {
|
|
4285
|
-
if (!propertyName) {
|
|
4286
|
-
return "";
|
|
4287
|
-
}
|
|
4288
|
-
return shouldTrimAll ? propertyName.replace(StringUtils.PATTERNS.WHITESPACE, "") : propertyName.trim();
|
|
4289
|
-
}
|
|
4290
|
-
getValueFormatByType(value) {
|
|
4291
|
-
if (this.isEmpty(value)) {
|
|
4292
|
-
return String();
|
|
4293
|
-
}
|
|
4294
|
-
if (this.isBoolean(value)) {
|
|
4295
|
-
return this.convertToBoolean(value);
|
|
4296
|
-
}
|
|
4297
|
-
if (this.isInteger(value)) {
|
|
4298
|
-
return this.convertInteger(value);
|
|
4299
|
-
}
|
|
4300
|
-
if (this.isFloat(value)) {
|
|
4301
|
-
return this.convertFloat(value);
|
|
4302
|
-
}
|
|
4303
|
-
return String(value);
|
|
4304
|
-
}
|
|
4305
|
-
hasContent(values = []) {
|
|
4306
|
-
return Array.isArray(values) && values.some((value) => Boolean(value));
|
|
4307
|
-
}
|
|
4308
|
-
isEmpty(value) {
|
|
4309
|
-
return value === undefined || value === "";
|
|
4310
|
-
}
|
|
4311
|
-
isBoolean(value) {
|
|
4312
|
-
const normalizedValue = value.toLowerCase();
|
|
4313
|
-
return normalizedValue === StringUtils.BOOLEAN_VALUES.TRUE || normalizedValue === StringUtils.BOOLEAN_VALUES.FALSE;
|
|
4314
|
-
}
|
|
4315
|
-
isInteger(value) {
|
|
4316
|
-
return StringUtils.PATTERNS.INTEGER.test(value);
|
|
4317
|
-
}
|
|
4318
|
-
isFloat(value) {
|
|
4319
|
-
return StringUtils.PATTERNS.FLOAT.test(value);
|
|
4320
|
-
}
|
|
4321
|
-
hasLeadingZero(value) {
|
|
4322
|
-
const isPositiveWithLeadingZero = value.length > 1 && value[0] === "0";
|
|
4323
|
-
const isNegativeWithLeadingZero = value.length > 2 && value[0] === "-" && value[1] === "0";
|
|
4324
|
-
return isPositiveWithLeadingZero || isNegativeWithLeadingZero;
|
|
4325
|
-
}
|
|
4326
|
-
convertToBoolean(value) {
|
|
4327
|
-
return JSON.parse(value.toLowerCase());
|
|
4328
|
-
}
|
|
4329
|
-
convertInteger(value) {
|
|
4330
|
-
if (this.hasLeadingZero(value)) {
|
|
4331
|
-
return String(value);
|
|
4332
|
-
}
|
|
4333
|
-
const num = Number(value);
|
|
4334
|
-
return Number.isSafeInteger(num) ? num : String(value);
|
|
4335
|
-
}
|
|
4336
|
-
convertFloat(value) {
|
|
4337
|
-
const num = Number(value);
|
|
4338
|
-
return Number.isFinite(num) ? num : String(value);
|
|
4339
|
-
}
|
|
4340
|
-
}
|
|
4341
|
-
module.exports = new StringUtils;
|
|
4342
|
-
});
|
|
4343
|
-
|
|
4344
|
-
// node_modules/convert-csv-to-json/src/util/jsonUtils.js
|
|
4345
|
-
var require_jsonUtils = __commonJS((exports, module) => {
|
|
4346
|
-
class JsonUtil {
|
|
4347
|
-
validateJson(json3) {
|
|
4348
|
-
try {
|
|
4349
|
-
JSON.parse(json3);
|
|
4350
|
-
} catch (err) {
|
|
4351
|
-
throw Error(`Parsed csv has generated an invalid json!!!
|
|
4352
|
-
` + err);
|
|
4353
|
-
}
|
|
4354
|
-
}
|
|
4355
|
-
}
|
|
4356
|
-
module.exports = new JsonUtil;
|
|
4357
|
-
});
|
|
4358
|
-
|
|
4359
|
-
// node_modules/convert-csv-to-json/src/csvToJson.js
|
|
4360
|
-
var require_csvToJson = __commonJS((exports, module) => {
|
|
4361
|
-
var fileUtils = require_fileUtils();
|
|
4362
|
-
var stringUtils = require_stringUtils();
|
|
4363
|
-
var jsonUtils = require_jsonUtils();
|
|
4364
|
-
var newLine = /\r?\n/;
|
|
4365
|
-
var defaultFieldDelimiter = ";";
|
|
4366
|
-
|
|
4367
|
-
class CsvToJson {
|
|
4368
|
-
formatValueByType(active) {
|
|
4369
|
-
this.printValueFormatByType = active;
|
|
4370
|
-
return this;
|
|
4371
|
-
}
|
|
4372
|
-
supportQuotedField(active) {
|
|
4373
|
-
this.isSupportQuotedField = active;
|
|
4374
|
-
return this;
|
|
4375
|
-
}
|
|
4376
|
-
fieldDelimiter(delimiter) {
|
|
4377
|
-
this.delimiter = delimiter;
|
|
4378
|
-
return this;
|
|
4379
|
-
}
|
|
4380
|
-
trimHeaderFieldWhiteSpace(active) {
|
|
4381
|
-
this.isTrimHeaderFieldWhiteSpace = active;
|
|
4382
|
-
return this;
|
|
4383
|
-
}
|
|
4384
|
-
indexHeader(indexHeaderValue) {
|
|
4385
|
-
if (isNaN(indexHeaderValue)) {
|
|
4386
|
-
throw new Error("The index Header must be a Number!");
|
|
4387
|
-
}
|
|
4388
|
-
this.indexHeaderValue = indexHeaderValue;
|
|
4389
|
-
return this;
|
|
4390
|
-
}
|
|
4391
|
-
parseSubArray(delimiter = "*", separator = ",") {
|
|
4392
|
-
this.parseSubArrayDelimiter = delimiter;
|
|
4393
|
-
this.parseSubArraySeparator = separator;
|
|
4394
|
-
}
|
|
4395
|
-
encoding(encoding) {
|
|
4396
|
-
this.encoding = encoding;
|
|
4397
|
-
return this;
|
|
4398
|
-
}
|
|
4399
|
-
generateJsonFileFromCsv(fileInputName, fileOutputName) {
|
|
4400
|
-
let jsonStringified = this.getJsonFromCsvStringified(fileInputName);
|
|
4401
|
-
fileUtils.writeFile(jsonStringified, fileOutputName);
|
|
4402
|
-
}
|
|
4403
|
-
getJsonFromCsvStringified(fileInputName) {
|
|
4404
|
-
let json3 = this.getJsonFromCsv(fileInputName);
|
|
4405
|
-
let jsonStringified = JSON.stringify(json3, undefined, 1);
|
|
4406
|
-
jsonUtils.validateJson(jsonStringified);
|
|
4407
|
-
return jsonStringified;
|
|
4408
|
-
}
|
|
4409
|
-
getJsonFromCsv(fileInputName) {
|
|
4410
|
-
let parsedCsv = fileUtils.readFile(fileInputName, this.encoding);
|
|
4411
|
-
return this.csvToJson(parsedCsv);
|
|
4412
|
-
}
|
|
4413
|
-
csvStringToJson(csvString) {
|
|
4414
|
-
return this.csvToJson(csvString);
|
|
4415
|
-
}
|
|
4416
|
-
csvStringToJsonStringified(csvString) {
|
|
4417
|
-
let json3 = this.csvStringToJson(csvString);
|
|
4418
|
-
let jsonStringified = JSON.stringify(json3, undefined, 1);
|
|
4419
|
-
jsonUtils.validateJson(jsonStringified);
|
|
4420
|
-
return jsonStringified;
|
|
4421
|
-
}
|
|
4422
|
-
csvToJson(parsedCsv) {
|
|
4423
|
-
this.validateInputConfig();
|
|
4424
|
-
let lines = parsedCsv.split(newLine);
|
|
4425
|
-
let fieldDelimiter = this.getFieldDelimiter();
|
|
4426
|
-
let index = this.getIndexHeader();
|
|
4427
|
-
let headers;
|
|
4428
|
-
if (this.isSupportQuotedField) {
|
|
4429
|
-
headers = this.split(lines[index]);
|
|
4430
|
-
} else {
|
|
4431
|
-
headers = lines[index].split(fieldDelimiter);
|
|
4432
|
-
}
|
|
4433
|
-
while (!stringUtils.hasContent(headers) && index <= lines.length) {
|
|
4434
|
-
index = index + 1;
|
|
4435
|
-
headers = lines[index].split(fieldDelimiter);
|
|
4436
|
-
}
|
|
4437
|
-
let jsonResult = [];
|
|
4438
|
-
for (let i2 = index + 1;i2 < lines.length; i2++) {
|
|
4439
|
-
let currentLine;
|
|
4440
|
-
if (this.isSupportQuotedField) {
|
|
4441
|
-
currentLine = this.split(lines[i2]);
|
|
4442
|
-
} else {
|
|
4443
|
-
currentLine = lines[i2].split(fieldDelimiter);
|
|
4444
|
-
}
|
|
4445
|
-
if (stringUtils.hasContent(currentLine)) {
|
|
4446
|
-
jsonResult.push(this.buildJsonResult(headers, currentLine));
|
|
4447
|
-
}
|
|
4448
|
-
}
|
|
4449
|
-
return jsonResult;
|
|
4450
|
-
}
|
|
4451
|
-
getFieldDelimiter() {
|
|
4452
|
-
if (this.delimiter) {
|
|
4453
|
-
return this.delimiter;
|
|
4454
|
-
}
|
|
4455
|
-
return defaultFieldDelimiter;
|
|
4456
|
-
}
|
|
4457
|
-
getIndexHeader() {
|
|
4458
|
-
if (this.indexHeaderValue !== null && !isNaN(this.indexHeaderValue)) {
|
|
4459
|
-
return this.indexHeaderValue;
|
|
4460
|
-
}
|
|
4461
|
-
return 0;
|
|
4462
|
-
}
|
|
4463
|
-
buildJsonResult(headers, currentLine) {
|
|
4464
|
-
let jsonObject = {};
|
|
4465
|
-
for (let j = 0;j < headers.length; j++) {
|
|
4466
|
-
let propertyName = stringUtils.trimPropertyName(this.isTrimHeaderFieldWhiteSpace, headers[j]);
|
|
4467
|
-
let value = currentLine[j];
|
|
4468
|
-
if (this.isParseSubArray(value)) {
|
|
4469
|
-
value = this.buildJsonSubArray(value);
|
|
4470
|
-
}
|
|
4471
|
-
if (this.printValueFormatByType && !Array.isArray(value)) {
|
|
4472
|
-
value = stringUtils.getValueFormatByType(currentLine[j]);
|
|
4473
|
-
}
|
|
4474
|
-
jsonObject[propertyName] = value;
|
|
4475
|
-
}
|
|
4476
|
-
return jsonObject;
|
|
4477
|
-
}
|
|
4478
|
-
buildJsonSubArray(value) {
|
|
4479
|
-
let extractedValues = value.substring(value.indexOf(this.parseSubArrayDelimiter) + 1, value.lastIndexOf(this.parseSubArrayDelimiter));
|
|
4480
|
-
extractedValues.trim();
|
|
4481
|
-
value = extractedValues.split(this.parseSubArraySeparator);
|
|
4482
|
-
if (this.printValueFormatByType) {
|
|
4483
|
-
for (let i2 = 0;i2 < value.length; i2++) {
|
|
4484
|
-
value[i2] = stringUtils.getValueFormatByType(value[i2]);
|
|
4485
|
-
}
|
|
4486
|
-
}
|
|
4487
|
-
return value;
|
|
4488
|
-
}
|
|
4489
|
-
isParseSubArray(value) {
|
|
4490
|
-
if (this.parseSubArrayDelimiter) {
|
|
4491
|
-
if (value && (value.indexOf(this.parseSubArrayDelimiter) === 0 && value.lastIndexOf(this.parseSubArrayDelimiter) === value.length - 1)) {
|
|
4492
|
-
return true;
|
|
4493
|
-
}
|
|
4494
|
-
}
|
|
4495
|
-
return false;
|
|
4496
|
-
}
|
|
4497
|
-
validateInputConfig() {
|
|
4498
|
-
if (this.isSupportQuotedField) {
|
|
4499
|
-
if (this.getFieldDelimiter() === '"') {
|
|
4500
|
-
throw new Error('When SupportQuotedFields is enabled you cannot defined the field delimiter as quote -> ["]');
|
|
4501
|
-
}
|
|
4502
|
-
if (this.parseSubArraySeparator === '"') {
|
|
4503
|
-
throw new Error('When SupportQuotedFields is enabled you cannot defined the field parseSubArraySeparator as quote -> ["]');
|
|
4504
|
-
}
|
|
4505
|
-
if (this.parseSubArrayDelimiter === '"') {
|
|
4506
|
-
throw new Error('When SupportQuotedFields is enabled you cannot defined the field parseSubArrayDelimiter as quote -> ["]');
|
|
4507
|
-
}
|
|
4508
|
-
}
|
|
4509
|
-
}
|
|
4510
|
-
hasQuotes(line) {
|
|
4511
|
-
return line.includes('"');
|
|
4512
|
-
}
|
|
4513
|
-
split(line) {
|
|
4514
|
-
if (line.length == 0) {
|
|
4515
|
-
return [];
|
|
4516
|
-
}
|
|
4517
|
-
let delim = this.getFieldDelimiter();
|
|
4518
|
-
let subSplits = [""];
|
|
4519
|
-
if (this.hasQuotes(line)) {
|
|
4520
|
-
let chars = line.split("");
|
|
4521
|
-
let subIndex = 0;
|
|
4522
|
-
let startQuote = false;
|
|
4523
|
-
let isDouble = false;
|
|
4524
|
-
chars.forEach((c, i2, arr) => {
|
|
4525
|
-
if (isDouble) {
|
|
4526
|
-
subSplits[subIndex] += c;
|
|
4527
|
-
isDouble = false;
|
|
4528
|
-
return;
|
|
4529
|
-
}
|
|
4530
|
-
if (c != '"' && c != delim) {
|
|
4531
|
-
subSplits[subIndex] += c;
|
|
4532
|
-
} else if (c == delim && startQuote) {
|
|
4533
|
-
subSplits[subIndex] += c;
|
|
4534
|
-
} else if (c == delim) {
|
|
4535
|
-
subIndex++;
|
|
4536
|
-
subSplits[subIndex] = "";
|
|
4537
|
-
return;
|
|
4538
|
-
} else {
|
|
4539
|
-
if (arr[i2 + 1] === '"') {
|
|
4540
|
-
isDouble = true;
|
|
4541
|
-
} else {
|
|
4542
|
-
if (!startQuote) {
|
|
4543
|
-
startQuote = true;
|
|
4544
|
-
} else {
|
|
4545
|
-
startQuote = false;
|
|
4546
|
-
}
|
|
4547
|
-
}
|
|
4548
|
-
}
|
|
4549
|
-
});
|
|
4550
|
-
if (startQuote) {
|
|
4551
|
-
throw new Error("Row contains mismatched quotes!");
|
|
4552
|
-
}
|
|
4553
|
-
return subSplits;
|
|
4554
|
-
} else {
|
|
4555
|
-
return line.split(delim);
|
|
4556
|
-
}
|
|
4557
|
-
}
|
|
4558
|
-
}
|
|
4559
|
-
module.exports = new CsvToJson;
|
|
4560
|
-
});
|
|
4561
|
-
|
|
4562
|
-
// node_modules/convert-csv-to-json/src/csvToJsonAsync.js
|
|
4563
|
-
var require_csvToJsonAsync = __commonJS((exports, module) => {
|
|
4564
|
-
var fileUtils = require_fileUtils();
|
|
4565
|
-
var csvToJson = require_csvToJson();
|
|
4566
|
-
|
|
4567
|
-
class CsvToJsonAsync {
|
|
4568
|
-
constructor() {
|
|
4569
|
-
this.csvToJson = csvToJson;
|
|
4570
|
-
}
|
|
4571
|
-
formatValueByType(active) {
|
|
4572
|
-
this.csvToJson.formatValueByType(active);
|
|
4573
|
-
return this;
|
|
4574
|
-
}
|
|
4575
|
-
supportQuotedField(active) {
|
|
4576
|
-
this.csvToJson.supportQuotedField(active);
|
|
4577
|
-
return this;
|
|
4578
|
-
}
|
|
4579
|
-
fieldDelimiter(delimiter) {
|
|
4580
|
-
this.csvToJson.fieldDelimiter(delimiter);
|
|
4581
|
-
return this;
|
|
4582
|
-
}
|
|
4583
|
-
trimHeaderFieldWhiteSpace(active) {
|
|
4584
|
-
this.csvToJson.trimHeaderFieldWhiteSpace(active);
|
|
4585
|
-
return this;
|
|
4586
|
-
}
|
|
4587
|
-
indexHeader(indexHeader) {
|
|
4588
|
-
this.csvToJson.indexHeader(indexHeader);
|
|
4589
|
-
return this;
|
|
4590
|
-
}
|
|
4591
|
-
parseSubArray(delimiter = "*", separator = ",") {
|
|
4592
|
-
this.csvToJson.parseSubArray(delimiter, separator);
|
|
4593
|
-
return this;
|
|
4594
|
-
}
|
|
4595
|
-
encoding(encoding) {
|
|
4596
|
-
this.csvToJson.encoding = encoding;
|
|
4597
|
-
return this;
|
|
4598
|
-
}
|
|
4599
|
-
async generateJsonFileFromCsv(fileInputName, fileOutputName) {
|
|
4600
|
-
const jsonStringified = await this.getJsonFromCsvStringified(fileInputName);
|
|
4601
|
-
await fileUtils.writeFileAsync(jsonStringified, fileOutputName);
|
|
4602
|
-
}
|
|
4603
|
-
async getJsonFromCsvStringified(fileInputName) {
|
|
4604
|
-
const json3 = await this.getJsonFromCsvAsync(fileInputName);
|
|
4605
|
-
return JSON.stringify(json3, undefined, 1);
|
|
4606
|
-
}
|
|
4607
|
-
async getJsonFromCsvAsync(inputFileNameOrCsv, options = {}) {
|
|
4608
|
-
if (inputFileNameOrCsv === null || inputFileNameOrCsv === undefined) {
|
|
4609
|
-
throw new Error("inputFileNameOrCsv is not defined!!!");
|
|
4610
|
-
}
|
|
4611
|
-
if (options.raw) {
|
|
4612
|
-
if (inputFileNameOrCsv === "") {
|
|
4613
|
-
return [];
|
|
4614
|
-
}
|
|
4615
|
-
return this.csvToJson.csvToJson(inputFileNameOrCsv);
|
|
4616
|
-
}
|
|
4617
|
-
const parsedCsv = await fileUtils.readFileAsync(inputFileNameOrCsv, this.csvToJson.encoding || "utf8");
|
|
4618
|
-
return this.csvToJson.csvToJson(parsedCsv);
|
|
4619
|
-
}
|
|
4620
|
-
csvStringToJsonAsync(csvString, options = { raw: true }) {
|
|
4621
|
-
return this.getJsonFromCsvAsync(csvString, options);
|
|
4622
|
-
}
|
|
4623
|
-
async csvStringToJsonStringifiedAsync(csvString) {
|
|
4624
|
-
const json3 = await this.csvStringToJsonAsync(csvString);
|
|
4625
|
-
return JSON.stringify(json3, undefined, 1);
|
|
4626
|
-
}
|
|
4627
|
-
}
|
|
4628
|
-
module.exports = new CsvToJsonAsync;
|
|
4629
|
-
});
|
|
4630
|
-
|
|
4631
|
-
// node_modules/convert-csv-to-json/src/browserApi.js
|
|
4632
|
-
var require_browserApi = __commonJS((exports, module) => {
|
|
4633
|
-
var csvToJson = require_csvToJson();
|
|
4634
|
-
|
|
4635
|
-
class BrowserApi {
|
|
4636
|
-
constructor() {
|
|
4637
|
-
this.csvToJson = csvToJson;
|
|
4638
|
-
}
|
|
4639
|
-
formatValueByType(active = true) {
|
|
4640
|
-
this.csvToJson.formatValueByType(active);
|
|
4641
|
-
return this;
|
|
4642
|
-
}
|
|
4643
|
-
supportQuotedField(active = false) {
|
|
4644
|
-
this.csvToJson.supportQuotedField(active);
|
|
4645
|
-
return this;
|
|
4646
|
-
}
|
|
4647
|
-
fieldDelimiter(delimiter) {
|
|
4648
|
-
this.csvToJson.fieldDelimiter(delimiter);
|
|
4649
|
-
return this;
|
|
4650
|
-
}
|
|
4651
|
-
trimHeaderFieldWhiteSpace(active = false) {
|
|
4652
|
-
this.csvToJson.trimHeaderFieldWhiteSpace(active);
|
|
4653
|
-
return this;
|
|
4654
|
-
}
|
|
4655
|
-
indexHeader(index) {
|
|
4656
|
-
this.csvToJson.indexHeader(index);
|
|
4657
|
-
return this;
|
|
4658
|
-
}
|
|
4659
|
-
parseSubArray(delimiter = "*", separator = ",") {
|
|
4660
|
-
this.csvToJson.parseSubArray(delimiter, separator);
|
|
4661
|
-
return this;
|
|
4662
|
-
}
|
|
4663
|
-
csvStringToJson(csvString) {
|
|
4664
|
-
if (csvString === undefined || csvString === null) {
|
|
4665
|
-
throw new Error("csvString is not defined!!!");
|
|
4666
|
-
}
|
|
4667
|
-
return this.csvToJson.csvToJson(csvString);
|
|
4668
|
-
}
|
|
4669
|
-
csvStringToJsonStringified(csvString) {
|
|
4670
|
-
if (csvString === undefined || csvString === null) {
|
|
4671
|
-
throw new Error("csvString is not defined!!!");
|
|
4672
|
-
}
|
|
4673
|
-
return this.csvToJson.csvStringToJsonStringified(csvString);
|
|
4674
|
-
}
|
|
4675
|
-
csvStringToJsonAsync(csvString) {
|
|
4676
|
-
return Promise.resolve(this.csvStringToJson(csvString));
|
|
4677
|
-
}
|
|
4678
|
-
csvStringToJsonStringifiedAsync(csvString) {
|
|
4679
|
-
return Promise.resolve(this.csvStringToJsonStringified(csvString));
|
|
4680
|
-
}
|
|
4681
|
-
parseFile(file2, options = {}) {
|
|
4682
|
-
if (!file2) {
|
|
4683
|
-
return Promise.reject(new Error("file is not defined!!!"));
|
|
4684
|
-
}
|
|
4685
|
-
return new Promise((resolve2, reject) => {
|
|
4686
|
-
if (typeof FileReader === "undefined") {
|
|
4687
|
-
reject(new Error("FileReader is not available in this environment"));
|
|
4688
|
-
return;
|
|
4689
|
-
}
|
|
4690
|
-
const reader = new FileReader;
|
|
4691
|
-
reader.onerror = () => reject(reader.error || new Error("Failed to read file"));
|
|
4692
|
-
reader.onload = () => {
|
|
4693
|
-
try {
|
|
4694
|
-
const text = reader.result;
|
|
4695
|
-
const result = this.csvToJson.csvToJson(String(text));
|
|
4696
|
-
resolve2(result);
|
|
4697
|
-
} catch (err) {
|
|
4698
|
-
reject(err);
|
|
4699
|
-
}
|
|
4700
|
-
};
|
|
4701
|
-
if (options.encoding) {
|
|
4702
|
-
reader.readAsText(file2, options.encoding);
|
|
4703
|
-
} else {
|
|
4704
|
-
reader.readAsText(file2);
|
|
4705
|
-
}
|
|
4706
|
-
});
|
|
4707
|
-
}
|
|
4708
|
-
}
|
|
4709
|
-
module.exports = new BrowserApi;
|
|
4710
|
-
});
|
|
4711
|
-
|
|
4712
|
-
// node_modules/convert-csv-to-json/index.js
|
|
4713
|
-
var require_convert_csv_to_json = __commonJS((exports) => {
|
|
4714
|
-
var csvToJson = require_csvToJson();
|
|
4715
|
-
var encodingOps = {
|
|
4716
|
-
utf8: "utf8",
|
|
4717
|
-
ucs2: "ucs2",
|
|
4718
|
-
utf16le: "utf16le",
|
|
4719
|
-
latin1: "latin1",
|
|
4720
|
-
ascii: "ascii",
|
|
4721
|
-
base64: "base64",
|
|
4722
|
-
hex: "hex"
|
|
4723
|
-
};
|
|
4724
|
-
exports.formatValueByType = function(active = true) {
|
|
4725
|
-
csvToJson.formatValueByType(active);
|
|
4726
|
-
return this;
|
|
4727
|
-
};
|
|
4728
|
-
exports.supportQuotedField = function(active = false) {
|
|
4729
|
-
csvToJson.supportQuotedField(active);
|
|
4730
|
-
return this;
|
|
4731
|
-
};
|
|
4732
|
-
exports.fieldDelimiter = function(delimiter) {
|
|
4733
|
-
csvToJson.fieldDelimiter(delimiter);
|
|
4734
|
-
return this;
|
|
4735
|
-
};
|
|
4736
|
-
exports.trimHeaderFieldWhiteSpace = function(active = false) {
|
|
4737
|
-
csvToJson.trimHeaderFieldWhiteSpace(active);
|
|
4738
|
-
return this;
|
|
4739
|
-
};
|
|
4740
|
-
exports.indexHeader = function(index) {
|
|
4741
|
-
csvToJson.indexHeader(index);
|
|
4742
|
-
return this;
|
|
4743
|
-
};
|
|
4744
|
-
exports.parseSubArray = function(delimiter, separator) {
|
|
4745
|
-
csvToJson.parseSubArray(delimiter, separator);
|
|
4746
|
-
return this;
|
|
4747
|
-
};
|
|
4748
|
-
exports.customEncoding = function(encoding) {
|
|
4749
|
-
csvToJson.encoding = encoding;
|
|
4750
|
-
return this;
|
|
4751
|
-
};
|
|
4752
|
-
exports.utf8Encoding = function utf8Encoding() {
|
|
4753
|
-
csvToJson.encoding = encodingOps.utf8;
|
|
4754
|
-
return this;
|
|
4755
|
-
};
|
|
4756
|
-
exports.ucs2Encoding = function() {
|
|
4757
|
-
csvToJson.encoding = encodingOps.ucs2;
|
|
4758
|
-
return this;
|
|
4759
|
-
};
|
|
4760
|
-
exports.utf16leEncoding = function() {
|
|
4761
|
-
csvToJson.encoding = encodingOps.utf16le;
|
|
4762
|
-
return this;
|
|
4763
|
-
};
|
|
4764
|
-
exports.latin1Encoding = function() {
|
|
4765
|
-
csvToJson.encoding = encodingOps.latin1;
|
|
4766
|
-
return this;
|
|
4767
|
-
};
|
|
4768
|
-
exports.asciiEncoding = function() {
|
|
4769
|
-
csvToJson.encoding = encodingOps.ascii;
|
|
4770
|
-
return this;
|
|
4771
|
-
};
|
|
4772
|
-
exports.base64Encoding = function() {
|
|
4773
|
-
this.csvToJson = encodingOps.base64;
|
|
4774
|
-
return this;
|
|
4775
|
-
};
|
|
4776
|
-
exports.hexEncoding = function() {
|
|
4777
|
-
this.csvToJson = encodingOps.hex;
|
|
4778
|
-
return this;
|
|
4779
|
-
};
|
|
4780
|
-
exports.generateJsonFileFromCsv = function(inputFileName, outputFileName) {
|
|
4781
|
-
if (!inputFileName) {
|
|
4782
|
-
throw new Error("inputFileName is not defined!!!");
|
|
4783
|
-
}
|
|
4784
|
-
if (!outputFileName) {
|
|
4785
|
-
throw new Error("outputFileName is not defined!!!");
|
|
4786
|
-
}
|
|
4787
|
-
csvToJson.generateJsonFileFromCsv(inputFileName, outputFileName);
|
|
4788
|
-
};
|
|
4789
|
-
exports.getJsonFromCsv = function(inputFileName) {
|
|
4790
|
-
if (!inputFileName) {
|
|
4791
|
-
throw new Error("inputFileName is not defined!!!");
|
|
4792
|
-
}
|
|
4793
|
-
return csvToJson.getJsonFromCsv(inputFileName);
|
|
4794
|
-
};
|
|
4795
|
-
var csvToJsonAsync = require_csvToJsonAsync();
|
|
4796
|
-
Object.assign(exports, {
|
|
4797
|
-
getJsonFromCsvAsync: function(input, options) {
|
|
4798
|
-
return csvToJsonAsync.getJsonFromCsvAsync(input, options);
|
|
4799
|
-
},
|
|
4800
|
-
csvStringToJsonAsync: function(input, options) {
|
|
4801
|
-
return csvToJsonAsync.csvStringToJsonAsync(input, options);
|
|
4802
|
-
},
|
|
4803
|
-
csvStringToJsonStringifiedAsync: function(input) {
|
|
4804
|
-
return csvToJsonAsync.csvStringToJsonStringifiedAsync(input);
|
|
4805
|
-
},
|
|
4806
|
-
generateJsonFileFromCsvAsync: function(input, output) {
|
|
4807
|
-
return csvToJsonAsync.generateJsonFileFromCsv(input, output);
|
|
4808
|
-
}
|
|
4809
|
-
});
|
|
4810
|
-
exports.csvStringToJson = function(csvString) {
|
|
4811
|
-
return csvToJson.csvStringToJson(csvString);
|
|
4812
|
-
};
|
|
4813
|
-
exports.csvStringToJsonStringified = function(csvString) {
|
|
4814
|
-
if (csvString === undefined || csvString === null) {
|
|
4815
|
-
throw new Error("csvString is not defined!!!");
|
|
4816
|
-
}
|
|
4817
|
-
return csvToJson.csvStringToJsonStringified(csvString);
|
|
4818
|
-
};
|
|
4819
|
-
exports.jsonToCsv = function(inputFileName, outputFileName) {
|
|
4820
|
-
csvToJson.generateJsonFileFromCsv(inputFileName, outputFileName);
|
|
4821
|
-
};
|
|
4822
|
-
exports.browser = require_browserApi();
|
|
4823
|
-
});
|
|
4824
|
-
|
|
4825
4225
|
// src/utils/accountSuggester.ts
|
|
4826
4226
|
var exports_accountSuggester = {};
|
|
4827
4227
|
__export(exports_accountSuggester, {
|
|
@@ -4914,7 +4314,7 @@ function buildBatchSuggestionPrompt(postings, context) {
|
|
|
4914
4314
|
prompt += `## Example Classification Patterns from Rules
|
|
4915
4315
|
|
|
4916
4316
|
`;
|
|
4917
|
-
const sampleSize = Math.min(
|
|
4317
|
+
const sampleSize = Math.min(EXAMPLE_PATTERN_SAMPLE_SIZE, context.existingRules.length);
|
|
4918
4318
|
for (let i2 = 0;i2 < sampleSize; i2++) {
|
|
4919
4319
|
const pattern = context.existingRules[i2];
|
|
4920
4320
|
prompt += `- If description matches "${pattern.condition}" \u2192 ${pattern.account}
|
|
@@ -5092,7 +4492,7 @@ function generateMockSuggestions(postings) {
|
|
|
5092
4492
|
});
|
|
5093
4493
|
return response;
|
|
5094
4494
|
}
|
|
5095
|
-
var suggestionCache;
|
|
4495
|
+
var EXAMPLE_PATTERN_SAMPLE_SIZE = 10, suggestionCache;
|
|
5096
4496
|
var init_accountSuggester = __esm(() => {
|
|
5097
4497
|
init_agentLoader();
|
|
5098
4498
|
suggestionCache = {};
|
|
@@ -5100,7 +4500,7 @@ var init_accountSuggester = __esm(() => {
|
|
|
5100
4500
|
|
|
5101
4501
|
// src/index.ts
|
|
5102
4502
|
init_agentLoader();
|
|
5103
|
-
import { dirname as
|
|
4503
|
+
import { dirname as dirname4, join as join12 } from "path";
|
|
5104
4504
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
5105
4505
|
|
|
5106
4506
|
// node_modules/zod/v4/classic/external.js
|
|
@@ -17425,7 +16825,7 @@ function tool(input) {
|
|
|
17425
16825
|
tool.schema = exports_external;
|
|
17426
16826
|
// src/tools/fetch-currency-prices.ts
|
|
17427
16827
|
var {$ } = globalThis.Bun;
|
|
17428
|
-
import * as
|
|
16828
|
+
import * as path4 from "path";
|
|
17429
16829
|
|
|
17430
16830
|
// src/utils/agentRestriction.ts
|
|
17431
16831
|
function checkAccountantAgent(agent, toolPrompt, additionalFields) {
|
|
@@ -17442,10 +16842,32 @@ function checkAccountantAgent(agent, toolPrompt, additionalFields) {
|
|
|
17442
16842
|
return JSON.stringify(errorResponse);
|
|
17443
16843
|
}
|
|
17444
16844
|
|
|
17445
|
-
// src/utils/
|
|
16845
|
+
// src/utils/yamlLoader.ts
|
|
17446
16846
|
init_js_yaml();
|
|
17447
16847
|
import * as fs from "fs";
|
|
17448
16848
|
import * as path from "path";
|
|
16849
|
+
function loadYamlConfig(directory, configFile, validator, notFoundMessage) {
|
|
16850
|
+
const configPath = path.join(directory, configFile);
|
|
16851
|
+
if (!fs.existsSync(configPath)) {
|
|
16852
|
+
throw new Error(notFoundMessage || `Configuration file not found: ${configFile}. Please create this file to configure the feature.`);
|
|
16853
|
+
}
|
|
16854
|
+
let parsed;
|
|
16855
|
+
try {
|
|
16856
|
+
const content = fs.readFileSync(configPath, "utf-8");
|
|
16857
|
+
parsed = jsYaml.load(content);
|
|
16858
|
+
} catch (err) {
|
|
16859
|
+
if (err instanceof jsYaml.YAMLException) {
|
|
16860
|
+
throw new Error(`Failed to parse ${configFile}: ${err.message}`);
|
|
16861
|
+
}
|
|
16862
|
+
throw err;
|
|
16863
|
+
}
|
|
16864
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
16865
|
+
throw new Error(`Invalid config: ${configFile} must contain a YAML object`);
|
|
16866
|
+
}
|
|
16867
|
+
return validator(parsed);
|
|
16868
|
+
}
|
|
16869
|
+
|
|
16870
|
+
// src/utils/pricesConfig.ts
|
|
17449
16871
|
var CONFIG_FILE = "config/prices.yaml";
|
|
17450
16872
|
var REQUIRED_CURRENCY_FIELDS = ["source", "pair", "file"];
|
|
17451
16873
|
function getDefaultBackfillDate() {
|
|
@@ -17471,48 +16893,84 @@ function validateCurrencyConfig(name, config2) {
|
|
|
17471
16893
|
};
|
|
17472
16894
|
}
|
|
17473
16895
|
function loadPricesConfig(directory) {
|
|
17474
|
-
|
|
17475
|
-
|
|
17476
|
-
|
|
17477
|
-
}
|
|
17478
|
-
let parsed;
|
|
17479
|
-
try {
|
|
17480
|
-
const content = fs.readFileSync(configPath, "utf-8");
|
|
17481
|
-
parsed = jsYaml.load(content);
|
|
17482
|
-
} catch (err) {
|
|
17483
|
-
if (err instanceof jsYaml.YAMLException) {
|
|
17484
|
-
throw new Error(`Failed to parse ${CONFIG_FILE}: ${err.message}`);
|
|
16896
|
+
return loadYamlConfig(directory, CONFIG_FILE, (parsedObj) => {
|
|
16897
|
+
if (!parsedObj.currencies || typeof parsedObj.currencies !== "object") {
|
|
16898
|
+
throw new Error(`Invalid config: 'currencies' section is required`);
|
|
17485
16899
|
}
|
|
17486
|
-
|
|
16900
|
+
const currenciesObj = parsedObj.currencies;
|
|
16901
|
+
if (Object.keys(currenciesObj).length === 0) {
|
|
16902
|
+
throw new Error(`Invalid config: 'currencies' section must contain at least one currency`);
|
|
16903
|
+
}
|
|
16904
|
+
const currencies = {};
|
|
16905
|
+
for (const [name, config2] of Object.entries(currenciesObj)) {
|
|
16906
|
+
currencies[name] = validateCurrencyConfig(name, config2);
|
|
16907
|
+
}
|
|
16908
|
+
return { currencies };
|
|
16909
|
+
}, `Configuration file not found: ${CONFIG_FILE}. Please refer to the plugin's GitHub repository for setup instructions.`);
|
|
16910
|
+
}
|
|
16911
|
+
|
|
16912
|
+
// src/utils/journalUtils.ts
|
|
16913
|
+
import * as fs3 from "fs";
|
|
16914
|
+
import * as path3 from "path";
|
|
16915
|
+
|
|
16916
|
+
// src/utils/fileUtils.ts
|
|
16917
|
+
import * as fs2 from "fs";
|
|
16918
|
+
import * as path2 from "path";
|
|
16919
|
+
function findCsvFiles(baseDir, options = {}) {
|
|
16920
|
+
if (!fs2.existsSync(baseDir)) {
|
|
16921
|
+
return [];
|
|
17487
16922
|
}
|
|
17488
|
-
|
|
17489
|
-
|
|
16923
|
+
let searchDir = baseDir;
|
|
16924
|
+
if (options.subdir) {
|
|
16925
|
+
searchDir = path2.join(searchDir, options.subdir);
|
|
16926
|
+
if (options.subsubdir) {
|
|
16927
|
+
searchDir = path2.join(searchDir, options.subsubdir);
|
|
16928
|
+
}
|
|
17490
16929
|
}
|
|
17491
|
-
|
|
17492
|
-
|
|
17493
|
-
throw new Error(`Invalid config: 'currencies' section is required`);
|
|
16930
|
+
if (!fs2.existsSync(searchDir)) {
|
|
16931
|
+
return [];
|
|
17494
16932
|
}
|
|
17495
|
-
const
|
|
17496
|
-
if (
|
|
17497
|
-
|
|
16933
|
+
const csvFiles = [];
|
|
16934
|
+
if (options.recursive) {
|
|
16935
|
+
let scanDirectory = function(dir) {
|
|
16936
|
+
const entries = fs2.readdirSync(dir, { withFileTypes: true });
|
|
16937
|
+
for (const entry of entries) {
|
|
16938
|
+
const fullPath = path2.join(dir, entry.name);
|
|
16939
|
+
if (entry.isDirectory()) {
|
|
16940
|
+
scanDirectory(fullPath);
|
|
16941
|
+
} else if (entry.isFile() && entry.name.toLowerCase().endsWith(".csv")) {
|
|
16942
|
+
csvFiles.push(options.fullPaths ? fullPath : entry.name);
|
|
16943
|
+
}
|
|
16944
|
+
}
|
|
16945
|
+
};
|
|
16946
|
+
scanDirectory(searchDir);
|
|
16947
|
+
} else {
|
|
16948
|
+
const entries = fs2.readdirSync(searchDir);
|
|
16949
|
+
for (const name of entries) {
|
|
16950
|
+
if (!name.toLowerCase().endsWith(".csv"))
|
|
16951
|
+
continue;
|
|
16952
|
+
const fullPath = path2.join(searchDir, name);
|
|
16953
|
+
if (fs2.statSync(fullPath).isFile()) {
|
|
16954
|
+
csvFiles.push(options.fullPaths ? fullPath : name);
|
|
16955
|
+
}
|
|
16956
|
+
}
|
|
17498
16957
|
}
|
|
17499
|
-
|
|
17500
|
-
|
|
17501
|
-
|
|
16958
|
+
return csvFiles.sort();
|
|
16959
|
+
}
|
|
16960
|
+
function ensureDirectory(dirPath) {
|
|
16961
|
+
if (!fs2.existsSync(dirPath)) {
|
|
16962
|
+
fs2.mkdirSync(dirPath, { recursive: true });
|
|
17502
16963
|
}
|
|
17503
|
-
return { currencies };
|
|
17504
16964
|
}
|
|
17505
16965
|
|
|
17506
16966
|
// src/utils/journalUtils.ts
|
|
17507
|
-
import * as fs2 from "fs";
|
|
17508
|
-
import * as path2 from "path";
|
|
17509
16967
|
function extractDateFromPriceLine(line) {
|
|
17510
16968
|
return line.split(" ")[1];
|
|
17511
16969
|
}
|
|
17512
16970
|
function updatePriceJournal(journalPath, newPriceLines) {
|
|
17513
16971
|
let existingLines = [];
|
|
17514
|
-
if (
|
|
17515
|
-
existingLines =
|
|
16972
|
+
if (fs3.existsSync(journalPath)) {
|
|
16973
|
+
existingLines = fs3.readFileSync(journalPath, "utf-8").split(`
|
|
17516
16974
|
`).filter((line) => line.trim() !== "");
|
|
17517
16975
|
}
|
|
17518
16976
|
const priceMap = new Map;
|
|
@@ -17527,54 +16985,25 @@ function updatePriceJournal(journalPath, newPriceLines) {
|
|
|
17527
16985
|
priceMap.set(date5, line);
|
|
17528
16986
|
}
|
|
17529
16987
|
const sortedLines = Array.from(priceMap.entries()).sort((a, b) => a[0].localeCompare(b[0])).map(([, line]) => line);
|
|
17530
|
-
|
|
16988
|
+
fs3.writeFileSync(journalPath, sortedLines.join(`
|
|
17531
16989
|
`) + `
|
|
17532
16990
|
`);
|
|
17533
16991
|
}
|
|
17534
|
-
function findCsvFiles(directory, provider, currency) {
|
|
17535
|
-
const csvFiles = [];
|
|
17536
|
-
if (!fs2.existsSync(directory)) {
|
|
17537
|
-
return csvFiles;
|
|
17538
|
-
}
|
|
17539
|
-
let searchPath = directory;
|
|
17540
|
-
if (provider) {
|
|
17541
|
-
searchPath = path2.join(searchPath, provider);
|
|
17542
|
-
if (currency) {
|
|
17543
|
-
searchPath = path2.join(searchPath, currency);
|
|
17544
|
-
}
|
|
17545
|
-
}
|
|
17546
|
-
if (!fs2.existsSync(searchPath)) {
|
|
17547
|
-
return csvFiles;
|
|
17548
|
-
}
|
|
17549
|
-
function scanDirectory(dir) {
|
|
17550
|
-
const entries = fs2.readdirSync(dir, { withFileTypes: true });
|
|
17551
|
-
for (const entry of entries) {
|
|
17552
|
-
const fullPath = path2.join(dir, entry.name);
|
|
17553
|
-
if (entry.isDirectory()) {
|
|
17554
|
-
scanDirectory(fullPath);
|
|
17555
|
-
} else if (entry.isFile() && entry.name.endsWith(".csv")) {
|
|
17556
|
-
csvFiles.push(fullPath);
|
|
17557
|
-
}
|
|
17558
|
-
}
|
|
17559
|
-
}
|
|
17560
|
-
scanDirectory(searchPath);
|
|
17561
|
-
return csvFiles.sort();
|
|
17562
|
-
}
|
|
17563
16992
|
function ensureYearJournalExists(directory, year) {
|
|
17564
|
-
const ledgerDir =
|
|
17565
|
-
const yearJournalPath =
|
|
17566
|
-
const mainJournalPath =
|
|
17567
|
-
if (!
|
|
17568
|
-
|
|
17569
|
-
}
|
|
17570
|
-
if (!
|
|
17571
|
-
|
|
16993
|
+
const ledgerDir = path3.join(directory, "ledger");
|
|
16994
|
+
const yearJournalPath = path3.join(ledgerDir, `${year}.journal`);
|
|
16995
|
+
const mainJournalPath = path3.join(directory, ".hledger.journal");
|
|
16996
|
+
if (!fs3.existsSync(ledgerDir)) {
|
|
16997
|
+
fs3.mkdirSync(ledgerDir, { recursive: true });
|
|
16998
|
+
}
|
|
16999
|
+
if (!fs3.existsSync(yearJournalPath)) {
|
|
17000
|
+
fs3.writeFileSync(yearJournalPath, `; ${year} transactions
|
|
17572
17001
|
`);
|
|
17573
17002
|
}
|
|
17574
|
-
if (!
|
|
17003
|
+
if (!fs3.existsSync(mainJournalPath)) {
|
|
17575
17004
|
throw new Error(`.hledger.journal not found at ${mainJournalPath}. Create it first with appropriate includes.`);
|
|
17576
17005
|
}
|
|
17577
|
-
const mainJournalContent =
|
|
17006
|
+
const mainJournalContent = fs3.readFileSync(mainJournalPath, "utf-8");
|
|
17578
17007
|
const includeDirective = `include ledger/${year}.journal`;
|
|
17579
17008
|
const lines = mainJournalContent.split(`
|
|
17580
17009
|
`);
|
|
@@ -17586,7 +17015,7 @@ function ensureYearJournalExists(directory, year) {
|
|
|
17586
17015
|
const newContent = mainJournalContent.trimEnd() + `
|
|
17587
17016
|
` + includeDirective + `
|
|
17588
17017
|
`;
|
|
17589
|
-
|
|
17018
|
+
fs3.writeFileSync(mainJournalPath, newContent);
|
|
17590
17019
|
}
|
|
17591
17020
|
return yearJournalPath;
|
|
17592
17021
|
}
|
|
@@ -17648,7 +17077,7 @@ function parsePriceLine(line) {
|
|
|
17648
17077
|
formattedLine: line
|
|
17649
17078
|
};
|
|
17650
17079
|
}
|
|
17651
|
-
function
|
|
17080
|
+
function filterAndSortPriceLinesByDateRange(priceLines, startDate, endDate) {
|
|
17652
17081
|
return priceLines.map(parsePriceLine).filter((parsed) => {
|
|
17653
17082
|
if (!parsed)
|
|
17654
17083
|
return false;
|
|
@@ -17684,7 +17113,7 @@ async function fetchCurrencyPrices(directory, agent, backfill, priceFetcher = de
|
|
|
17684
17113
|
});
|
|
17685
17114
|
continue;
|
|
17686
17115
|
}
|
|
17687
|
-
const priceLines =
|
|
17116
|
+
const priceLines = filterAndSortPriceLinesByDateRange(rawPriceLines, startDate, endDate);
|
|
17688
17117
|
if (priceLines.length === 0) {
|
|
17689
17118
|
results.push({
|
|
17690
17119
|
ticker,
|
|
@@ -17692,7 +17121,7 @@ async function fetchCurrencyPrices(directory, agent, backfill, priceFetcher = de
|
|
|
17692
17121
|
});
|
|
17693
17122
|
continue;
|
|
17694
17123
|
}
|
|
17695
|
-
const journalPath =
|
|
17124
|
+
const journalPath = path4.join(directory, "ledger", "currencies", currencyConfig.file);
|
|
17696
17125
|
updatePriceJournal(journalPath, priceLines);
|
|
17697
17126
|
const latestPriceLine = priceLines[priceLines.length - 1];
|
|
17698
17127
|
results.push({
|
|
@@ -17725,9 +17154,6 @@ import * as fs5 from "fs";
|
|
|
17725
17154
|
import * as path6 from "path";
|
|
17726
17155
|
|
|
17727
17156
|
// src/utils/importConfig.ts
|
|
17728
|
-
init_js_yaml();
|
|
17729
|
-
import * as fs3 from "fs";
|
|
17730
|
-
import * as path4 from "path";
|
|
17731
17157
|
var CONFIG_FILE2 = "config/import/providers.yaml";
|
|
17732
17158
|
var REQUIRED_PATH_FIELDS = [
|
|
17733
17159
|
"import",
|
|
@@ -17850,43 +17276,27 @@ function validateProviderConfig(name, config2) {
|
|
|
17850
17276
|
return { detect, currencies };
|
|
17851
17277
|
}
|
|
17852
17278
|
function loadImportConfig(directory) {
|
|
17853
|
-
|
|
17854
|
-
|
|
17855
|
-
|
|
17856
|
-
}
|
|
17857
|
-
let parsed;
|
|
17858
|
-
try {
|
|
17859
|
-
const content = fs3.readFileSync(configPath, "utf-8");
|
|
17860
|
-
parsed = jsYaml.load(content);
|
|
17861
|
-
} catch (err) {
|
|
17862
|
-
if (err instanceof jsYaml.YAMLException) {
|
|
17863
|
-
throw new Error(`Failed to parse ${CONFIG_FILE2}: ${err.message}`);
|
|
17279
|
+
return loadYamlConfig(directory, CONFIG_FILE2, (parsedObj) => {
|
|
17280
|
+
if (!parsedObj.paths) {
|
|
17281
|
+
throw new Error("Invalid config: 'paths' section is required");
|
|
17864
17282
|
}
|
|
17865
|
-
|
|
17866
|
-
|
|
17867
|
-
|
|
17868
|
-
|
|
17869
|
-
|
|
17870
|
-
|
|
17871
|
-
|
|
17872
|
-
|
|
17873
|
-
|
|
17874
|
-
|
|
17875
|
-
|
|
17876
|
-
|
|
17877
|
-
|
|
17878
|
-
|
|
17879
|
-
|
|
17880
|
-
|
|
17881
|
-
}
|
|
17882
|
-
const providers = {};
|
|
17883
|
-
for (const [name, config2] of Object.entries(providersObj)) {
|
|
17884
|
-
providers[name] = validateProviderConfig(name, config2);
|
|
17885
|
-
}
|
|
17886
|
-
if (!paths.logs) {
|
|
17887
|
-
paths.logs = ".memory";
|
|
17888
|
-
}
|
|
17889
|
-
return { paths, providers };
|
|
17283
|
+
const paths = validatePaths(parsedObj.paths);
|
|
17284
|
+
if (!parsedObj.providers || typeof parsedObj.providers !== "object") {
|
|
17285
|
+
throw new Error("Invalid config: 'providers' section is required");
|
|
17286
|
+
}
|
|
17287
|
+
const providersObj = parsedObj.providers;
|
|
17288
|
+
if (Object.keys(providersObj).length === 0) {
|
|
17289
|
+
throw new Error("Invalid config: 'providers' section must contain at least one provider");
|
|
17290
|
+
}
|
|
17291
|
+
const providers = {};
|
|
17292
|
+
for (const [name, config2] of Object.entries(providersObj)) {
|
|
17293
|
+
providers[name] = validateProviderConfig(name, config2);
|
|
17294
|
+
}
|
|
17295
|
+
if (!paths.logs) {
|
|
17296
|
+
paths.logs = ".memory";
|
|
17297
|
+
}
|
|
17298
|
+
return { paths, providers };
|
|
17299
|
+
}, `Configuration file not found: ${CONFIG_FILE2}. Please create this file to configure statement imports.`);
|
|
17890
17300
|
}
|
|
17891
17301
|
|
|
17892
17302
|
// src/utils/providerDetector.ts
|
|
@@ -17994,23 +17404,84 @@ function detectProvider(filename, content, config2) {
|
|
|
17994
17404
|
return null;
|
|
17995
17405
|
}
|
|
17996
17406
|
|
|
17997
|
-
// src/utils/
|
|
17407
|
+
// src/utils/importContext.ts
|
|
17998
17408
|
import * as fs4 from "fs";
|
|
17999
17409
|
import * as path5 from "path";
|
|
18000
|
-
|
|
18001
|
-
|
|
18002
|
-
|
|
17410
|
+
import { randomUUID } from "crypto";
|
|
17411
|
+
function getContextPath(directory, contextId) {
|
|
17412
|
+
return path5.join(directory, ".memory", `${contextId}.json`);
|
|
17413
|
+
}
|
|
17414
|
+
function ensureMemoryDir(directory) {
|
|
17415
|
+
const memoryDir = path5.join(directory, ".memory");
|
|
17416
|
+
if (!fs4.existsSync(memoryDir)) {
|
|
17417
|
+
fs4.mkdirSync(memoryDir, { recursive: true });
|
|
18003
17418
|
}
|
|
18004
|
-
return fs4.readdirSync(importsDir).filter((file2) => file2.toLowerCase().endsWith(".csv")).filter((file2) => {
|
|
18005
|
-
const fullPath = path5.join(importsDir, file2);
|
|
18006
|
-
return fs4.statSync(fullPath).isFile();
|
|
18007
|
-
});
|
|
18008
17419
|
}
|
|
18009
|
-
function
|
|
18010
|
-
|
|
18011
|
-
|
|
17420
|
+
function createContext(directory, params) {
|
|
17421
|
+
const now = new Date().toISOString();
|
|
17422
|
+
const context = {
|
|
17423
|
+
id: randomUUID(),
|
|
17424
|
+
createdAt: now,
|
|
17425
|
+
updatedAt: now,
|
|
17426
|
+
filename: params.filename,
|
|
17427
|
+
filePath: params.filePath,
|
|
17428
|
+
provider: params.provider,
|
|
17429
|
+
currency: params.currency,
|
|
17430
|
+
accountNumber: params.accountNumber,
|
|
17431
|
+
originalFilename: params.originalFilename,
|
|
17432
|
+
fromDate: params.fromDate,
|
|
17433
|
+
untilDate: params.untilDate,
|
|
17434
|
+
openingBalance: params.openingBalance,
|
|
17435
|
+
closingBalance: params.closingBalance,
|
|
17436
|
+
account: params.account
|
|
17437
|
+
};
|
|
17438
|
+
ensureMemoryDir(directory);
|
|
17439
|
+
const contextPath = getContextPath(directory, context.id);
|
|
17440
|
+
fs4.writeFileSync(contextPath, JSON.stringify(context, null, 2), "utf-8");
|
|
17441
|
+
return context;
|
|
17442
|
+
}
|
|
17443
|
+
function validateContext(context, contextId) {
|
|
17444
|
+
const requiredFields = [
|
|
17445
|
+
"id",
|
|
17446
|
+
"filename",
|
|
17447
|
+
"filePath",
|
|
17448
|
+
"provider",
|
|
17449
|
+
"currency"
|
|
17450
|
+
];
|
|
17451
|
+
for (const field of requiredFields) {
|
|
17452
|
+
if (!context[field]) {
|
|
17453
|
+
throw new Error(`Invalid context ${contextId}: missing required field '${field}'`);
|
|
17454
|
+
}
|
|
18012
17455
|
}
|
|
18013
17456
|
}
|
|
17457
|
+
function loadContext(directory, contextId) {
|
|
17458
|
+
const contextPath = getContextPath(directory, contextId);
|
|
17459
|
+
if (!fs4.existsSync(contextPath)) {
|
|
17460
|
+
throw new Error(`Context not found: ${contextId}`);
|
|
17461
|
+
}
|
|
17462
|
+
const content = fs4.readFileSync(contextPath, "utf-8");
|
|
17463
|
+
let context;
|
|
17464
|
+
try {
|
|
17465
|
+
context = JSON.parse(content);
|
|
17466
|
+
} catch (err) {
|
|
17467
|
+
throw new Error(`Malformed context file ${contextId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
17468
|
+
}
|
|
17469
|
+
validateContext(context, contextId);
|
|
17470
|
+
return context;
|
|
17471
|
+
}
|
|
17472
|
+
function updateContext(directory, contextId, updates) {
|
|
17473
|
+
const context = loadContext(directory, contextId);
|
|
17474
|
+
const updatedContext = {
|
|
17475
|
+
...context,
|
|
17476
|
+
...updates,
|
|
17477
|
+
id: context.id,
|
|
17478
|
+
createdAt: context.createdAt,
|
|
17479
|
+
updatedAt: new Date().toISOString()
|
|
17480
|
+
};
|
|
17481
|
+
const contextPath = getContextPath(directory, contextId);
|
|
17482
|
+
fs4.writeFileSync(contextPath, JSON.stringify(updatedContext, null, 2), "utf-8");
|
|
17483
|
+
return updatedContext;
|
|
17484
|
+
}
|
|
18014
17485
|
|
|
18015
17486
|
// src/tools/classify-statements.ts
|
|
18016
17487
|
function buildSuccessResult2(classified, unrecognized, message) {
|
|
@@ -18078,7 +17549,20 @@ function planMoves(csvFiles, importsDir, pendingDir, unrecognizedDir, config2) {
|
|
|
18078
17549
|
}
|
|
18079
17550
|
return { plannedMoves, collisions };
|
|
18080
17551
|
}
|
|
18081
|
-
function
|
|
17552
|
+
function extractMetadata2(detection) {
|
|
17553
|
+
const metadata = detection.metadata;
|
|
17554
|
+
if (!metadata) {
|
|
17555
|
+
return {};
|
|
17556
|
+
}
|
|
17557
|
+
return {
|
|
17558
|
+
accountNumber: metadata["account-number"],
|
|
17559
|
+
fromDate: metadata["from-date"],
|
|
17560
|
+
untilDate: metadata["until-date"],
|
|
17561
|
+
openingBalance: metadata["opening-balance"],
|
|
17562
|
+
closingBalance: metadata["closing-balance"]
|
|
17563
|
+
};
|
|
17564
|
+
}
|
|
17565
|
+
function executeMoves(plannedMoves, config2, unrecognizedDir, directory) {
|
|
18082
17566
|
const classified = [];
|
|
18083
17567
|
const unrecognized = [];
|
|
18084
17568
|
for (const move of plannedMoves) {
|
|
@@ -18086,12 +17570,27 @@ function executeMoves(plannedMoves, config2, unrecognizedDir) {
|
|
|
18086
17570
|
const targetDir = path6.dirname(move.targetPath);
|
|
18087
17571
|
ensureDirectory(targetDir);
|
|
18088
17572
|
fs5.renameSync(move.sourcePath, move.targetPath);
|
|
17573
|
+
const targetPath = path6.join(config2.paths.pending, move.detection.provider, move.detection.currency, move.targetFilename);
|
|
17574
|
+
const metadata = extractMetadata2(move.detection);
|
|
17575
|
+
const context = createContext(directory, {
|
|
17576
|
+
filename: move.targetFilename,
|
|
17577
|
+
filePath: targetPath,
|
|
17578
|
+
provider: move.detection.provider,
|
|
17579
|
+
currency: move.detection.currency,
|
|
17580
|
+
originalFilename: move.detection.outputFilename ? move.filename : undefined,
|
|
17581
|
+
accountNumber: metadata.accountNumber,
|
|
17582
|
+
fromDate: metadata.fromDate,
|
|
17583
|
+
untilDate: metadata.untilDate,
|
|
17584
|
+
openingBalance: metadata.openingBalance,
|
|
17585
|
+
closingBalance: metadata.closingBalance
|
|
17586
|
+
});
|
|
18089
17587
|
classified.push({
|
|
18090
17588
|
filename: move.targetFilename,
|
|
18091
17589
|
originalFilename: move.detection.outputFilename ? move.filename : undefined,
|
|
18092
17590
|
provider: move.detection.provider,
|
|
18093
17591
|
currency: move.detection.currency,
|
|
18094
|
-
targetPath
|
|
17592
|
+
targetPath,
|
|
17593
|
+
contextId: context.id
|
|
18095
17594
|
});
|
|
18096
17595
|
} else {
|
|
18097
17596
|
ensureDirectory(unrecognizedDir);
|
|
@@ -18122,7 +17621,7 @@ async function classifyStatements(directory, agent, configLoader = loadImportCon
|
|
|
18122
17621
|
const importsDir = path6.join(directory, config2.paths.import);
|
|
18123
17622
|
const pendingDir = path6.join(directory, config2.paths.pending);
|
|
18124
17623
|
const unrecognizedDir = path6.join(directory, config2.paths.unrecognized);
|
|
18125
|
-
const csvFiles =
|
|
17624
|
+
const csvFiles = findCsvFiles(importsDir);
|
|
18126
17625
|
if (csvFiles.length === 0) {
|
|
18127
17626
|
return buildSuccessResult2([], [], `No CSV files found in ${config2.paths.import}`);
|
|
18128
17627
|
}
|
|
@@ -18130,11 +17629,19 @@ async function classifyStatements(directory, agent, configLoader = loadImportCon
|
|
|
18130
17629
|
if (collisions.length > 0) {
|
|
18131
17630
|
return buildCollisionError(collisions);
|
|
18132
17631
|
}
|
|
18133
|
-
const { classified, unrecognized } = executeMoves(plannedMoves, config2, unrecognizedDir);
|
|
17632
|
+
const { classified, unrecognized } = executeMoves(plannedMoves, config2, unrecognizedDir, directory);
|
|
18134
17633
|
return buildSuccessResult2(classified, unrecognized);
|
|
18135
17634
|
}
|
|
18136
17635
|
var classify_statements_default = tool({
|
|
18137
|
-
description:
|
|
17636
|
+
description: `ACCOUNTANT AGENT ONLY: Classifies bank statement CSV files from the imports directory by detecting their provider and currency, then moves them to the appropriate pending import directories.
|
|
17637
|
+
|
|
17638
|
+
For each CSV file:
|
|
17639
|
+
- Detects the provider and currency using rules from providers.yaml
|
|
17640
|
+
- Creates an import context (.memory/{uuid}.json) to track the file through the pipeline
|
|
17641
|
+
- Extracts metadata (account number, dates, balances) from CSV headers
|
|
17642
|
+
- Moves the file to import/pending/{provider}/{currency}/
|
|
17643
|
+
- Checks for file collisions before any moves (atomic: all-or-nothing)
|
|
17644
|
+
- Unrecognized files are moved to import/unrecognized/`,
|
|
18138
17645
|
args: {},
|
|
18139
17646
|
async execute(_params, context) {
|
|
18140
17647
|
const { directory, agent } = context;
|
|
@@ -20474,7 +19981,7 @@ class LRUCache {
|
|
|
20474
19981
|
// node_modules/path-scurry/dist/esm/index.js
|
|
20475
19982
|
import { posix, win32 } from "path";
|
|
20476
19983
|
import { fileURLToPath } from "url";
|
|
20477
|
-
import { lstatSync, readdir as readdirCB, readdirSync as
|
|
19984
|
+
import { lstatSync, readdir as readdirCB, readdirSync as readdirSync2, readlinkSync, realpathSync as rps } from "fs";
|
|
20478
19985
|
import * as actualFS from "fs";
|
|
20479
19986
|
import { lstat, readdir, readlink, realpath } from "fs/promises";
|
|
20480
19987
|
|
|
@@ -21146,7 +20653,7 @@ var realpathSync = rps.native;
|
|
|
21146
20653
|
var defaultFS = {
|
|
21147
20654
|
lstatSync,
|
|
21148
20655
|
readdir: readdirCB,
|
|
21149
|
-
readdirSync:
|
|
20656
|
+
readdirSync: readdirSync2,
|
|
21150
20657
|
readlinkSync,
|
|
21151
20658
|
realpathSync,
|
|
21152
20659
|
promises: {
|
|
@@ -23456,7 +22963,12 @@ function loadRulesMapping(rulesDir) {
|
|
|
23456
22963
|
if (!stat.isFile()) {
|
|
23457
22964
|
continue;
|
|
23458
22965
|
}
|
|
23459
|
-
|
|
22966
|
+
let content;
|
|
22967
|
+
try {
|
|
22968
|
+
content = fs6.readFileSync(rulesFilePath, "utf-8");
|
|
22969
|
+
} catch {
|
|
22970
|
+
continue;
|
|
22971
|
+
}
|
|
23460
22972
|
const sourcePath = parseSourceDirective(content);
|
|
23461
22973
|
if (!sourcePath) {
|
|
23462
22974
|
continue;
|
|
@@ -23499,18 +23011,30 @@ function findRulesForCsv(csvPath, mapping) {
|
|
|
23499
23011
|
|
|
23500
23012
|
// src/utils/hledgerExecutor.ts
|
|
23501
23013
|
var {$: $2 } = globalThis.Bun;
|
|
23014
|
+
var STDERR_TRUNCATE_LENGTH = 500;
|
|
23502
23015
|
async function defaultHledgerExecutor(cmdArgs) {
|
|
23503
23016
|
try {
|
|
23504
23017
|
const result = await $2`hledger ${cmdArgs}`.quiet().nothrow();
|
|
23018
|
+
const stdout = result.stdout.toString();
|
|
23019
|
+
const stderr = result.stderr.toString();
|
|
23020
|
+
if (result.exitCode !== 0 && stderr) {
|
|
23021
|
+
process.stderr.write(`[hledger] command failed (exit ${result.exitCode}): hledger ${cmdArgs.join(" ")}
|
|
23022
|
+
`);
|
|
23023
|
+
process.stderr.write(`[hledger] stderr: ${stderr.slice(0, STDERR_TRUNCATE_LENGTH)}
|
|
23024
|
+
`);
|
|
23025
|
+
}
|
|
23505
23026
|
return {
|
|
23506
|
-
stdout
|
|
23507
|
-
stderr
|
|
23027
|
+
stdout,
|
|
23028
|
+
stderr,
|
|
23508
23029
|
exitCode: result.exitCode
|
|
23509
23030
|
};
|
|
23510
23031
|
} catch (error45) {
|
|
23032
|
+
const errorMessage = error45 instanceof Error ? error45.message : String(error45);
|
|
23033
|
+
process.stderr.write(`[hledger] exception: ${errorMessage}
|
|
23034
|
+
`);
|
|
23511
23035
|
return {
|
|
23512
23036
|
stdout: "",
|
|
23513
|
-
stderr:
|
|
23037
|
+
stderr: errorMessage,
|
|
23514
23038
|
exitCode: 1
|
|
23515
23039
|
};
|
|
23516
23040
|
}
|
|
@@ -23701,7 +23225,7 @@ function parseRulesFile(rulesContent) {
|
|
|
23701
23225
|
}
|
|
23702
23226
|
|
|
23703
23227
|
// src/utils/csvParser.ts
|
|
23704
|
-
var
|
|
23228
|
+
var import_papaparse2 = __toESM(require_papaparse(), 1);
|
|
23705
23229
|
import * as fs8 from "fs";
|
|
23706
23230
|
|
|
23707
23231
|
// src/utils/balanceUtils.ts
|
|
@@ -23750,6 +23274,7 @@ function balancesMatch(balance1, balance2) {
|
|
|
23750
23274
|
}
|
|
23751
23275
|
|
|
23752
23276
|
// src/utils/csvParser.ts
|
|
23277
|
+
var AMOUNT_MATCH_TOLERANCE = 0.001;
|
|
23753
23278
|
function parseCsvFile(csvPath, config2) {
|
|
23754
23279
|
const csvContent = fs8.readFileSync(csvPath, "utf-8");
|
|
23755
23280
|
const lines = csvContent.split(`
|
|
@@ -23758,22 +23283,26 @@ function parseCsvFile(csvPath, config2) {
|
|
|
23758
23283
|
if (headerIndex >= lines.length) {
|
|
23759
23284
|
return [];
|
|
23760
23285
|
}
|
|
23761
|
-
const
|
|
23762
|
-
const dataLines = lines.slice(headerIndex + 1).filter((line) => line.trim() !== "");
|
|
23763
|
-
const csvWithHeader = [headerLine, ...dataLines].join(`
|
|
23286
|
+
const csvWithHeader = lines.slice(headerIndex).join(`
|
|
23764
23287
|
`);
|
|
23765
|
-
const
|
|
23766
|
-
const
|
|
23767
|
-
|
|
23768
|
-
|
|
23769
|
-
|
|
23770
|
-
|
|
23771
|
-
|
|
23772
|
-
|
|
23773
|
-
|
|
23774
|
-
|
|
23288
|
+
const useFieldNames = config2.fieldNames.length > 0;
|
|
23289
|
+
const result = import_papaparse2.default.parse(csvWithHeader, {
|
|
23290
|
+
header: !useFieldNames,
|
|
23291
|
+
delimiter: config2.separator,
|
|
23292
|
+
skipEmptyLines: true
|
|
23293
|
+
});
|
|
23294
|
+
if (useFieldNames) {
|
|
23295
|
+
const rawRows = result.data;
|
|
23296
|
+
const dataRows = rawRows.slice(1);
|
|
23297
|
+
return dataRows.map((values) => {
|
|
23298
|
+
const row = {};
|
|
23299
|
+
for (let i2 = 0;i2 < config2.fieldNames.length && i2 < values.length; i2++) {
|
|
23300
|
+
row[config2.fieldNames[i2]] = values[i2];
|
|
23301
|
+
}
|
|
23302
|
+
return row;
|
|
23303
|
+
});
|
|
23775
23304
|
}
|
|
23776
|
-
return
|
|
23305
|
+
return result.data;
|
|
23777
23306
|
}
|
|
23778
23307
|
function getRowAmount(row, amountFields) {
|
|
23779
23308
|
if (amountFields.single) {
|
|
@@ -23851,7 +23380,7 @@ function findMatchingCsvRow(posting, csvRows, config2) {
|
|
|
23851
23380
|
const rowAmount = getRowAmount(row, config2.amountFields);
|
|
23852
23381
|
if (rowDate !== posting.date)
|
|
23853
23382
|
return false;
|
|
23854
|
-
if (Math.abs(rowAmount - postingAmount) >
|
|
23383
|
+
if (Math.abs(rowAmount - postingAmount) > AMOUNT_MATCH_TOLERANCE)
|
|
23855
23384
|
return false;
|
|
23856
23385
|
return true;
|
|
23857
23386
|
});
|
|
@@ -23885,11 +23414,21 @@ function findMatchingCsvRow(posting, csvRows, config2) {
|
|
|
23885
23414
|
|
|
23886
23415
|
// src/tools/import-statements.ts
|
|
23887
23416
|
function buildErrorResult3(error45, hint) {
|
|
23888
|
-
|
|
23417
|
+
const result = {
|
|
23889
23418
|
success: false,
|
|
23890
23419
|
error: error45,
|
|
23891
|
-
hint
|
|
23892
|
-
|
|
23420
|
+
hint,
|
|
23421
|
+
files: [],
|
|
23422
|
+
summary: {
|
|
23423
|
+
filesProcessed: 0,
|
|
23424
|
+
filesWithErrors: 0,
|
|
23425
|
+
filesWithoutRules: 0,
|
|
23426
|
+
totalTransactions: 0,
|
|
23427
|
+
matched: 0,
|
|
23428
|
+
unknown: 0
|
|
23429
|
+
}
|
|
23430
|
+
};
|
|
23431
|
+
return JSON.stringify(result);
|
|
23893
23432
|
}
|
|
23894
23433
|
function buildErrorResultWithDetails(error45, files, summary, hint) {
|
|
23895
23434
|
return JSON.stringify({
|
|
@@ -24068,17 +23607,12 @@ async function importStatements(directory, agent, options, configLoader = loadIm
|
|
|
24068
23607
|
const rulesDir = path9.join(directory, config2.paths.rules);
|
|
24069
23608
|
const doneDir = path9.join(directory, config2.paths.done);
|
|
24070
23609
|
const rulesMapping = loadRulesMapping(rulesDir);
|
|
24071
|
-
const
|
|
24072
|
-
|
|
24073
|
-
|
|
24074
|
-
|
|
24075
|
-
filesWithErrors: 0,
|
|
24076
|
-
filesWithoutRules: 0,
|
|
24077
|
-
totalTransactions: 0,
|
|
24078
|
-
matched: 0,
|
|
24079
|
-
unknown: 0
|
|
24080
|
-
}, "No CSV files found to process");
|
|
23610
|
+
const importContext = loadContext(directory, options.contextId);
|
|
23611
|
+
const csvPath = path9.join(directory, importContext.filePath);
|
|
23612
|
+
if (!fs9.existsSync(csvPath)) {
|
|
23613
|
+
return buildErrorResult3(`CSV file not found: ${importContext.filePath}`, "The file may have been moved or deleted");
|
|
24081
23614
|
}
|
|
23615
|
+
const csvFiles = [csvPath];
|
|
24082
23616
|
const fileResults = [];
|
|
24083
23617
|
let totalTransactions = 0;
|
|
24084
23618
|
let totalMatched = 0;
|
|
@@ -24169,6 +23703,16 @@ async function importStatements(directory, agent, options, configLoader = loadIm
|
|
|
24169
23703
|
unknown: totalUnknown
|
|
24170
23704
|
}, importResult.hint);
|
|
24171
23705
|
}
|
|
23706
|
+
if (fileResults.length > 0) {
|
|
23707
|
+
const firstResult = fileResults[0];
|
|
23708
|
+
const newFilePath = path9.join(config2.paths.done, path9.relative(pendingDir, path9.join(directory, firstResult.csv)));
|
|
23709
|
+
updateContext(directory, options.contextId, {
|
|
23710
|
+
filePath: newFilePath,
|
|
23711
|
+
rulesFile: firstResult.rulesFile || undefined,
|
|
23712
|
+
yearJournal: firstResult.transactionYear ? `ledger/${firstResult.transactionYear}.journal` : undefined,
|
|
23713
|
+
transactionCount: firstResult.totalTransactions
|
|
23714
|
+
});
|
|
23715
|
+
}
|
|
24172
23716
|
return buildSuccessResult3(fileResults.map((f) => ({
|
|
24173
23717
|
...f,
|
|
24174
23718
|
imported: true
|
|
@@ -24204,15 +23748,13 @@ This tool processes CSV files in the pending import directory and uses hledger's
|
|
|
24204
23748
|
|
|
24205
23749
|
Note: This tool is typically called via import-pipeline for the full workflow.`,
|
|
24206
23750
|
args: {
|
|
24207
|
-
|
|
24208
|
-
currency: tool.schema.string().optional().describe('Filter by currency (e.g., "chf", "eur"). If omitted, process all currencies for the provider.'),
|
|
23751
|
+
contextId: tool.schema.string().describe("Context ID from classify step. Used to locate the specific CSV file to process."),
|
|
24209
23752
|
checkOnly: tool.schema.boolean().optional().describe("If true (default), only check for unknown accounts without importing. Set to false to perform actual import.")
|
|
24210
23753
|
},
|
|
24211
23754
|
async execute(params, context) {
|
|
24212
23755
|
const { directory, agent } = context;
|
|
24213
23756
|
return importStatements(directory, agent, {
|
|
24214
|
-
|
|
24215
|
-
currency: params.currency,
|
|
23757
|
+
contextId: params.contextId,
|
|
24216
23758
|
checkOnly: params.checkOnly
|
|
24217
23759
|
});
|
|
24218
23760
|
}
|
|
@@ -24221,10 +23763,19 @@ Note: This tool is typically called via import-pipeline for the full workflow.`,
|
|
|
24221
23763
|
import * as fs10 from "fs";
|
|
24222
23764
|
import * as path10 from "path";
|
|
24223
23765
|
function buildErrorResult4(params) {
|
|
24224
|
-
|
|
23766
|
+
const result = {
|
|
24225
23767
|
success: false,
|
|
24226
|
-
|
|
24227
|
-
|
|
23768
|
+
account: params.account ?? "",
|
|
23769
|
+
expectedBalance: params.expectedBalance ?? "",
|
|
23770
|
+
actualBalance: params.actualBalance ?? "",
|
|
23771
|
+
lastTransactionDate: params.lastTransactionDate ?? "",
|
|
23772
|
+
csvFile: params.csvFile ?? "",
|
|
23773
|
+
difference: params.difference,
|
|
23774
|
+
metadata: params.metadata,
|
|
23775
|
+
error: params.error,
|
|
23776
|
+
hint: params.hint
|
|
23777
|
+
};
|
|
23778
|
+
return JSON.stringify(result);
|
|
24228
23779
|
}
|
|
24229
23780
|
function loadConfiguration(directory, configLoader) {
|
|
24230
23781
|
try {
|
|
@@ -24239,69 +23790,87 @@ function loadConfiguration(directory, configLoader) {
|
|
|
24239
23790
|
};
|
|
24240
23791
|
}
|
|
24241
23792
|
}
|
|
24242
|
-
function
|
|
24243
|
-
const
|
|
24244
|
-
if (
|
|
24245
|
-
const providerFilter = options.provider ? ` --provider=${options.provider}` : "";
|
|
24246
|
-
const currencyFilter = options.currency ? ` --currency=${options.currency}` : "";
|
|
23793
|
+
function verifyCsvExists(directory, importContext) {
|
|
23794
|
+
const csvFile = path10.join(directory, importContext.filePath);
|
|
23795
|
+
if (!fs10.existsSync(csvFile)) {
|
|
24247
23796
|
return {
|
|
24248
23797
|
error: buildErrorResult4({
|
|
24249
|
-
error: `
|
|
24250
|
-
hint: `
|
|
23798
|
+
error: `CSV file not found: ${importContext.filePath}`,
|
|
23799
|
+
hint: `The file may have been moved or deleted. Context ID: ${importContext.id}`
|
|
24251
23800
|
})
|
|
24252
23801
|
};
|
|
24253
23802
|
}
|
|
24254
|
-
|
|
24255
|
-
|
|
24256
|
-
|
|
23803
|
+
return { csvFile, relativePath: importContext.filePath };
|
|
23804
|
+
}
|
|
23805
|
+
function getBalanceFromContext(importContext) {
|
|
23806
|
+
if (!importContext.closingBalance)
|
|
23807
|
+
return;
|
|
23808
|
+
let balance = importContext.closingBalance;
|
|
23809
|
+
if (importContext.currency && !balance.includes(importContext.currency.toUpperCase())) {
|
|
23810
|
+
balance = `${importContext.currency.toUpperCase()} ${balance}`;
|
|
23811
|
+
}
|
|
23812
|
+
return balance;
|
|
23813
|
+
}
|
|
23814
|
+
function getBalanceFromCsvMetadata(metadata) {
|
|
23815
|
+
if (!metadata?.["closing-balance"])
|
|
23816
|
+
return;
|
|
23817
|
+
let balance = metadata["closing-balance"];
|
|
23818
|
+
const currency = metadata.currency;
|
|
23819
|
+
if (currency && balance && !balance.includes(currency)) {
|
|
23820
|
+
balance = `${currency} ${balance}`;
|
|
23821
|
+
}
|
|
23822
|
+
return balance;
|
|
23823
|
+
}
|
|
23824
|
+
function getBalanceFromCsvAnalysis(csvFile, rulesDir) {
|
|
23825
|
+
const csvAnalysis = tryExtractClosingBalanceFromCSV(csvFile, rulesDir);
|
|
23826
|
+
if (csvAnalysis && csvAnalysis.confidence === "high") {
|
|
23827
|
+
return csvAnalysis.balance;
|
|
23828
|
+
}
|
|
23829
|
+
return;
|
|
24257
23830
|
}
|
|
24258
|
-
function
|
|
24259
|
-
let metadata;
|
|
23831
|
+
function extractCsvMetadata(csvFile, config2) {
|
|
24260
23832
|
try {
|
|
24261
23833
|
const content = fs10.readFileSync(csvFile, "utf-8");
|
|
24262
23834
|
const filename = path10.basename(csvFile);
|
|
24263
23835
|
const detectionResult = detectProvider(filename, content, config2);
|
|
24264
|
-
|
|
24265
|
-
|
|
24266
|
-
|
|
24267
|
-
|
|
24268
|
-
|
|
24269
|
-
|
|
24270
|
-
|
|
24271
|
-
|
|
24272
|
-
|
|
24273
|
-
|
|
24274
|
-
closingBalance = `${currency} ${closingBalance}`;
|
|
24275
|
-
}
|
|
24276
|
-
}
|
|
24277
|
-
if (!closingBalance) {
|
|
24278
|
-
const csvAnalysis = tryExtractClosingBalanceFromCSV(csvFile, rulesDir);
|
|
24279
|
-
if (csvAnalysis && csvAnalysis.confidence === "high") {
|
|
24280
|
-
closingBalance = csvAnalysis.balance;
|
|
24281
|
-
return { closingBalance, metadata, fromCSVAnalysis: true };
|
|
23836
|
+
if (detectionResult?.metadata) {
|
|
23837
|
+
const m = detectionResult.metadata;
|
|
23838
|
+
return {
|
|
23839
|
+
"from-date": m["from-date"],
|
|
23840
|
+
"until-date": m["until-date"],
|
|
23841
|
+
"opening-balance": m["opening-balance"],
|
|
23842
|
+
"closing-balance": m["closing-balance"],
|
|
23843
|
+
currency: m["currency"],
|
|
23844
|
+
"account-number": m["account-number"]
|
|
23845
|
+
};
|
|
24282
23846
|
}
|
|
24283
|
-
}
|
|
24284
|
-
|
|
24285
|
-
const retryCmd = buildRetryCommand(options, "CHF 2324.79", options.account);
|
|
24286
|
-
return {
|
|
24287
|
-
error: buildErrorResult4({
|
|
24288
|
-
csvFile: relativeCsvPath,
|
|
24289
|
-
error: "No closing balance found in CSV metadata or data",
|
|
24290
|
-
hint: `Provide closingBalance parameter manually. Example retry: ${retryCmd}`,
|
|
24291
|
-
metadata
|
|
24292
|
-
})
|
|
24293
|
-
};
|
|
24294
|
-
}
|
|
24295
|
-
return { closingBalance, metadata };
|
|
23847
|
+
} catch {}
|
|
23848
|
+
return;
|
|
24296
23849
|
}
|
|
24297
|
-
function
|
|
24298
|
-
const
|
|
24299
|
-
|
|
24300
|
-
|
|
23850
|
+
function determineClosingBalance(csvFile, config2, importContext, manualClosingBalance, relativeCsvPath, rulesDir) {
|
|
23851
|
+
const metadata = extractCsvMetadata(csvFile, config2);
|
|
23852
|
+
const closingBalance = manualClosingBalance || getBalanceFromContext(importContext) || getBalanceFromCsvMetadata(metadata);
|
|
23853
|
+
if (closingBalance) {
|
|
23854
|
+
return { closingBalance, metadata };
|
|
24301
23855
|
}
|
|
24302
|
-
|
|
24303
|
-
|
|
23856
|
+
const analysisBalance = getBalanceFromCsvAnalysis(csvFile, rulesDir);
|
|
23857
|
+
if (analysisBalance) {
|
|
23858
|
+
return { closingBalance: analysisBalance, metadata, fromCSVAnalysis: true };
|
|
24304
23859
|
}
|
|
23860
|
+
const currency = importContext.currency?.toUpperCase() || "CHF";
|
|
23861
|
+
const exampleBalance = `${currency} <amount>`;
|
|
23862
|
+
const retryCmd = buildRetryCommand(importContext.id, exampleBalance);
|
|
23863
|
+
return {
|
|
23864
|
+
error: buildErrorResult4({
|
|
23865
|
+
csvFile: relativeCsvPath,
|
|
23866
|
+
error: "No closing balance found in CSV metadata or data",
|
|
23867
|
+
hint: `Provide closingBalance parameter manually. Example retry: ${retryCmd}`,
|
|
23868
|
+
metadata
|
|
23869
|
+
})
|
|
23870
|
+
};
|
|
23871
|
+
}
|
|
23872
|
+
function buildRetryCommand(contextId, closingBalance, account) {
|
|
23873
|
+
const parts = ["reconcile-statement", `--contextId ${contextId}`];
|
|
24305
23874
|
if (closingBalance) {
|
|
24306
23875
|
parts.push(`--closingBalance "${closingBalance}"`);
|
|
24307
23876
|
}
|
|
@@ -24310,19 +23879,17 @@ function buildRetryCommand(options, closingBalance, account) {
|
|
|
24310
23879
|
}
|
|
24311
23880
|
return parts.join(" ");
|
|
24312
23881
|
}
|
|
24313
|
-
function determineAccount(csvFile, rulesDir,
|
|
24314
|
-
let account =
|
|
23882
|
+
function determineAccount(csvFile, rulesDir, importContext, manualAccount, relativeCsvPath, metadata) {
|
|
23883
|
+
let account = manualAccount;
|
|
23884
|
+
const rulesMapping = loadRulesMapping(rulesDir);
|
|
23885
|
+
const rulesFile = findRulesForCsv(csvFile, rulesMapping);
|
|
24315
23886
|
if (!account) {
|
|
24316
|
-
const rulesMapping = loadRulesMapping(rulesDir);
|
|
24317
|
-
const rulesFile = findRulesForCsv(csvFile, rulesMapping);
|
|
24318
23887
|
if (rulesFile) {
|
|
24319
23888
|
account = getAccountFromRulesFile(rulesFile) ?? undefined;
|
|
24320
23889
|
}
|
|
24321
23890
|
}
|
|
24322
23891
|
if (!account) {
|
|
24323
|
-
const
|
|
24324
|
-
const rulesFile = findRulesForCsv(csvFile, rulesMapping);
|
|
24325
|
-
const rulesHint = rulesFile ? `Add 'account1 assets:bank:...' to ${rulesFile} or retry with: ${buildRetryCommand(options, undefined, "assets:bank:...")}` : `Create a rules file in ${rulesDir} with 'account1' directive or retry with: ${buildRetryCommand(options, undefined, "assets:bank:...")}`;
|
|
23892
|
+
const rulesHint = rulesFile ? `Add 'account1 assets:bank:...' to ${rulesFile} or retry with: ${buildRetryCommand(importContext.id, undefined, "assets:bank:...")}` : `Create a rules file in ${rulesDir} with 'account1' directive or retry with: ${buildRetryCommand(importContext.id, undefined, "assets:bank:...")}`;
|
|
24326
23893
|
return {
|
|
24327
23894
|
error: buildErrorResult4({
|
|
24328
23895
|
csvFile: relativeCsvPath,
|
|
@@ -24403,25 +23970,33 @@ async function reconcileStatement(directory, agent, options, configLoader = load
|
|
|
24403
23970
|
if (restrictionError) {
|
|
24404
23971
|
return restrictionError;
|
|
24405
23972
|
}
|
|
23973
|
+
let importContext;
|
|
23974
|
+
try {
|
|
23975
|
+
importContext = loadContext(directory, options.contextId);
|
|
23976
|
+
} catch {
|
|
23977
|
+
return buildErrorResult4({
|
|
23978
|
+
error: `Failed to load import context: ${options.contextId}`,
|
|
23979
|
+
hint: "Ensure the context ID is valid and the context file exists in .memory/"
|
|
23980
|
+
});
|
|
23981
|
+
}
|
|
24406
23982
|
const configResult = loadConfiguration(directory, configLoader);
|
|
24407
23983
|
if ("error" in configResult) {
|
|
24408
23984
|
return configResult.error;
|
|
24409
23985
|
}
|
|
24410
23986
|
const { config: config2 } = configResult;
|
|
24411
|
-
const doneDir = path10.join(directory, config2.paths.done);
|
|
24412
23987
|
const rulesDir = path10.join(directory, config2.paths.rules);
|
|
24413
23988
|
const mainJournalPath = path10.join(directory, ".hledger.journal");
|
|
24414
|
-
const csvResult =
|
|
23989
|
+
const csvResult = verifyCsvExists(directory, importContext);
|
|
24415
23990
|
if ("error" in csvResult) {
|
|
24416
23991
|
return csvResult.error;
|
|
24417
23992
|
}
|
|
24418
23993
|
const { csvFile, relativePath: relativeCsvPath } = csvResult;
|
|
24419
|
-
const balanceResult = determineClosingBalance(csvFile, config2, options, relativeCsvPath, rulesDir);
|
|
23994
|
+
const balanceResult = determineClosingBalance(csvFile, config2, importContext, options.closingBalance, relativeCsvPath, rulesDir);
|
|
24420
23995
|
if ("error" in balanceResult) {
|
|
24421
23996
|
return balanceResult.error;
|
|
24422
23997
|
}
|
|
24423
23998
|
const { closingBalance, metadata, fromCSVAnalysis } = balanceResult;
|
|
24424
|
-
const accountResult = determineAccount(csvFile, rulesDir, options, relativeCsvPath, metadata);
|
|
23999
|
+
const accountResult = determineAccount(csvFile, rulesDir, importContext, options.account, relativeCsvPath, metadata);
|
|
24425
24000
|
if ("error" in accountResult) {
|
|
24426
24001
|
return accountResult.error;
|
|
24427
24002
|
}
|
|
@@ -24508,30 +24083,30 @@ var reconcile_statement_default = tool({
|
|
|
24508
24083
|
This tool validates that the imported transactions result in the correct closing balance.
|
|
24509
24084
|
|
|
24510
24085
|
**Workflow:**
|
|
24511
|
-
1.
|
|
24512
|
-
2. Extracts closing balance from CSV metadata (or uses manual override)
|
|
24086
|
+
1. Loads import context to find the CSV file
|
|
24087
|
+
2. Extracts closing balance from context/CSV metadata (or uses manual override)
|
|
24513
24088
|
3. Determines the account from the matching rules file (or uses manual override)
|
|
24514
24089
|
4. Queries hledger for the actual balance as of the last transaction date
|
|
24515
24090
|
5. Compares expected vs actual balance
|
|
24516
24091
|
|
|
24517
24092
|
**Balance Sources:**
|
|
24518
|
-
- Automatic:
|
|
24093
|
+
- Automatic: From import context or CSV header metadata (e.g., UBS files have "Closing balance:" row)
|
|
24519
24094
|
- Manual: Provided via closingBalance parameter (required for providers like Revolut)
|
|
24520
24095
|
|
|
24521
24096
|
**Account Detection:**
|
|
24522
24097
|
- Automatic: Parsed from account1 directive in matching rules file
|
|
24523
|
-
- Manual: Provided via account parameter
|
|
24098
|
+
- Manual: Provided via account parameter
|
|
24099
|
+
|
|
24100
|
+
Note: This tool requires a contextId from a prior classify/import step.`,
|
|
24524
24101
|
args: {
|
|
24525
|
-
|
|
24526
|
-
currency: tool.schema.string().optional().describe('Filter by currency (e.g., "chf", "eur")'),
|
|
24102
|
+
contextId: tool.schema.string().describe("Context ID from classify/import step (required)"),
|
|
24527
24103
|
closingBalance: tool.schema.string().optional().describe('Manual closing balance (e.g., "CHF 2324.79"). Required if not in CSV metadata.'),
|
|
24528
24104
|
account: tool.schema.string().optional().describe('Manual account (e.g., "assets:bank:ubs:checking"). Auto-detected from rules file if not provided.')
|
|
24529
24105
|
},
|
|
24530
24106
|
async execute(params, context) {
|
|
24531
24107
|
const { directory, agent } = context;
|
|
24532
24108
|
return reconcileStatement(directory, agent, {
|
|
24533
|
-
|
|
24534
|
-
currency: params.currency,
|
|
24109
|
+
contextId: params.contextId,
|
|
24535
24110
|
closingBalance: params.closingBalance,
|
|
24536
24111
|
account: params.account
|
|
24537
24112
|
});
|
|
@@ -24658,6 +24233,7 @@ function ensureAccountDeclarations(yearJournalPath, accounts) {
|
|
|
24658
24233
|
// src/utils/logger.ts
|
|
24659
24234
|
import fs12 from "fs/promises";
|
|
24660
24235
|
import path11 from "path";
|
|
24236
|
+
var LOG_LINE_LIMIT = 50;
|
|
24661
24237
|
|
|
24662
24238
|
class MarkdownLogger {
|
|
24663
24239
|
buffer = [];
|
|
@@ -24665,6 +24241,7 @@ class MarkdownLogger {
|
|
|
24665
24241
|
context = {};
|
|
24666
24242
|
autoFlush;
|
|
24667
24243
|
sectionDepth = 0;
|
|
24244
|
+
pendingFlush = null;
|
|
24668
24245
|
constructor(config2) {
|
|
24669
24246
|
this.autoFlush = config2.autoFlush ?? true;
|
|
24670
24247
|
this.context = config2.context || {};
|
|
@@ -24739,9 +24316,9 @@ class MarkdownLogger {
|
|
|
24739
24316
|
this.buffer.push("");
|
|
24740
24317
|
const lines = output.trim().split(`
|
|
24741
24318
|
`);
|
|
24742
|
-
if (lines.length >
|
|
24743
|
-
this.buffer.push(...lines.slice(0,
|
|
24744
|
-
this.buffer.push(`... (${lines.length -
|
|
24319
|
+
if (lines.length > LOG_LINE_LIMIT) {
|
|
24320
|
+
this.buffer.push(...lines.slice(0, LOG_LINE_LIMIT));
|
|
24321
|
+
this.buffer.push(`... (${lines.length - LOG_LINE_LIMIT} more lines omitted)`);
|
|
24745
24322
|
} else {
|
|
24746
24323
|
this.buffer.push(output.trim());
|
|
24747
24324
|
}
|
|
@@ -24763,6 +24340,9 @@ class MarkdownLogger {
|
|
|
24763
24340
|
this.context[key] = value;
|
|
24764
24341
|
}
|
|
24765
24342
|
async flush() {
|
|
24343
|
+
if (this.pendingFlush) {
|
|
24344
|
+
await this.pendingFlush;
|
|
24345
|
+
}
|
|
24766
24346
|
if (this.buffer.length === 0)
|
|
24767
24347
|
return;
|
|
24768
24348
|
try {
|
|
@@ -24775,7 +24355,7 @@ class MarkdownLogger {
|
|
|
24775
24355
|
return this.logPath;
|
|
24776
24356
|
}
|
|
24777
24357
|
flushAsync() {
|
|
24778
|
-
this.flush().catch(() => {});
|
|
24358
|
+
this.pendingFlush = this.flush().catch(() => {});
|
|
24779
24359
|
}
|
|
24780
24360
|
getTimestamp() {
|
|
24781
24361
|
return new Date().toISOString().replace(/:/g, "-").split(".")[0];
|
|
@@ -24841,16 +24421,28 @@ async function executeClassifyStep(context, logger) {
|
|
|
24841
24421
|
logger?.info("Classification skipped (skipClassify: true)");
|
|
24842
24422
|
context.result.steps.classify = buildStepResult(true, "Classification skipped (skipClassify: true)");
|
|
24843
24423
|
logger?.endSection();
|
|
24844
|
-
return;
|
|
24424
|
+
return [];
|
|
24845
24425
|
}
|
|
24846
24426
|
const classifyResult = await classifyStatements(context.directory, context.agent, context.configLoader);
|
|
24847
24427
|
const classifyParsed = JSON.parse(classifyResult);
|
|
24848
24428
|
const success2 = classifyParsed.success !== false;
|
|
24429
|
+
const contextIds = [];
|
|
24430
|
+
if (classifyParsed.classified?.length > 0) {
|
|
24431
|
+
for (const file2 of classifyParsed.classified) {
|
|
24432
|
+
if (file2.contextId) {
|
|
24433
|
+
contextIds.push(file2.contextId);
|
|
24434
|
+
}
|
|
24435
|
+
}
|
|
24436
|
+
}
|
|
24849
24437
|
let message = success2 ? "Classification complete" : "Classification had issues";
|
|
24850
24438
|
if (classifyParsed.unrecognized?.length > 0) {
|
|
24851
24439
|
message = `Classification complete with ${classifyParsed.unrecognized.length} unrecognized file(s)`;
|
|
24852
24440
|
logger?.warn(`${classifyParsed.unrecognized.length} unrecognized file(s)`);
|
|
24853
24441
|
}
|
|
24442
|
+
if (contextIds.length > 0) {
|
|
24443
|
+
message += ` (${contextIds.length} context(s) created)`;
|
|
24444
|
+
logger?.info(`Created ${contextIds.length} import context(s)`);
|
|
24445
|
+
}
|
|
24854
24446
|
logger?.logStep("Classify", success2 ? "success" : "error", message);
|
|
24855
24447
|
const details = {
|
|
24856
24448
|
success: success2,
|
|
@@ -24858,23 +24450,18 @@ async function executeClassifyStep(context, logger) {
|
|
|
24858
24450
|
classified: classifyParsed
|
|
24859
24451
|
};
|
|
24860
24452
|
context.result.steps.classify = buildStepResult(success2, message, details);
|
|
24453
|
+
context.result.contexts = contextIds;
|
|
24861
24454
|
logger?.endSection();
|
|
24455
|
+
return contextIds;
|
|
24862
24456
|
}
|
|
24863
|
-
async function executeAccountDeclarationsStep(context, logger) {
|
|
24457
|
+
async function executeAccountDeclarationsStep(context, contextId, logger) {
|
|
24864
24458
|
logger?.startSection("Step 2: Check Account Declarations");
|
|
24865
24459
|
logger?.logStep("Check Accounts", "start");
|
|
24866
24460
|
const config2 = context.configLoader(context.directory);
|
|
24867
|
-
const pendingDir = path12.join(context.directory, config2.paths.pending);
|
|
24868
24461
|
const rulesDir = path12.join(context.directory, config2.paths.rules);
|
|
24869
|
-
const
|
|
24870
|
-
|
|
24871
|
-
|
|
24872
|
-
accountsAdded: [],
|
|
24873
|
-
journalUpdated: "",
|
|
24874
|
-
rulesScanned: []
|
|
24875
|
-
});
|
|
24876
|
-
return;
|
|
24877
|
-
}
|
|
24462
|
+
const importCtx = loadContext(context.directory, contextId);
|
|
24463
|
+
const csvPath = path12.join(context.directory, importCtx.filePath);
|
|
24464
|
+
const csvFiles = [csvPath];
|
|
24878
24465
|
const rulesMapping = loadRulesMapping(rulesDir);
|
|
24879
24466
|
const matchedRulesFiles = new Set;
|
|
24880
24467
|
for (const csvFile of csvFiles) {
|
|
@@ -24949,12 +24536,11 @@ async function executeAccountDeclarationsStep(context, logger) {
|
|
|
24949
24536
|
});
|
|
24950
24537
|
logger?.endSection();
|
|
24951
24538
|
}
|
|
24952
|
-
async function executeDryRunStep(context, logger) {
|
|
24539
|
+
async function executeDryRunStep(context, contextId, logger) {
|
|
24953
24540
|
logger?.startSection("Step 3: Dry Run Import");
|
|
24954
24541
|
logger?.logStep("Dry Run", "start");
|
|
24955
24542
|
const dryRunResult = await importStatements(context.directory, context.agent, {
|
|
24956
|
-
|
|
24957
|
-
currency: context.options.currency,
|
|
24543
|
+
contextId,
|
|
24958
24544
|
checkOnly: true
|
|
24959
24545
|
}, context.configLoader, context.hledgerExecutor);
|
|
24960
24546
|
const dryRunParsed = JSON.parse(dryRunResult);
|
|
@@ -24979,30 +24565,25 @@ async function executeDryRunStep(context, logger) {
|
|
|
24979
24565
|
extractRulePatternsFromFile: extractRulePatternsFromFile2
|
|
24980
24566
|
} = await Promise.resolve().then(() => (init_accountSuggester(), exports_accountSuggester));
|
|
24981
24567
|
const config2 = context.configLoader(context.directory);
|
|
24982
|
-
const pendingDir = path12.join(context.directory, config2.paths.pending);
|
|
24983
24568
|
const rulesDir = path12.join(context.directory, config2.paths.rules);
|
|
24984
|
-
const
|
|
24569
|
+
const importCtx = loadContext(context.directory, contextId);
|
|
24570
|
+
const csvPath = path12.join(context.directory, importCtx.filePath);
|
|
24985
24571
|
const rulesMapping = loadRulesMapping(rulesDir);
|
|
24986
24572
|
let yearJournalPath;
|
|
24987
24573
|
let firstRulesFile;
|
|
24988
|
-
|
|
24989
|
-
|
|
24990
|
-
|
|
24991
|
-
|
|
24992
|
-
|
|
24993
|
-
|
|
24994
|
-
|
|
24995
|
-
|
|
24996
|
-
|
|
24997
|
-
|
|
24998
|
-
yearJournalPath = ensureYearJournalExists(context.directory, transactionYear);
|
|
24999
|
-
break;
|
|
25000
|
-
}
|
|
24574
|
+
const rulesFile = findRulesForCsv(csvPath, rulesMapping);
|
|
24575
|
+
if (rulesFile) {
|
|
24576
|
+
firstRulesFile = rulesFile;
|
|
24577
|
+
try {
|
|
24578
|
+
const result = await context.hledgerExecutor(["print", "-f", rulesFile]);
|
|
24579
|
+
if (result.exitCode === 0) {
|
|
24580
|
+
const years = extractTransactionYears(result.stdout);
|
|
24581
|
+
if (years.size > 0) {
|
|
24582
|
+
const transactionYear = Array.from(years)[0];
|
|
24583
|
+
yearJournalPath = ensureYearJournalExists(context.directory, transactionYear);
|
|
25001
24584
|
}
|
|
25002
|
-
} catch {
|
|
25003
|
-
continue;
|
|
25004
24585
|
}
|
|
25005
|
-
}
|
|
24586
|
+
} catch {}
|
|
25006
24587
|
}
|
|
25007
24588
|
const suggestionContext = {
|
|
25008
24589
|
existingAccounts: yearJournalPath ? await loadExistingAccounts2(yearJournalPath) : [],
|
|
@@ -25075,12 +24656,12 @@ function formatUnknownPostingsLog(postings) {
|
|
|
25075
24656
|
`;
|
|
25076
24657
|
return log;
|
|
25077
24658
|
}
|
|
25078
|
-
async function executeImportStep(context, logger) {
|
|
25079
|
-
|
|
24659
|
+
async function executeImportStep(context, contextId, logger) {
|
|
24660
|
+
const importContext = loadContext(context.directory, contextId);
|
|
24661
|
+
logger?.startSection(`Step 4: Import Transactions (${importContext.accountNumber || contextId})`);
|
|
25080
24662
|
logger?.logStep("Import", "start");
|
|
25081
24663
|
const importResult = await importStatements(context.directory, context.agent, {
|
|
25082
|
-
|
|
25083
|
-
currency: context.options.currency,
|
|
24664
|
+
contextId,
|
|
25084
24665
|
checkOnly: false
|
|
25085
24666
|
}, context.configLoader, context.hledgerExecutor);
|
|
25086
24667
|
const importParsed = JSON.parse(importResult);
|
|
@@ -25091,6 +24672,13 @@ async function executeImportStep(context, logger) {
|
|
|
25091
24672
|
summary: importParsed.summary,
|
|
25092
24673
|
error: importParsed.error
|
|
25093
24674
|
});
|
|
24675
|
+
if (importParsed.success) {
|
|
24676
|
+
updateContext(context.directory, contextId, {
|
|
24677
|
+
rulesFile: importParsed.files?.[0]?.rulesFile,
|
|
24678
|
+
yearJournal: importParsed.files?.[0]?.yearJournal,
|
|
24679
|
+
transactionCount: importParsed.summary?.totalTransactions
|
|
24680
|
+
});
|
|
24681
|
+
}
|
|
25094
24682
|
if (!importParsed.success) {
|
|
25095
24683
|
logger?.error("Import failed", new Error(importParsed.error || "Unknown error"));
|
|
25096
24684
|
logger?.endSection();
|
|
@@ -25099,12 +24687,12 @@ async function executeImportStep(context, logger) {
|
|
|
25099
24687
|
}
|
|
25100
24688
|
logger?.endSection();
|
|
25101
24689
|
}
|
|
25102
|
-
async function executeReconcileStep(context, logger) {
|
|
25103
|
-
|
|
24690
|
+
async function executeReconcileStep(context, contextId, logger) {
|
|
24691
|
+
const importContext = loadContext(context.directory, contextId);
|
|
24692
|
+
logger?.startSection(`Step 5: Reconcile Balance (${importContext.accountNumber || contextId})`);
|
|
25104
24693
|
logger?.logStep("Reconcile", "start");
|
|
25105
24694
|
const reconcileResult = await reconcileStatement(context.directory, context.agent, {
|
|
25106
|
-
|
|
25107
|
-
currency: context.options.currency,
|
|
24695
|
+
contextId,
|
|
25108
24696
|
closingBalance: context.options.closingBalance,
|
|
25109
24697
|
account: context.options.account
|
|
25110
24698
|
}, context.configLoader, context.hledgerExecutor);
|
|
@@ -25122,6 +24710,14 @@ async function executeReconcileStep(context, logger) {
|
|
|
25122
24710
|
metadata: reconcileParsed.metadata,
|
|
25123
24711
|
error: reconcileParsed.error
|
|
25124
24712
|
});
|
|
24713
|
+
if (reconcileParsed.success) {
|
|
24714
|
+
updateContext(context.directory, contextId, {
|
|
24715
|
+
reconciledAccount: reconcileParsed.account,
|
|
24716
|
+
actualBalance: reconcileParsed.actualBalance,
|
|
24717
|
+
lastTransactionDate: reconcileParsed.lastTransactionDate,
|
|
24718
|
+
reconciled: true
|
|
24719
|
+
});
|
|
24720
|
+
}
|
|
25125
24721
|
if (!reconcileParsed.success) {
|
|
25126
24722
|
logger?.error("Reconciliation failed", new Error(reconcileParsed.error || "Balance mismatch"));
|
|
25127
24723
|
logger?.endSection();
|
|
@@ -25160,21 +24756,31 @@ async function importPipeline(directory, agent, options, configLoader = loadImpo
|
|
|
25160
24756
|
result
|
|
25161
24757
|
};
|
|
25162
24758
|
try {
|
|
25163
|
-
await executeClassifyStep(context, logger);
|
|
25164
|
-
|
|
25165
|
-
|
|
25166
|
-
|
|
25167
|
-
|
|
25168
|
-
|
|
24759
|
+
const contextIds = await executeClassifyStep(context, logger);
|
|
24760
|
+
if (contextIds.length === 0) {
|
|
24761
|
+
logger.info("No files classified, nothing to import");
|
|
24762
|
+
return buildSuccessResult4(result, "No files to import");
|
|
24763
|
+
}
|
|
24764
|
+
let totalTransactions = 0;
|
|
24765
|
+
for (const contextId of contextIds) {
|
|
24766
|
+
const importContext = loadContext(context.directory, contextId);
|
|
24767
|
+
logger.info(`Processing: ${importContext.filename} (${importContext.accountNumber || "unknown account"})`);
|
|
24768
|
+
await executeAccountDeclarationsStep(context, contextId, logger);
|
|
24769
|
+
await executeDryRunStep(context, contextId, logger);
|
|
24770
|
+
await executeImportStep(context, contextId, logger);
|
|
24771
|
+
await executeReconcileStep(context, contextId, logger);
|
|
24772
|
+
totalTransactions += context.result.steps.import?.details?.summary?.totalTransactions || 0;
|
|
24773
|
+
}
|
|
25169
24774
|
logger.startSection("Summary");
|
|
25170
24775
|
logger.info(`Import completed successfully`);
|
|
25171
|
-
logger.info(`
|
|
24776
|
+
logger.info(`Contexts processed: ${contextIds.length}`);
|
|
24777
|
+
logger.info(`Total transactions imported: ${totalTransactions}`);
|
|
25172
24778
|
if (context.result.steps.reconcile?.details?.actualBalance) {
|
|
25173
24779
|
logger.info(`Balance: ${context.result.steps.reconcile.details.actualBalance}`);
|
|
25174
24780
|
}
|
|
25175
24781
|
logger.info(`Log file: ${logger.getLogPath()}`);
|
|
25176
24782
|
logger.endSection();
|
|
25177
|
-
return buildSuccessResult4(result, `Successfully imported ${
|
|
24783
|
+
return buildSuccessResult4(result, `Successfully imported ${totalTransactions} transaction(s) from ${contextIds.length} file(s)`);
|
|
25178
24784
|
} catch (error45) {
|
|
25179
24785
|
logger.error("Pipeline step failed", error45);
|
|
25180
24786
|
logger.info(`Log file: ${logger.getLogPath()}`);
|
|
@@ -25361,7 +24967,7 @@ You can now drop CSV files into import/incoming/ and run import-pipeline.`);
|
|
|
25361
24967
|
}
|
|
25362
24968
|
});
|
|
25363
24969
|
// src/index.ts
|
|
25364
|
-
var __dirname2 =
|
|
24970
|
+
var __dirname2 = dirname4(fileURLToPath3(import.meta.url));
|
|
25365
24971
|
var AGENT_FILE = join12(__dirname2, "..", "agent", "accountant.md");
|
|
25366
24972
|
var AccountantPlugin = async () => {
|
|
25367
24973
|
const agent = loadAgent(AGENT_FILE);
|