@fuzzle/opencode-accountant 0.0.9 → 0.0.10-next.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -9
- package/agent/accountant.md +31 -23
- package/dist/index.js +1198 -6
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -1340,8 +1340,608 @@ var require_papaparse = __commonJS((exports, module) => {
|
|
|
1340
1340
|
});
|
|
1341
1341
|
});
|
|
1342
1342
|
|
|
1343
|
+
// node_modules/convert-csv-to-json/src/util/fileUtils.js
|
|
1344
|
+
var require_fileUtils = __commonJS((exports, module) => {
|
|
1345
|
+
var fs6 = __require("fs");
|
|
1346
|
+
|
|
1347
|
+
class FileUtils {
|
|
1348
|
+
readFile(fileInputName, encoding) {
|
|
1349
|
+
return fs6.readFileSync(fileInputName, encoding).toString();
|
|
1350
|
+
}
|
|
1351
|
+
readFileAsync(fileInputName, encoding = "utf8") {
|
|
1352
|
+
if (fs6.promises && typeof fs6.promises.readFile === "function") {
|
|
1353
|
+
return fs6.promises.readFile(fileInputName, encoding).then((buf) => buf.toString());
|
|
1354
|
+
}
|
|
1355
|
+
return new Promise((resolve2, reject) => {
|
|
1356
|
+
fs6.readFile(fileInputName, encoding, (err, data) => {
|
|
1357
|
+
if (err) {
|
|
1358
|
+
reject(err);
|
|
1359
|
+
return;
|
|
1360
|
+
}
|
|
1361
|
+
resolve2(data.toString());
|
|
1362
|
+
});
|
|
1363
|
+
});
|
|
1364
|
+
}
|
|
1365
|
+
writeFile(json3, fileOutputName) {
|
|
1366
|
+
fs6.writeFile(fileOutputName, json3, function(err) {
|
|
1367
|
+
if (err) {
|
|
1368
|
+
throw err;
|
|
1369
|
+
} else {
|
|
1370
|
+
console.log("File saved: " + fileOutputName);
|
|
1371
|
+
}
|
|
1372
|
+
});
|
|
1373
|
+
}
|
|
1374
|
+
writeFileAsync(json3, fileOutputName) {
|
|
1375
|
+
if (fs6.promises && typeof fs6.promises.writeFile === "function") {
|
|
1376
|
+
return fs6.promises.writeFile(fileOutputName, json3);
|
|
1377
|
+
}
|
|
1378
|
+
return new Promise((resolve2, reject) => {
|
|
1379
|
+
fs6.writeFile(fileOutputName, json3, (err) => {
|
|
1380
|
+
if (err)
|
|
1381
|
+
return reject(err);
|
|
1382
|
+
resolve2();
|
|
1383
|
+
});
|
|
1384
|
+
});
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
module.exports = new FileUtils;
|
|
1388
|
+
});
|
|
1389
|
+
|
|
1390
|
+
// node_modules/convert-csv-to-json/src/util/stringUtils.js
|
|
1391
|
+
var require_stringUtils = __commonJS((exports, module) => {
|
|
1392
|
+
class StringUtils {
|
|
1393
|
+
static PATTERNS = {
|
|
1394
|
+
INTEGER: /^-?\d+$/,
|
|
1395
|
+
FLOAT: /^-?\d*\.\d+$/,
|
|
1396
|
+
WHITESPACE: /\s/g
|
|
1397
|
+
};
|
|
1398
|
+
static BOOLEAN_VALUES = {
|
|
1399
|
+
TRUE: "true",
|
|
1400
|
+
FALSE: "false"
|
|
1401
|
+
};
|
|
1402
|
+
trimPropertyName(shouldTrimAll, propertyName) {
|
|
1403
|
+
if (!propertyName) {
|
|
1404
|
+
return "";
|
|
1405
|
+
}
|
|
1406
|
+
return shouldTrimAll ? propertyName.replace(StringUtils.PATTERNS.WHITESPACE, "") : propertyName.trim();
|
|
1407
|
+
}
|
|
1408
|
+
getValueFormatByType(value) {
|
|
1409
|
+
if (this.isEmpty(value)) {
|
|
1410
|
+
return String();
|
|
1411
|
+
}
|
|
1412
|
+
if (this.isBoolean(value)) {
|
|
1413
|
+
return this.convertToBoolean(value);
|
|
1414
|
+
}
|
|
1415
|
+
if (this.isInteger(value)) {
|
|
1416
|
+
return this.convertInteger(value);
|
|
1417
|
+
}
|
|
1418
|
+
if (this.isFloat(value)) {
|
|
1419
|
+
return this.convertFloat(value);
|
|
1420
|
+
}
|
|
1421
|
+
return String(value);
|
|
1422
|
+
}
|
|
1423
|
+
hasContent(values = []) {
|
|
1424
|
+
return Array.isArray(values) && values.some((value) => Boolean(value));
|
|
1425
|
+
}
|
|
1426
|
+
isEmpty(value) {
|
|
1427
|
+
return value === undefined || value === "";
|
|
1428
|
+
}
|
|
1429
|
+
isBoolean(value) {
|
|
1430
|
+
const normalizedValue = value.toLowerCase();
|
|
1431
|
+
return normalizedValue === StringUtils.BOOLEAN_VALUES.TRUE || normalizedValue === StringUtils.BOOLEAN_VALUES.FALSE;
|
|
1432
|
+
}
|
|
1433
|
+
isInteger(value) {
|
|
1434
|
+
return StringUtils.PATTERNS.INTEGER.test(value);
|
|
1435
|
+
}
|
|
1436
|
+
isFloat(value) {
|
|
1437
|
+
return StringUtils.PATTERNS.FLOAT.test(value);
|
|
1438
|
+
}
|
|
1439
|
+
hasLeadingZero(value) {
|
|
1440
|
+
const isPositiveWithLeadingZero = value.length > 1 && value[0] === "0";
|
|
1441
|
+
const isNegativeWithLeadingZero = value.length > 2 && value[0] === "-" && value[1] === "0";
|
|
1442
|
+
return isPositiveWithLeadingZero || isNegativeWithLeadingZero;
|
|
1443
|
+
}
|
|
1444
|
+
convertToBoolean(value) {
|
|
1445
|
+
return JSON.parse(value.toLowerCase());
|
|
1446
|
+
}
|
|
1447
|
+
convertInteger(value) {
|
|
1448
|
+
if (this.hasLeadingZero(value)) {
|
|
1449
|
+
return String(value);
|
|
1450
|
+
}
|
|
1451
|
+
const num = Number(value);
|
|
1452
|
+
return Number.isSafeInteger(num) ? num : String(value);
|
|
1453
|
+
}
|
|
1454
|
+
convertFloat(value) {
|
|
1455
|
+
const num = Number(value);
|
|
1456
|
+
return Number.isFinite(num) ? num : String(value);
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
module.exports = new StringUtils;
|
|
1460
|
+
});
|
|
1461
|
+
|
|
1462
|
+
// node_modules/convert-csv-to-json/src/util/jsonUtils.js
|
|
1463
|
+
var require_jsonUtils = __commonJS((exports, module) => {
|
|
1464
|
+
class JsonUtil {
|
|
1465
|
+
validateJson(json3) {
|
|
1466
|
+
try {
|
|
1467
|
+
JSON.parse(json3);
|
|
1468
|
+
} catch (err) {
|
|
1469
|
+
throw Error(`Parsed csv has generated an invalid json!!!
|
|
1470
|
+
` + err);
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
module.exports = new JsonUtil;
|
|
1475
|
+
});
|
|
1476
|
+
|
|
1477
|
+
// node_modules/convert-csv-to-json/src/csvToJson.js
|
|
1478
|
+
var require_csvToJson = __commonJS((exports, module) => {
|
|
1479
|
+
var fileUtils = require_fileUtils();
|
|
1480
|
+
var stringUtils = require_stringUtils();
|
|
1481
|
+
var jsonUtils = require_jsonUtils();
|
|
1482
|
+
var newLine = /\r?\n/;
|
|
1483
|
+
var defaultFieldDelimiter = ";";
|
|
1484
|
+
|
|
1485
|
+
class CsvToJson {
|
|
1486
|
+
formatValueByType(active) {
|
|
1487
|
+
this.printValueFormatByType = active;
|
|
1488
|
+
return this;
|
|
1489
|
+
}
|
|
1490
|
+
supportQuotedField(active) {
|
|
1491
|
+
this.isSupportQuotedField = active;
|
|
1492
|
+
return this;
|
|
1493
|
+
}
|
|
1494
|
+
fieldDelimiter(delimiter) {
|
|
1495
|
+
this.delimiter = delimiter;
|
|
1496
|
+
return this;
|
|
1497
|
+
}
|
|
1498
|
+
trimHeaderFieldWhiteSpace(active) {
|
|
1499
|
+
this.isTrimHeaderFieldWhiteSpace = active;
|
|
1500
|
+
return this;
|
|
1501
|
+
}
|
|
1502
|
+
indexHeader(indexHeaderValue) {
|
|
1503
|
+
if (isNaN(indexHeaderValue)) {
|
|
1504
|
+
throw new Error("The index Header must be a Number!");
|
|
1505
|
+
}
|
|
1506
|
+
this.indexHeaderValue = indexHeaderValue;
|
|
1507
|
+
return this;
|
|
1508
|
+
}
|
|
1509
|
+
parseSubArray(delimiter = "*", separator = ",") {
|
|
1510
|
+
this.parseSubArrayDelimiter = delimiter;
|
|
1511
|
+
this.parseSubArraySeparator = separator;
|
|
1512
|
+
}
|
|
1513
|
+
encoding(encoding) {
|
|
1514
|
+
this.encoding = encoding;
|
|
1515
|
+
return this;
|
|
1516
|
+
}
|
|
1517
|
+
generateJsonFileFromCsv(fileInputName, fileOutputName) {
|
|
1518
|
+
let jsonStringified = this.getJsonFromCsvStringified(fileInputName);
|
|
1519
|
+
fileUtils.writeFile(jsonStringified, fileOutputName);
|
|
1520
|
+
}
|
|
1521
|
+
getJsonFromCsvStringified(fileInputName) {
|
|
1522
|
+
let json3 = this.getJsonFromCsv(fileInputName);
|
|
1523
|
+
let jsonStringified = JSON.stringify(json3, undefined, 1);
|
|
1524
|
+
jsonUtils.validateJson(jsonStringified);
|
|
1525
|
+
return jsonStringified;
|
|
1526
|
+
}
|
|
1527
|
+
getJsonFromCsv(fileInputName) {
|
|
1528
|
+
let parsedCsv = fileUtils.readFile(fileInputName, this.encoding);
|
|
1529
|
+
return this.csvToJson(parsedCsv);
|
|
1530
|
+
}
|
|
1531
|
+
csvStringToJson(csvString) {
|
|
1532
|
+
return this.csvToJson(csvString);
|
|
1533
|
+
}
|
|
1534
|
+
csvStringToJsonStringified(csvString) {
|
|
1535
|
+
let json3 = this.csvStringToJson(csvString);
|
|
1536
|
+
let jsonStringified = JSON.stringify(json3, undefined, 1);
|
|
1537
|
+
jsonUtils.validateJson(jsonStringified);
|
|
1538
|
+
return jsonStringified;
|
|
1539
|
+
}
|
|
1540
|
+
csvToJson(parsedCsv) {
|
|
1541
|
+
this.validateInputConfig();
|
|
1542
|
+
let lines = parsedCsv.split(newLine);
|
|
1543
|
+
let fieldDelimiter = this.getFieldDelimiter();
|
|
1544
|
+
let index = this.getIndexHeader();
|
|
1545
|
+
let headers;
|
|
1546
|
+
if (this.isSupportQuotedField) {
|
|
1547
|
+
headers = this.split(lines[index]);
|
|
1548
|
+
} else {
|
|
1549
|
+
headers = lines[index].split(fieldDelimiter);
|
|
1550
|
+
}
|
|
1551
|
+
while (!stringUtils.hasContent(headers) && index <= lines.length) {
|
|
1552
|
+
index = index + 1;
|
|
1553
|
+
headers = lines[index].split(fieldDelimiter);
|
|
1554
|
+
}
|
|
1555
|
+
let jsonResult = [];
|
|
1556
|
+
for (let i2 = index + 1;i2 < lines.length; i2++) {
|
|
1557
|
+
let currentLine;
|
|
1558
|
+
if (this.isSupportQuotedField) {
|
|
1559
|
+
currentLine = this.split(lines[i2]);
|
|
1560
|
+
} else {
|
|
1561
|
+
currentLine = lines[i2].split(fieldDelimiter);
|
|
1562
|
+
}
|
|
1563
|
+
if (stringUtils.hasContent(currentLine)) {
|
|
1564
|
+
jsonResult.push(this.buildJsonResult(headers, currentLine));
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
return jsonResult;
|
|
1568
|
+
}
|
|
1569
|
+
getFieldDelimiter() {
|
|
1570
|
+
if (this.delimiter) {
|
|
1571
|
+
return this.delimiter;
|
|
1572
|
+
}
|
|
1573
|
+
return defaultFieldDelimiter;
|
|
1574
|
+
}
|
|
1575
|
+
getIndexHeader() {
|
|
1576
|
+
if (this.indexHeaderValue !== null && !isNaN(this.indexHeaderValue)) {
|
|
1577
|
+
return this.indexHeaderValue;
|
|
1578
|
+
}
|
|
1579
|
+
return 0;
|
|
1580
|
+
}
|
|
1581
|
+
buildJsonResult(headers, currentLine) {
|
|
1582
|
+
let jsonObject = {};
|
|
1583
|
+
for (let j = 0;j < headers.length; j++) {
|
|
1584
|
+
let propertyName = stringUtils.trimPropertyName(this.isTrimHeaderFieldWhiteSpace, headers[j]);
|
|
1585
|
+
let value = currentLine[j];
|
|
1586
|
+
if (this.isParseSubArray(value)) {
|
|
1587
|
+
value = this.buildJsonSubArray(value);
|
|
1588
|
+
}
|
|
1589
|
+
if (this.printValueFormatByType && !Array.isArray(value)) {
|
|
1590
|
+
value = stringUtils.getValueFormatByType(currentLine[j]);
|
|
1591
|
+
}
|
|
1592
|
+
jsonObject[propertyName] = value;
|
|
1593
|
+
}
|
|
1594
|
+
return jsonObject;
|
|
1595
|
+
}
|
|
1596
|
+
buildJsonSubArray(value) {
|
|
1597
|
+
let extractedValues = value.substring(value.indexOf(this.parseSubArrayDelimiter) + 1, value.lastIndexOf(this.parseSubArrayDelimiter));
|
|
1598
|
+
extractedValues.trim();
|
|
1599
|
+
value = extractedValues.split(this.parseSubArraySeparator);
|
|
1600
|
+
if (this.printValueFormatByType) {
|
|
1601
|
+
for (let i2 = 0;i2 < value.length; i2++) {
|
|
1602
|
+
value[i2] = stringUtils.getValueFormatByType(value[i2]);
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
return value;
|
|
1606
|
+
}
|
|
1607
|
+
isParseSubArray(value) {
|
|
1608
|
+
if (this.parseSubArrayDelimiter) {
|
|
1609
|
+
if (value && (value.indexOf(this.parseSubArrayDelimiter) === 0 && value.lastIndexOf(this.parseSubArrayDelimiter) === value.length - 1)) {
|
|
1610
|
+
return true;
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
return false;
|
|
1614
|
+
}
|
|
1615
|
+
validateInputConfig() {
|
|
1616
|
+
if (this.isSupportQuotedField) {
|
|
1617
|
+
if (this.getFieldDelimiter() === '"') {
|
|
1618
|
+
throw new Error('When SupportQuotedFields is enabled you cannot defined the field delimiter as quote -> ["]');
|
|
1619
|
+
}
|
|
1620
|
+
if (this.parseSubArraySeparator === '"') {
|
|
1621
|
+
throw new Error('When SupportQuotedFields is enabled you cannot defined the field parseSubArraySeparator as quote -> ["]');
|
|
1622
|
+
}
|
|
1623
|
+
if (this.parseSubArrayDelimiter === '"') {
|
|
1624
|
+
throw new Error('When SupportQuotedFields is enabled you cannot defined the field parseSubArrayDelimiter as quote -> ["]');
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
hasQuotes(line) {
|
|
1629
|
+
return line.includes('"');
|
|
1630
|
+
}
|
|
1631
|
+
split(line) {
|
|
1632
|
+
if (line.length == 0) {
|
|
1633
|
+
return [];
|
|
1634
|
+
}
|
|
1635
|
+
let delim = this.getFieldDelimiter();
|
|
1636
|
+
let subSplits = [""];
|
|
1637
|
+
if (this.hasQuotes(line)) {
|
|
1638
|
+
let chars = line.split("");
|
|
1639
|
+
let subIndex = 0;
|
|
1640
|
+
let startQuote = false;
|
|
1641
|
+
let isDouble = false;
|
|
1642
|
+
chars.forEach((c, i2, arr) => {
|
|
1643
|
+
if (isDouble) {
|
|
1644
|
+
subSplits[subIndex] += c;
|
|
1645
|
+
isDouble = false;
|
|
1646
|
+
return;
|
|
1647
|
+
}
|
|
1648
|
+
if (c != '"' && c != delim) {
|
|
1649
|
+
subSplits[subIndex] += c;
|
|
1650
|
+
} else if (c == delim && startQuote) {
|
|
1651
|
+
subSplits[subIndex] += c;
|
|
1652
|
+
} else if (c == delim) {
|
|
1653
|
+
subIndex++;
|
|
1654
|
+
subSplits[subIndex] = "";
|
|
1655
|
+
return;
|
|
1656
|
+
} else {
|
|
1657
|
+
if (arr[i2 + 1] === '"') {
|
|
1658
|
+
isDouble = true;
|
|
1659
|
+
} else {
|
|
1660
|
+
if (!startQuote) {
|
|
1661
|
+
startQuote = true;
|
|
1662
|
+
} else {
|
|
1663
|
+
startQuote = false;
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
});
|
|
1668
|
+
if (startQuote) {
|
|
1669
|
+
throw new Error("Row contains mismatched quotes!");
|
|
1670
|
+
}
|
|
1671
|
+
return subSplits;
|
|
1672
|
+
} else {
|
|
1673
|
+
return line.split(delim);
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
module.exports = new CsvToJson;
|
|
1678
|
+
});
|
|
1679
|
+
|
|
1680
|
+
// node_modules/convert-csv-to-json/src/csvToJsonAsync.js
|
|
1681
|
+
var require_csvToJsonAsync = __commonJS((exports, module) => {
|
|
1682
|
+
var fileUtils = require_fileUtils();
|
|
1683
|
+
var csvToJson = require_csvToJson();
|
|
1684
|
+
|
|
1685
|
+
class CsvToJsonAsync {
|
|
1686
|
+
constructor() {
|
|
1687
|
+
this.csvToJson = csvToJson;
|
|
1688
|
+
}
|
|
1689
|
+
formatValueByType(active) {
|
|
1690
|
+
this.csvToJson.formatValueByType(active);
|
|
1691
|
+
return this;
|
|
1692
|
+
}
|
|
1693
|
+
supportQuotedField(active) {
|
|
1694
|
+
this.csvToJson.supportQuotedField(active);
|
|
1695
|
+
return this;
|
|
1696
|
+
}
|
|
1697
|
+
fieldDelimiter(delimiter) {
|
|
1698
|
+
this.csvToJson.fieldDelimiter(delimiter);
|
|
1699
|
+
return this;
|
|
1700
|
+
}
|
|
1701
|
+
trimHeaderFieldWhiteSpace(active) {
|
|
1702
|
+
this.csvToJson.trimHeaderFieldWhiteSpace(active);
|
|
1703
|
+
return this;
|
|
1704
|
+
}
|
|
1705
|
+
indexHeader(indexHeader) {
|
|
1706
|
+
this.csvToJson.indexHeader(indexHeader);
|
|
1707
|
+
return this;
|
|
1708
|
+
}
|
|
1709
|
+
parseSubArray(delimiter = "*", separator = ",") {
|
|
1710
|
+
this.csvToJson.parseSubArray(delimiter, separator);
|
|
1711
|
+
return this;
|
|
1712
|
+
}
|
|
1713
|
+
encoding(encoding) {
|
|
1714
|
+
this.csvToJson.encoding = encoding;
|
|
1715
|
+
return this;
|
|
1716
|
+
}
|
|
1717
|
+
async generateJsonFileFromCsv(fileInputName, fileOutputName) {
|
|
1718
|
+
const jsonStringified = await this.getJsonFromCsvStringified(fileInputName);
|
|
1719
|
+
await fileUtils.writeFileAsync(jsonStringified, fileOutputName);
|
|
1720
|
+
}
|
|
1721
|
+
async getJsonFromCsvStringified(fileInputName) {
|
|
1722
|
+
const json3 = await this.getJsonFromCsvAsync(fileInputName);
|
|
1723
|
+
return JSON.stringify(json3, undefined, 1);
|
|
1724
|
+
}
|
|
1725
|
+
async getJsonFromCsvAsync(inputFileNameOrCsv, options = {}) {
|
|
1726
|
+
if (inputFileNameOrCsv === null || inputFileNameOrCsv === undefined) {
|
|
1727
|
+
throw new Error("inputFileNameOrCsv is not defined!!!");
|
|
1728
|
+
}
|
|
1729
|
+
if (options.raw) {
|
|
1730
|
+
if (inputFileNameOrCsv === "") {
|
|
1731
|
+
return [];
|
|
1732
|
+
}
|
|
1733
|
+
return this.csvToJson.csvToJson(inputFileNameOrCsv);
|
|
1734
|
+
}
|
|
1735
|
+
const parsedCsv = await fileUtils.readFileAsync(inputFileNameOrCsv, this.csvToJson.encoding || "utf8");
|
|
1736
|
+
return this.csvToJson.csvToJson(parsedCsv);
|
|
1737
|
+
}
|
|
1738
|
+
csvStringToJsonAsync(csvString, options = { raw: true }) {
|
|
1739
|
+
return this.getJsonFromCsvAsync(csvString, options);
|
|
1740
|
+
}
|
|
1741
|
+
async csvStringToJsonStringifiedAsync(csvString) {
|
|
1742
|
+
const json3 = await this.csvStringToJsonAsync(csvString);
|
|
1743
|
+
return JSON.stringify(json3, undefined, 1);
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
module.exports = new CsvToJsonAsync;
|
|
1747
|
+
});
|
|
1748
|
+
|
|
1749
|
+
// node_modules/convert-csv-to-json/src/browserApi.js
|
|
1750
|
+
var require_browserApi = __commonJS((exports, module) => {
|
|
1751
|
+
var csvToJson = require_csvToJson();
|
|
1752
|
+
|
|
1753
|
+
class BrowserApi {
|
|
1754
|
+
constructor() {
|
|
1755
|
+
this.csvToJson = csvToJson;
|
|
1756
|
+
}
|
|
1757
|
+
formatValueByType(active = true) {
|
|
1758
|
+
this.csvToJson.formatValueByType(active);
|
|
1759
|
+
return this;
|
|
1760
|
+
}
|
|
1761
|
+
supportQuotedField(active = false) {
|
|
1762
|
+
this.csvToJson.supportQuotedField(active);
|
|
1763
|
+
return this;
|
|
1764
|
+
}
|
|
1765
|
+
fieldDelimiter(delimiter) {
|
|
1766
|
+
this.csvToJson.fieldDelimiter(delimiter);
|
|
1767
|
+
return this;
|
|
1768
|
+
}
|
|
1769
|
+
trimHeaderFieldWhiteSpace(active = false) {
|
|
1770
|
+
this.csvToJson.trimHeaderFieldWhiteSpace(active);
|
|
1771
|
+
return this;
|
|
1772
|
+
}
|
|
1773
|
+
indexHeader(index) {
|
|
1774
|
+
this.csvToJson.indexHeader(index);
|
|
1775
|
+
return this;
|
|
1776
|
+
}
|
|
1777
|
+
parseSubArray(delimiter = "*", separator = ",") {
|
|
1778
|
+
this.csvToJson.parseSubArray(delimiter, separator);
|
|
1779
|
+
return this;
|
|
1780
|
+
}
|
|
1781
|
+
csvStringToJson(csvString) {
|
|
1782
|
+
if (csvString === undefined || csvString === null) {
|
|
1783
|
+
throw new Error("csvString is not defined!!!");
|
|
1784
|
+
}
|
|
1785
|
+
return this.csvToJson.csvToJson(csvString);
|
|
1786
|
+
}
|
|
1787
|
+
csvStringToJsonStringified(csvString) {
|
|
1788
|
+
if (csvString === undefined || csvString === null) {
|
|
1789
|
+
throw new Error("csvString is not defined!!!");
|
|
1790
|
+
}
|
|
1791
|
+
return this.csvToJson.csvStringToJsonStringified(csvString);
|
|
1792
|
+
}
|
|
1793
|
+
csvStringToJsonAsync(csvString) {
|
|
1794
|
+
return Promise.resolve(this.csvStringToJson(csvString));
|
|
1795
|
+
}
|
|
1796
|
+
csvStringToJsonStringifiedAsync(csvString) {
|
|
1797
|
+
return Promise.resolve(this.csvStringToJsonStringified(csvString));
|
|
1798
|
+
}
|
|
1799
|
+
parseFile(file2, options = {}) {
|
|
1800
|
+
if (!file2) {
|
|
1801
|
+
return Promise.reject(new Error("file is not defined!!!"));
|
|
1802
|
+
}
|
|
1803
|
+
return new Promise((resolve2, reject) => {
|
|
1804
|
+
if (typeof FileReader === "undefined") {
|
|
1805
|
+
reject(new Error("FileReader is not available in this environment"));
|
|
1806
|
+
return;
|
|
1807
|
+
}
|
|
1808
|
+
const reader = new FileReader;
|
|
1809
|
+
reader.onerror = () => reject(reader.error || new Error("Failed to read file"));
|
|
1810
|
+
reader.onload = () => {
|
|
1811
|
+
try {
|
|
1812
|
+
const text = reader.result;
|
|
1813
|
+
const result = this.csvToJson.csvToJson(String(text));
|
|
1814
|
+
resolve2(result);
|
|
1815
|
+
} catch (err) {
|
|
1816
|
+
reject(err);
|
|
1817
|
+
}
|
|
1818
|
+
};
|
|
1819
|
+
if (options.encoding) {
|
|
1820
|
+
reader.readAsText(file2, options.encoding);
|
|
1821
|
+
} else {
|
|
1822
|
+
reader.readAsText(file2);
|
|
1823
|
+
}
|
|
1824
|
+
});
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
module.exports = new BrowserApi;
|
|
1828
|
+
});
|
|
1829
|
+
|
|
1830
|
+
// node_modules/convert-csv-to-json/index.js
|
|
1831
|
+
var require_convert_csv_to_json = __commonJS((exports) => {
|
|
1832
|
+
var csvToJson = require_csvToJson();
|
|
1833
|
+
var encodingOps = {
|
|
1834
|
+
utf8: "utf8",
|
|
1835
|
+
ucs2: "ucs2",
|
|
1836
|
+
utf16le: "utf16le",
|
|
1837
|
+
latin1: "latin1",
|
|
1838
|
+
ascii: "ascii",
|
|
1839
|
+
base64: "base64",
|
|
1840
|
+
hex: "hex"
|
|
1841
|
+
};
|
|
1842
|
+
exports.formatValueByType = function(active = true) {
|
|
1843
|
+
csvToJson.formatValueByType(active);
|
|
1844
|
+
return this;
|
|
1845
|
+
};
|
|
1846
|
+
exports.supportQuotedField = function(active = false) {
|
|
1847
|
+
csvToJson.supportQuotedField(active);
|
|
1848
|
+
return this;
|
|
1849
|
+
};
|
|
1850
|
+
exports.fieldDelimiter = function(delimiter) {
|
|
1851
|
+
csvToJson.fieldDelimiter(delimiter);
|
|
1852
|
+
return this;
|
|
1853
|
+
};
|
|
1854
|
+
exports.trimHeaderFieldWhiteSpace = function(active = false) {
|
|
1855
|
+
csvToJson.trimHeaderFieldWhiteSpace(active);
|
|
1856
|
+
return this;
|
|
1857
|
+
};
|
|
1858
|
+
exports.indexHeader = function(index) {
|
|
1859
|
+
csvToJson.indexHeader(index);
|
|
1860
|
+
return this;
|
|
1861
|
+
};
|
|
1862
|
+
exports.parseSubArray = function(delimiter, separator) {
|
|
1863
|
+
csvToJson.parseSubArray(delimiter, separator);
|
|
1864
|
+
return this;
|
|
1865
|
+
};
|
|
1866
|
+
exports.customEncoding = function(encoding) {
|
|
1867
|
+
csvToJson.encoding = encoding;
|
|
1868
|
+
return this;
|
|
1869
|
+
};
|
|
1870
|
+
exports.utf8Encoding = function utf8Encoding() {
|
|
1871
|
+
csvToJson.encoding = encodingOps.utf8;
|
|
1872
|
+
return this;
|
|
1873
|
+
};
|
|
1874
|
+
exports.ucs2Encoding = function() {
|
|
1875
|
+
csvToJson.encoding = encodingOps.ucs2;
|
|
1876
|
+
return this;
|
|
1877
|
+
};
|
|
1878
|
+
exports.utf16leEncoding = function() {
|
|
1879
|
+
csvToJson.encoding = encodingOps.utf16le;
|
|
1880
|
+
return this;
|
|
1881
|
+
};
|
|
1882
|
+
exports.latin1Encoding = function() {
|
|
1883
|
+
csvToJson.encoding = encodingOps.latin1;
|
|
1884
|
+
return this;
|
|
1885
|
+
};
|
|
1886
|
+
exports.asciiEncoding = function() {
|
|
1887
|
+
csvToJson.encoding = encodingOps.ascii;
|
|
1888
|
+
return this;
|
|
1889
|
+
};
|
|
1890
|
+
exports.base64Encoding = function() {
|
|
1891
|
+
this.csvToJson = encodingOps.base64;
|
|
1892
|
+
return this;
|
|
1893
|
+
};
|
|
1894
|
+
exports.hexEncoding = function() {
|
|
1895
|
+
this.csvToJson = encodingOps.hex;
|
|
1896
|
+
return this;
|
|
1897
|
+
};
|
|
1898
|
+
exports.generateJsonFileFromCsv = function(inputFileName, outputFileName) {
|
|
1899
|
+
if (!inputFileName) {
|
|
1900
|
+
throw new Error("inputFileName is not defined!!!");
|
|
1901
|
+
}
|
|
1902
|
+
if (!outputFileName) {
|
|
1903
|
+
throw new Error("outputFileName is not defined!!!");
|
|
1904
|
+
}
|
|
1905
|
+
csvToJson.generateJsonFileFromCsv(inputFileName, outputFileName);
|
|
1906
|
+
};
|
|
1907
|
+
exports.getJsonFromCsv = function(inputFileName) {
|
|
1908
|
+
if (!inputFileName) {
|
|
1909
|
+
throw new Error("inputFileName is not defined!!!");
|
|
1910
|
+
}
|
|
1911
|
+
return csvToJson.getJsonFromCsv(inputFileName);
|
|
1912
|
+
};
|
|
1913
|
+
var csvToJsonAsync = require_csvToJsonAsync();
|
|
1914
|
+
Object.assign(exports, {
|
|
1915
|
+
getJsonFromCsvAsync: function(input, options) {
|
|
1916
|
+
return csvToJsonAsync.getJsonFromCsvAsync(input, options);
|
|
1917
|
+
},
|
|
1918
|
+
csvStringToJsonAsync: function(input, options) {
|
|
1919
|
+
return csvToJsonAsync.csvStringToJsonAsync(input, options);
|
|
1920
|
+
},
|
|
1921
|
+
csvStringToJsonStringifiedAsync: function(input) {
|
|
1922
|
+
return csvToJsonAsync.csvStringToJsonStringifiedAsync(input);
|
|
1923
|
+
},
|
|
1924
|
+
generateJsonFileFromCsvAsync: function(input, output) {
|
|
1925
|
+
return csvToJsonAsync.generateJsonFileFromCsv(input, output);
|
|
1926
|
+
}
|
|
1927
|
+
});
|
|
1928
|
+
exports.csvStringToJson = function(csvString) {
|
|
1929
|
+
return csvToJson.csvStringToJson(csvString);
|
|
1930
|
+
};
|
|
1931
|
+
exports.csvStringToJsonStringified = function(csvString) {
|
|
1932
|
+
if (csvString === undefined || csvString === null) {
|
|
1933
|
+
throw new Error("csvString is not defined!!!");
|
|
1934
|
+
}
|
|
1935
|
+
return csvToJson.csvStringToJsonStringified(csvString);
|
|
1936
|
+
};
|
|
1937
|
+
exports.jsonToCsv = function(inputFileName, outputFileName) {
|
|
1938
|
+
csvToJson.generateJsonFileFromCsv(inputFileName, outputFileName);
|
|
1939
|
+
};
|
|
1940
|
+
exports.browser = require_browserApi();
|
|
1941
|
+
});
|
|
1942
|
+
|
|
1343
1943
|
// src/index.ts
|
|
1344
|
-
import { dirname as
|
|
1944
|
+
import { dirname as dirname4, join as join7 } from "path";
|
|
1345
1945
|
import { fileURLToPath } from "url";
|
|
1346
1946
|
|
|
1347
1947
|
// src/utils/agentLoader.ts
|
|
@@ -16584,7 +17184,13 @@ import * as fs4 from "fs";
|
|
|
16584
17184
|
import * as fs3 from "fs";
|
|
16585
17185
|
import * as path3 from "path";
|
|
16586
17186
|
var CONFIG_FILE2 = "config/import/providers.yaml";
|
|
16587
|
-
var REQUIRED_PATH_FIELDS = [
|
|
17187
|
+
var REQUIRED_PATH_FIELDS = [
|
|
17188
|
+
"import",
|
|
17189
|
+
"pending",
|
|
17190
|
+
"done",
|
|
17191
|
+
"unrecognized",
|
|
17192
|
+
"rules"
|
|
17193
|
+
];
|
|
16588
17194
|
var REQUIRED_DETECTION_FIELDS = ["header", "currencyField"];
|
|
16589
17195
|
function validatePaths(paths) {
|
|
16590
17196
|
if (typeof paths !== "object" || paths === null) {
|
|
@@ -16600,7 +17206,8 @@ function validatePaths(paths) {
|
|
|
16600
17206
|
import: pathsObj.import,
|
|
16601
17207
|
pending: pathsObj.pending,
|
|
16602
17208
|
done: pathsObj.done,
|
|
16603
|
-
unrecognized: pathsObj.unrecognized
|
|
17209
|
+
unrecognized: pathsObj.unrecognized,
|
|
17210
|
+
rules: pathsObj.rules
|
|
16604
17211
|
};
|
|
16605
17212
|
}
|
|
16606
17213
|
function validateDetectionRule(providerName, index, rule) {
|
|
@@ -16968,15 +17575,600 @@ var classify_statements_default = tool({
|
|
|
16968
17575
|
return classifyStatementsCore(directory, agent);
|
|
16969
17576
|
}
|
|
16970
17577
|
});
|
|
17578
|
+
// src/tools/import-statements.ts
|
|
17579
|
+
import * as fs7 from "fs";
|
|
17580
|
+
import * as path6 from "path";
|
|
17581
|
+
|
|
17582
|
+
// src/utils/rulesMatcher.ts
|
|
17583
|
+
import * as fs5 from "fs";
|
|
17584
|
+
import * as path5 from "path";
|
|
17585
|
+
function parseSourceDirective(content) {
|
|
17586
|
+
const match = content.match(/^source\s+([^\n#]+)/m);
|
|
17587
|
+
if (!match) {
|
|
17588
|
+
return null;
|
|
17589
|
+
}
|
|
17590
|
+
return match[1].trim();
|
|
17591
|
+
}
|
|
17592
|
+
function resolveSourcePath(sourcePath, rulesFilePath) {
|
|
17593
|
+
if (path5.isAbsolute(sourcePath)) {
|
|
17594
|
+
return sourcePath;
|
|
17595
|
+
}
|
|
17596
|
+
const rulesDir = path5.dirname(rulesFilePath);
|
|
17597
|
+
return path5.resolve(rulesDir, sourcePath);
|
|
17598
|
+
}
|
|
17599
|
+
function loadRulesMapping(rulesDir) {
|
|
17600
|
+
const mapping = {};
|
|
17601
|
+
if (!fs5.existsSync(rulesDir)) {
|
|
17602
|
+
return mapping;
|
|
17603
|
+
}
|
|
17604
|
+
const files = fs5.readdirSync(rulesDir);
|
|
17605
|
+
for (const file2 of files) {
|
|
17606
|
+
if (!file2.endsWith(".rules")) {
|
|
17607
|
+
continue;
|
|
17608
|
+
}
|
|
17609
|
+
const rulesFilePath = path5.join(rulesDir, file2);
|
|
17610
|
+
const stat = fs5.statSync(rulesFilePath);
|
|
17611
|
+
if (!stat.isFile()) {
|
|
17612
|
+
continue;
|
|
17613
|
+
}
|
|
17614
|
+
const content = fs5.readFileSync(rulesFilePath, "utf-8");
|
|
17615
|
+
const sourcePath = parseSourceDirective(content);
|
|
17616
|
+
if (!sourcePath) {
|
|
17617
|
+
continue;
|
|
17618
|
+
}
|
|
17619
|
+
const absoluteCsvPath = resolveSourcePath(sourcePath, rulesFilePath);
|
|
17620
|
+
mapping[absoluteCsvPath] = rulesFilePath;
|
|
17621
|
+
}
|
|
17622
|
+
return mapping;
|
|
17623
|
+
}
|
|
17624
|
+
function findRulesForCsv(csvPath, mapping) {
|
|
17625
|
+
if (mapping[csvPath]) {
|
|
17626
|
+
return mapping[csvPath];
|
|
17627
|
+
}
|
|
17628
|
+
const normalizedCsvPath = path5.normalize(csvPath);
|
|
17629
|
+
for (const [mappedCsv, rulesFile] of Object.entries(mapping)) {
|
|
17630
|
+
if (path5.normalize(mappedCsv) === normalizedCsvPath) {
|
|
17631
|
+
return rulesFile;
|
|
17632
|
+
}
|
|
17633
|
+
}
|
|
17634
|
+
return null;
|
|
17635
|
+
}
|
|
17636
|
+
|
|
17637
|
+
// src/utils/hledgerExecutor.ts
|
|
17638
|
+
var {$: $2 } = globalThis.Bun;
|
|
17639
|
+
async function defaultHledgerExecutor(cmdArgs) {
|
|
17640
|
+
try {
|
|
17641
|
+
const result = await $2`hledger ${cmdArgs}`.quiet().nothrow();
|
|
17642
|
+
return {
|
|
17643
|
+
stdout: result.stdout.toString(),
|
|
17644
|
+
stderr: result.stderr.toString(),
|
|
17645
|
+
exitCode: result.exitCode
|
|
17646
|
+
};
|
|
17647
|
+
} catch (error45) {
|
|
17648
|
+
return {
|
|
17649
|
+
stdout: "",
|
|
17650
|
+
stderr: error45 instanceof Error ? error45.message : String(error45),
|
|
17651
|
+
exitCode: 1
|
|
17652
|
+
};
|
|
17653
|
+
}
|
|
17654
|
+
}
|
|
17655
|
+
function parseUnknownPostings(hledgerOutput) {
|
|
17656
|
+
const unknownPostings = [];
|
|
17657
|
+
const lines = hledgerOutput.split(`
|
|
17658
|
+
`);
|
|
17659
|
+
let currentDate = "";
|
|
17660
|
+
let currentDescription = "";
|
|
17661
|
+
for (const line of lines) {
|
|
17662
|
+
const headerMatch = line.match(/^(\d{4}-\d{2}-\d{2})\s+(.+)$/);
|
|
17663
|
+
if (headerMatch) {
|
|
17664
|
+
currentDate = headerMatch[1];
|
|
17665
|
+
currentDescription = headerMatch[2].trim();
|
|
17666
|
+
continue;
|
|
17667
|
+
}
|
|
17668
|
+
const postingMatch = line.match(/^\s+(income:unknown|expenses:unknown)\s+([^\s]+(?:\s+[^\s=]+)?)\s*(?:=\s*(.+))?$/);
|
|
17669
|
+
if (postingMatch && currentDate) {
|
|
17670
|
+
unknownPostings.push({
|
|
17671
|
+
date: currentDate,
|
|
17672
|
+
description: currentDescription,
|
|
17673
|
+
amount: postingMatch[2].trim(),
|
|
17674
|
+
account: postingMatch[1],
|
|
17675
|
+
balance: postingMatch[3]?.trim()
|
|
17676
|
+
});
|
|
17677
|
+
}
|
|
17678
|
+
}
|
|
17679
|
+
return unknownPostings;
|
|
17680
|
+
}
|
|
17681
|
+
function countTransactions(hledgerOutput) {
|
|
17682
|
+
const lines = hledgerOutput.split(`
|
|
17683
|
+
`);
|
|
17684
|
+
let count = 0;
|
|
17685
|
+
for (const line of lines) {
|
|
17686
|
+
if (/^\d{4}-\d{2}-\d{2}\s+/.test(line)) {
|
|
17687
|
+
count++;
|
|
17688
|
+
}
|
|
17689
|
+
}
|
|
17690
|
+
return count;
|
|
17691
|
+
}
|
|
17692
|
+
|
|
17693
|
+
// src/utils/rulesParser.ts
|
|
17694
|
+
function parseSkipRows(rulesContent) {
|
|
17695
|
+
const match = rulesContent.match(/^skip\s+(\d+)/m);
|
|
17696
|
+
return match ? parseInt(match[1], 10) : 0;
|
|
17697
|
+
}
|
|
17698
|
+
function parseSeparator(rulesContent) {
|
|
17699
|
+
const match = rulesContent.match(/^separator\s+(.)/m);
|
|
17700
|
+
return match ? match[1] : ",";
|
|
17701
|
+
}
|
|
17702
|
+
function parseFieldNames(rulesContent) {
|
|
17703
|
+
const match = rulesContent.match(/^fields\s+(.+)$/m);
|
|
17704
|
+
if (!match) {
|
|
17705
|
+
return [];
|
|
17706
|
+
}
|
|
17707
|
+
return match[1].split(",").map((field) => field.trim());
|
|
17708
|
+
}
|
|
17709
|
+
function parseDateFormat(rulesContent) {
|
|
17710
|
+
const match = rulesContent.match(/^date-format\s+(.+)$/m);
|
|
17711
|
+
return match ? match[1].trim() : "%Y-%m-%d";
|
|
17712
|
+
}
|
|
17713
|
+
function parseDateField(rulesContent, fieldNames) {
|
|
17714
|
+
const match = rulesContent.match(/^date\s+%(\w+|\d+)/m);
|
|
17715
|
+
if (!match) {
|
|
17716
|
+
return fieldNames[0] || "date";
|
|
17717
|
+
}
|
|
17718
|
+
const value = match[1];
|
|
17719
|
+
if (/^\d+$/.test(value)) {
|
|
17720
|
+
const index = parseInt(value, 10) - 1;
|
|
17721
|
+
return fieldNames[index] || value;
|
|
17722
|
+
}
|
|
17723
|
+
return value;
|
|
17724
|
+
}
|
|
17725
|
+
function parseAmountFields(rulesContent, fieldNames) {
|
|
17726
|
+
const result = {};
|
|
17727
|
+
const simpleMatch = rulesContent.match(/^amount\s+(-?)%(\w+|\d+)/m);
|
|
17728
|
+
if (simpleMatch) {
|
|
17729
|
+
const fieldRef = simpleMatch[2];
|
|
17730
|
+
if (/^\d+$/.test(fieldRef)) {
|
|
17731
|
+
const index = parseInt(fieldRef, 10) - 1;
|
|
17732
|
+
result.single = fieldNames[index] || fieldRef;
|
|
17733
|
+
} else {
|
|
17734
|
+
result.single = fieldRef;
|
|
17735
|
+
}
|
|
17736
|
+
}
|
|
17737
|
+
const debitMatch = rulesContent.match(/if\s+%(\w+)\s+\.\s*\n\s*amount\s+-?%\1/m);
|
|
17738
|
+
if (debitMatch) {
|
|
17739
|
+
result.debit = debitMatch[1];
|
|
17740
|
+
}
|
|
17741
|
+
const creditMatch = rulesContent.match(/if\s+%(\w+)\s+\.\s*\n\s*amount\s+%\1(?!\w)/m);
|
|
17742
|
+
if (creditMatch && creditMatch[1] !== result.debit) {
|
|
17743
|
+
result.credit = creditMatch[1];
|
|
17744
|
+
}
|
|
17745
|
+
if (result.debit || result.credit) {
|
|
17746
|
+
delete result.single;
|
|
17747
|
+
}
|
|
17748
|
+
if (!result.single && !result.debit && !result.credit) {
|
|
17749
|
+
result.single = "amount";
|
|
17750
|
+
}
|
|
17751
|
+
return result;
|
|
17752
|
+
}
|
|
17753
|
+
function parseRulesFile(rulesContent) {
|
|
17754
|
+
const fieldNames = parseFieldNames(rulesContent);
|
|
17755
|
+
return {
|
|
17756
|
+
skipRows: parseSkipRows(rulesContent),
|
|
17757
|
+
separator: parseSeparator(rulesContent),
|
|
17758
|
+
fieldNames,
|
|
17759
|
+
dateFormat: parseDateFormat(rulesContent),
|
|
17760
|
+
dateField: parseDateField(rulesContent, fieldNames),
|
|
17761
|
+
amountFields: parseAmountFields(rulesContent, fieldNames)
|
|
17762
|
+
};
|
|
17763
|
+
}
|
|
17764
|
+
|
|
17765
|
+
// src/utils/csvParser.ts
|
|
17766
|
+
var import_convert_csv_to_json = __toESM(require_convert_csv_to_json(), 1);
|
|
17767
|
+
import * as fs6 from "fs";
|
|
17768
|
+
function parseCsvFile(csvPath, config2) {
|
|
17769
|
+
const csvContent = fs6.readFileSync(csvPath, "utf-8");
|
|
17770
|
+
const lines = csvContent.split(`
|
|
17771
|
+
`);
|
|
17772
|
+
const headerIndex = config2.skipRows;
|
|
17773
|
+
if (headerIndex >= lines.length) {
|
|
17774
|
+
return [];
|
|
17775
|
+
}
|
|
17776
|
+
const headerLine = lines[headerIndex];
|
|
17777
|
+
const dataLines = lines.slice(headerIndex + 1).filter((line) => line.trim() !== "");
|
|
17778
|
+
const csvWithHeader = [headerLine, ...dataLines].join(`
|
|
17779
|
+
`);
|
|
17780
|
+
const rawRows = import_convert_csv_to_json.default.indexHeader(0).fieldDelimiter(config2.separator).supportQuotedField(true).csvStringToJson(csvWithHeader);
|
|
17781
|
+
const fieldNames = config2.fieldNames.length > 0 ? config2.fieldNames : Object.keys(rawRows[0] || {});
|
|
17782
|
+
const mappedRows = [];
|
|
17783
|
+
for (const parsedRow of rawRows) {
|
|
17784
|
+
const row = {};
|
|
17785
|
+
const values = Object.values(parsedRow);
|
|
17786
|
+
for (let i2 = 0;i2 < fieldNames.length && i2 < values.length; i2++) {
|
|
17787
|
+
row[fieldNames[i2]] = values[i2];
|
|
17788
|
+
}
|
|
17789
|
+
mappedRows.push(row);
|
|
17790
|
+
}
|
|
17791
|
+
return mappedRows;
|
|
17792
|
+
}
|
|
17793
|
+
function parseAmountValue(amountStr) {
|
|
17794
|
+
const cleaned = amountStr.replace(/[A-Z]{3}\s*/g, "").trim();
|
|
17795
|
+
return parseFloat(cleaned) || 0;
|
|
17796
|
+
}
|
|
17797
|
+
function getRowAmount(row, amountFields) {
|
|
17798
|
+
if (amountFields.single) {
|
|
17799
|
+
return parseAmountValue(row[amountFields.single] || "0");
|
|
17800
|
+
}
|
|
17801
|
+
const debitValue = amountFields.debit ? parseAmountValue(row[amountFields.debit] || "0") : 0;
|
|
17802
|
+
const creditValue = amountFields.credit ? parseAmountValue(row[amountFields.credit] || "0") : 0;
|
|
17803
|
+
if (debitValue !== 0) {
|
|
17804
|
+
return -Math.abs(debitValue);
|
|
17805
|
+
}
|
|
17806
|
+
if (creditValue !== 0) {
|
|
17807
|
+
return Math.abs(creditValue);
|
|
17808
|
+
}
|
|
17809
|
+
return 0;
|
|
17810
|
+
}
|
|
17811
|
+
function parseDateToIso(dateStr, dateFormat) {
|
|
17812
|
+
if (!dateStr)
|
|
17813
|
+
return "";
|
|
17814
|
+
if (dateFormat === "%Y-%m-%d" || dateFormat === "%F") {
|
|
17815
|
+
return dateStr.trim();
|
|
17816
|
+
}
|
|
17817
|
+
if (dateFormat === "%d.%m.%Y") {
|
|
17818
|
+
const parts = dateStr.split(".");
|
|
17819
|
+
if (parts.length === 3) {
|
|
17820
|
+
return `${parts[2]}-${parts[1].padStart(2, "0")}-${parts[0].padStart(2, "0")}`;
|
|
17821
|
+
}
|
|
17822
|
+
}
|
|
17823
|
+
if (dateFormat === "%m/%d/%Y") {
|
|
17824
|
+
const parts = dateStr.split("/");
|
|
17825
|
+
if (parts.length === 3) {
|
|
17826
|
+
return `${parts[2]}-${parts[0].padStart(2, "0")}-${parts[1].padStart(2, "0")}`;
|
|
17827
|
+
}
|
|
17828
|
+
}
|
|
17829
|
+
if (dateFormat === "%d/%m/%Y") {
|
|
17830
|
+
const parts = dateStr.split("/");
|
|
17831
|
+
if (parts.length === 3) {
|
|
17832
|
+
return `${parts[2]}-${parts[1].padStart(2, "0")}-${parts[0].padStart(2, "0")}`;
|
|
17833
|
+
}
|
|
17834
|
+
}
|
|
17835
|
+
return dateStr.trim();
|
|
17836
|
+
}
|
|
17837
|
+
function looksLikeTransactionId(fieldName, value) {
|
|
17838
|
+
if (!value || value.trim() === "")
|
|
17839
|
+
return false;
|
|
17840
|
+
const idFieldPatterns = [
|
|
17841
|
+
/transaction/i,
|
|
17842
|
+
/trans_?no/i,
|
|
17843
|
+
/trans_?id/i,
|
|
17844
|
+
/reference/i,
|
|
17845
|
+
/ref_?no/i,
|
|
17846
|
+
/ref_?id/i,
|
|
17847
|
+
/booking_?id/i,
|
|
17848
|
+
/payment_?id/i,
|
|
17849
|
+
/order_?id/i
|
|
17850
|
+
];
|
|
17851
|
+
const nameMatches = idFieldPatterns.some((pattern) => pattern.test(fieldName));
|
|
17852
|
+
if (!nameMatches)
|
|
17853
|
+
return false;
|
|
17854
|
+
const trimmedValue = value.trim();
|
|
17855
|
+
const looksLikeId = /^[A-Za-z0-9_-]+$/.test(trimmedValue) && trimmedValue.length >= 3;
|
|
17856
|
+
return looksLikeId;
|
|
17857
|
+
}
|
|
17858
|
+
function findTransactionId(row) {
|
|
17859
|
+
for (const [field, value] of Object.entries(row)) {
|
|
17860
|
+
if (looksLikeTransactionId(field, value)) {
|
|
17861
|
+
return { field, value: value.trim() };
|
|
17862
|
+
}
|
|
17863
|
+
}
|
|
17864
|
+
return null;
|
|
17865
|
+
}
|
|
17866
|
+
function findMatchingCsvRow(posting, csvRows, config2) {
|
|
17867
|
+
const postingAmount = parseAmountValue(posting.amount);
|
|
17868
|
+
let candidates = csvRows.filter((row) => {
|
|
17869
|
+
const rowDate = parseDateToIso(row[config2.dateField] || "", config2.dateFormat);
|
|
17870
|
+
const rowAmount = getRowAmount(row, config2.amountFields);
|
|
17871
|
+
if (rowDate !== posting.date)
|
|
17872
|
+
return false;
|
|
17873
|
+
if (Math.abs(rowAmount - postingAmount) > 0.001)
|
|
17874
|
+
return false;
|
|
17875
|
+
return true;
|
|
17876
|
+
});
|
|
17877
|
+
if (candidates.length === 1) {
|
|
17878
|
+
return candidates[0];
|
|
17879
|
+
}
|
|
17880
|
+
if (candidates.length === 0) {
|
|
17881
|
+
throw new Error(`Bug: Could not find CSV row for posting: ${posting.date} ${posting.description} ${posting.amount}. ` + `This indicates a mismatch between hledger output and CSV parsing.`);
|
|
17882
|
+
}
|
|
17883
|
+
for (const candidate of candidates) {
|
|
17884
|
+
const txId = findTransactionId(candidate);
|
|
17885
|
+
if (txId) {
|
|
17886
|
+
const withSameTxId = candidates.filter((row) => row[txId.field] === txId.value);
|
|
17887
|
+
if (withSameTxId.length === 1) {
|
|
17888
|
+
return withSameTxId[0];
|
|
17889
|
+
}
|
|
17890
|
+
}
|
|
17891
|
+
}
|
|
17892
|
+
const descriptionLower = posting.description.toLowerCase();
|
|
17893
|
+
const descMatches = candidates.filter((row) => {
|
|
17894
|
+
return Object.values(row).some((value) => value && value.toLowerCase().includes(descriptionLower));
|
|
17895
|
+
});
|
|
17896
|
+
if (descMatches.length === 1) {
|
|
17897
|
+
return descMatches[0];
|
|
17898
|
+
}
|
|
17899
|
+
if (descMatches.length > 1) {
|
|
17900
|
+
return descMatches[0];
|
|
17901
|
+
}
|
|
17902
|
+
return candidates[0];
|
|
17903
|
+
}
|
|
17904
|
+
|
|
17905
|
+
// src/tools/import-statements.ts
|
|
17906
|
+
function findPendingCsvFiles(pendingDir, provider, currency) {
|
|
17907
|
+
const csvFiles = [];
|
|
17908
|
+
if (!fs7.existsSync(pendingDir)) {
|
|
17909
|
+
return csvFiles;
|
|
17910
|
+
}
|
|
17911
|
+
let searchPath = pendingDir;
|
|
17912
|
+
if (provider) {
|
|
17913
|
+
searchPath = path6.join(searchPath, provider);
|
|
17914
|
+
if (currency) {
|
|
17915
|
+
searchPath = path6.join(searchPath, currency);
|
|
17916
|
+
}
|
|
17917
|
+
}
|
|
17918
|
+
if (!fs7.existsSync(searchPath)) {
|
|
17919
|
+
return csvFiles;
|
|
17920
|
+
}
|
|
17921
|
+
function scanDirectory(directory) {
|
|
17922
|
+
const entries = fs7.readdirSync(directory, { withFileTypes: true });
|
|
17923
|
+
for (const entry of entries) {
|
|
17924
|
+
const fullPath = path6.join(directory, entry.name);
|
|
17925
|
+
if (entry.isDirectory()) {
|
|
17926
|
+
scanDirectory(fullPath);
|
|
17927
|
+
} else if (entry.isFile() && entry.name.endsWith(".csv")) {
|
|
17928
|
+
csvFiles.push(fullPath);
|
|
17929
|
+
}
|
|
17930
|
+
}
|
|
17931
|
+
}
|
|
17932
|
+
scanDirectory(searchPath);
|
|
17933
|
+
return csvFiles.sort();
|
|
17934
|
+
}
|
|
17935
|
+
async function importStatementsCore(directory, agent, options, configLoader = loadImportConfig, hledgerExecutor = defaultHledgerExecutor) {
|
|
17936
|
+
if (agent !== "accountant") {
|
|
17937
|
+
return JSON.stringify({
|
|
17938
|
+
success: false,
|
|
17939
|
+
error: `This tool is restricted to the accountant agent only. Called by: ${agent || "main assistant"}`,
|
|
17940
|
+
hint: "Use: Task(subagent_type='accountant', prompt='import statements')"
|
|
17941
|
+
});
|
|
17942
|
+
}
|
|
17943
|
+
let config2;
|
|
17944
|
+
try {
|
|
17945
|
+
config2 = configLoader(directory);
|
|
17946
|
+
} catch (error45) {
|
|
17947
|
+
return JSON.stringify({
|
|
17948
|
+
success: false,
|
|
17949
|
+
error: `Failed to load configuration: ${error45 instanceof Error ? error45.message : String(error45)}`,
|
|
17950
|
+
hint: 'Ensure config/import/providers.yaml exists with required paths including "rules"'
|
|
17951
|
+
});
|
|
17952
|
+
}
|
|
17953
|
+
const pendingDir = path6.join(directory, config2.paths.pending);
|
|
17954
|
+
const rulesDir = path6.join(directory, config2.paths.rules);
|
|
17955
|
+
const doneDir = path6.join(directory, config2.paths.done);
|
|
17956
|
+
const rulesMapping = loadRulesMapping(rulesDir);
|
|
17957
|
+
const csvFiles = findPendingCsvFiles(pendingDir, options.provider, options.currency);
|
|
17958
|
+
if (csvFiles.length === 0) {
|
|
17959
|
+
return JSON.stringify({
|
|
17960
|
+
success: true,
|
|
17961
|
+
files: [],
|
|
17962
|
+
summary: {
|
|
17963
|
+
filesProcessed: 0,
|
|
17964
|
+
filesWithErrors: 0,
|
|
17965
|
+
filesWithoutRules: 0,
|
|
17966
|
+
totalTransactions: 0,
|
|
17967
|
+
matched: 0,
|
|
17968
|
+
unknown: 0
|
|
17969
|
+
},
|
|
17970
|
+
message: "No CSV files found to process"
|
|
17971
|
+
});
|
|
17972
|
+
}
|
|
17973
|
+
const fileResults = [];
|
|
17974
|
+
let totalTransactions = 0;
|
|
17975
|
+
let totalMatched = 0;
|
|
17976
|
+
let totalUnknown = 0;
|
|
17977
|
+
let filesWithErrors = 0;
|
|
17978
|
+
let filesWithoutRules = 0;
|
|
17979
|
+
for (const csvFile of csvFiles) {
|
|
17980
|
+
const rulesFile = findRulesForCsv(csvFile, rulesMapping);
|
|
17981
|
+
if (!rulesFile) {
|
|
17982
|
+
filesWithoutRules++;
|
|
17983
|
+
fileResults.push({
|
|
17984
|
+
csv: path6.relative(directory, csvFile),
|
|
17985
|
+
rulesFile: null,
|
|
17986
|
+
totalTransactions: 0,
|
|
17987
|
+
matchedTransactions: 0,
|
|
17988
|
+
unknownPostings: [],
|
|
17989
|
+
error: "No matching rules file found"
|
|
17990
|
+
});
|
|
17991
|
+
continue;
|
|
17992
|
+
}
|
|
17993
|
+
const result = await hledgerExecutor(["print", "-f", csvFile, "--rules-file", rulesFile]);
|
|
17994
|
+
if (result.exitCode !== 0) {
|
|
17995
|
+
filesWithErrors++;
|
|
17996
|
+
fileResults.push({
|
|
17997
|
+
csv: path6.relative(directory, csvFile),
|
|
17998
|
+
rulesFile: path6.relative(directory, rulesFile),
|
|
17999
|
+
totalTransactions: 0,
|
|
18000
|
+
matchedTransactions: 0,
|
|
18001
|
+
unknownPostings: [],
|
|
18002
|
+
error: `hledger error: ${result.stderr.trim() || "Unknown error"}`
|
|
18003
|
+
});
|
|
18004
|
+
continue;
|
|
18005
|
+
}
|
|
18006
|
+
const unknownPostings = parseUnknownPostings(result.stdout);
|
|
18007
|
+
const transactionCount = countTransactions(result.stdout);
|
|
18008
|
+
const matchedCount = transactionCount - unknownPostings.length;
|
|
18009
|
+
if (unknownPostings.length > 0) {
|
|
18010
|
+
try {
|
|
18011
|
+
const rulesContent = fs7.readFileSync(rulesFile, "utf-8");
|
|
18012
|
+
const rulesConfig = parseRulesFile(rulesContent);
|
|
18013
|
+
const csvRows = parseCsvFile(csvFile, rulesConfig);
|
|
18014
|
+
for (const posting of unknownPostings) {
|
|
18015
|
+
const csvRow = findMatchingCsvRow({
|
|
18016
|
+
date: posting.date,
|
|
18017
|
+
description: posting.description,
|
|
18018
|
+
amount: posting.amount
|
|
18019
|
+
}, csvRows, rulesConfig);
|
|
18020
|
+
posting.csvRow = csvRow;
|
|
18021
|
+
}
|
|
18022
|
+
} catch {
|
|
18023
|
+
for (const posting of unknownPostings) {
|
|
18024
|
+
posting.csvRow = undefined;
|
|
18025
|
+
}
|
|
18026
|
+
}
|
|
18027
|
+
}
|
|
18028
|
+
totalTransactions += transactionCount;
|
|
18029
|
+
totalMatched += matchedCount;
|
|
18030
|
+
totalUnknown += unknownPostings.length;
|
|
18031
|
+
fileResults.push({
|
|
18032
|
+
csv: path6.relative(directory, csvFile),
|
|
18033
|
+
rulesFile: path6.relative(directory, rulesFile),
|
|
18034
|
+
totalTransactions: transactionCount,
|
|
18035
|
+
matchedTransactions: matchedCount,
|
|
18036
|
+
unknownPostings
|
|
18037
|
+
});
|
|
18038
|
+
}
|
|
18039
|
+
const hasUnknowns = totalUnknown > 0;
|
|
18040
|
+
const hasErrors = filesWithErrors > 0 || filesWithoutRules > 0;
|
|
18041
|
+
if (options.checkOnly !== false) {
|
|
18042
|
+
const result = {
|
|
18043
|
+
success: !hasUnknowns && !hasErrors,
|
|
18044
|
+
files: fileResults,
|
|
18045
|
+
summary: {
|
|
18046
|
+
filesProcessed: csvFiles.length,
|
|
18047
|
+
filesWithErrors,
|
|
18048
|
+
filesWithoutRules,
|
|
18049
|
+
totalTransactions,
|
|
18050
|
+
matched: totalMatched,
|
|
18051
|
+
unknown: totalUnknown
|
|
18052
|
+
}
|
|
18053
|
+
};
|
|
18054
|
+
if (hasUnknowns) {
|
|
18055
|
+
result.message = `Found ${totalUnknown} transaction(s) with unknown accounts. Add rules to categorize them.`;
|
|
18056
|
+
} else if (hasErrors) {
|
|
18057
|
+
result.message = `Some files had errors. Check the file results for details.`;
|
|
18058
|
+
} else {
|
|
18059
|
+
result.message = "All transactions matched. Ready to import with checkOnly: false";
|
|
18060
|
+
}
|
|
18061
|
+
return JSON.stringify(result);
|
|
18062
|
+
}
|
|
18063
|
+
if (hasUnknowns || hasErrors) {
|
|
18064
|
+
return JSON.stringify({
|
|
18065
|
+
success: false,
|
|
18066
|
+
files: fileResults,
|
|
18067
|
+
summary: {
|
|
18068
|
+
filesProcessed: csvFiles.length,
|
|
18069
|
+
filesWithErrors,
|
|
18070
|
+
filesWithoutRules,
|
|
18071
|
+
totalTransactions,
|
|
18072
|
+
matched: totalMatched,
|
|
18073
|
+
unknown: totalUnknown
|
|
18074
|
+
},
|
|
18075
|
+
error: "Cannot import: some transactions have unknown accounts or files have errors",
|
|
18076
|
+
hint: "Run with checkOnly: true to see details, then add missing rules"
|
|
18077
|
+
});
|
|
18078
|
+
}
|
|
18079
|
+
const importedFiles = [];
|
|
18080
|
+
for (const csvFile of csvFiles) {
|
|
18081
|
+
const rulesFile = findRulesForCsv(csvFile, rulesMapping);
|
|
18082
|
+
if (!rulesFile)
|
|
18083
|
+
continue;
|
|
18084
|
+
const result = await hledgerExecutor(["import", csvFile, "--rules-file", rulesFile]);
|
|
18085
|
+
if (result.exitCode !== 0) {
|
|
18086
|
+
return JSON.stringify({
|
|
18087
|
+
success: false,
|
|
18088
|
+
files: fileResults,
|
|
18089
|
+
summary: {
|
|
18090
|
+
filesProcessed: csvFiles.length,
|
|
18091
|
+
filesWithErrors: 1,
|
|
18092
|
+
filesWithoutRules,
|
|
18093
|
+
totalTransactions,
|
|
18094
|
+
matched: totalMatched,
|
|
18095
|
+
unknown: totalUnknown
|
|
18096
|
+
},
|
|
18097
|
+
error: `Import failed for ${path6.relative(directory, csvFile)}: ${result.stderr.trim()}`
|
|
18098
|
+
});
|
|
18099
|
+
}
|
|
18100
|
+
importedFiles.push(csvFile);
|
|
18101
|
+
}
|
|
18102
|
+
for (const csvFile of importedFiles) {
|
|
18103
|
+
const relativePath = path6.relative(pendingDir, csvFile);
|
|
18104
|
+
const destPath = path6.join(doneDir, relativePath);
|
|
18105
|
+
const destDir = path6.dirname(destPath);
|
|
18106
|
+
if (!fs7.existsSync(destDir)) {
|
|
18107
|
+
fs7.mkdirSync(destDir, { recursive: true });
|
|
18108
|
+
}
|
|
18109
|
+
fs7.renameSync(csvFile, destPath);
|
|
18110
|
+
}
|
|
18111
|
+
return JSON.stringify({
|
|
18112
|
+
success: true,
|
|
18113
|
+
files: fileResults.map((f) => ({
|
|
18114
|
+
...f,
|
|
18115
|
+
imported: true
|
|
18116
|
+
})),
|
|
18117
|
+
summary: {
|
|
18118
|
+
filesProcessed: csvFiles.length,
|
|
18119
|
+
filesWithErrors: 0,
|
|
18120
|
+
filesWithoutRules: 0,
|
|
18121
|
+
totalTransactions,
|
|
18122
|
+
matched: totalMatched,
|
|
18123
|
+
unknown: 0
|
|
18124
|
+
},
|
|
18125
|
+
message: `Successfully imported ${totalTransactions} transaction(s) from ${importedFiles.length} file(s)`
|
|
18126
|
+
});
|
|
18127
|
+
}
|
|
18128
|
+
var import_statements_default = tool({
|
|
18129
|
+
description: `Import classified bank statement CSVs into hledger using rules files.
|
|
18130
|
+
|
|
18131
|
+
This tool processes CSV files in the pending import directory and uses hledger's CSV import capabilities with matching rules files.
|
|
18132
|
+
|
|
18133
|
+
**Check Mode (checkOnly: true, default):**
|
|
18134
|
+
- Runs hledger print to preview transactions
|
|
18135
|
+
- Identifies transactions with 'income:unknown' or 'expenses:unknown' accounts
|
|
18136
|
+
- These indicate missing rules that need to be added
|
|
18137
|
+
|
|
18138
|
+
**Import Mode (checkOnly: false):**
|
|
18139
|
+
- First validates all transactions have known accounts
|
|
18140
|
+
- If any unknowns exist, aborts and reports them
|
|
18141
|
+
- If all clean, imports transactions and moves CSVs to done directory
|
|
18142
|
+
|
|
18143
|
+
**Workflow:**
|
|
18144
|
+
1. Run with checkOnly: true (or no args)
|
|
18145
|
+
2. If unknowns found, add rules to the appropriate .rules file
|
|
18146
|
+
3. Repeat until no unknowns
|
|
18147
|
+
4. Run with checkOnly: false to import`,
|
|
18148
|
+
args: {
|
|
18149
|
+
provider: tool.schema.string().optional().describe('Filter by provider (e.g., "revolut", "ubs"). If omitted, process all providers.'),
|
|
18150
|
+
currency: tool.schema.string().optional().describe('Filter by currency (e.g., "chf", "eur"). If omitted, process all currencies for the provider.'),
|
|
18151
|
+
checkOnly: tool.schema.boolean().optional().describe("If true (default), only check for unknown accounts without importing. Set to false to perform actual import.")
|
|
18152
|
+
},
|
|
18153
|
+
async execute(params, context) {
|
|
18154
|
+
const { directory, agent } = context;
|
|
18155
|
+
return importStatementsCore(directory, agent, {
|
|
18156
|
+
provider: params.provider,
|
|
18157
|
+
currency: params.currency,
|
|
18158
|
+
checkOnly: params.checkOnly
|
|
18159
|
+
});
|
|
18160
|
+
}
|
|
18161
|
+
});
|
|
16971
18162
|
// src/index.ts
|
|
16972
|
-
var __dirname2 =
|
|
16973
|
-
var AGENT_FILE =
|
|
18163
|
+
var __dirname2 = dirname4(fileURLToPath(import.meta.url));
|
|
18164
|
+
var AGENT_FILE = join7(__dirname2, "..", "agent", "accountant.md");
|
|
16974
18165
|
var AccountantPlugin = async () => {
|
|
16975
18166
|
const agent = loadAgent(AGENT_FILE);
|
|
16976
18167
|
return {
|
|
16977
18168
|
tool: {
|
|
16978
18169
|
"update-prices": update_prices_default,
|
|
16979
|
-
"classify-statements": classify_statements_default
|
|
18170
|
+
"classify-statements": classify_statements_default,
|
|
18171
|
+
"import-statements": import_statements_default
|
|
16980
18172
|
},
|
|
16981
18173
|
config: async (config2) => {
|
|
16982
18174
|
if (agent) {
|