@fuzzle/opencode-accountant 0.0.16 → 0.0.17-next.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1342,28 +1342,28 @@ var require_papaparse = __commonJS((exports, module) => {
1342
1342
 
1343
1343
  // node_modules/convert-csv-to-json/src/util/fileUtils.js
1344
1344
  var require_fileUtils = __commonJS((exports, module) => {
1345
- var fs6 = __require("fs");
1345
+ var fs3 = __require("fs");
1346
1346
 
1347
1347
  class FileUtils {
1348
1348
  readFile(fileInputName, encoding) {
1349
- return fs6.readFileSync(fileInputName, encoding).toString();
1349
+ return fs3.readFileSync(fileInputName, encoding).toString();
1350
1350
  }
1351
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());
1352
+ if (fs3.promises && typeof fs3.promises.readFile === "function") {
1353
+ return fs3.promises.readFile(fileInputName, encoding).then((buf) => buf.toString());
1354
1354
  }
1355
- return new Promise((resolve2, reject) => {
1356
- fs6.readFile(fileInputName, encoding, (err, data) => {
1355
+ return new Promise((resolve, reject) => {
1356
+ fs3.readFile(fileInputName, encoding, (err, data) => {
1357
1357
  if (err) {
1358
1358
  reject(err);
1359
1359
  return;
1360
1360
  }
1361
- resolve2(data.toString());
1361
+ resolve(data.toString());
1362
1362
  });
1363
1363
  });
1364
1364
  }
1365
1365
  writeFile(json3, fileOutputName) {
1366
- fs6.writeFile(fileOutputName, json3, function(err) {
1366
+ fs3.writeFile(fileOutputName, json3, function(err) {
1367
1367
  if (err) {
1368
1368
  throw err;
1369
1369
  } else {
@@ -1372,14 +1372,14 @@ var require_fileUtils = __commonJS((exports, module) => {
1372
1372
  });
1373
1373
  }
1374
1374
  writeFileAsync(json3, fileOutputName) {
1375
- if (fs6.promises && typeof fs6.promises.writeFile === "function") {
1376
- return fs6.promises.writeFile(fileOutputName, json3);
1375
+ if (fs3.promises && typeof fs3.promises.writeFile === "function") {
1376
+ return fs3.promises.writeFile(fileOutputName, json3);
1377
1377
  }
1378
- return new Promise((resolve2, reject) => {
1379
- fs6.writeFile(fileOutputName, json3, (err) => {
1378
+ return new Promise((resolve, reject) => {
1379
+ fs3.writeFile(fileOutputName, json3, (err) => {
1380
1380
  if (err)
1381
1381
  return reject(err);
1382
- resolve2();
1382
+ resolve();
1383
1383
  });
1384
1384
  });
1385
1385
  }
@@ -1800,7 +1800,7 @@ var require_browserApi = __commonJS((exports, module) => {
1800
1800
  if (!file2) {
1801
1801
  return Promise.reject(new Error("file is not defined!!!"));
1802
1802
  }
1803
- return new Promise((resolve2, reject) => {
1803
+ return new Promise((resolve, reject) => {
1804
1804
  if (typeof FileReader === "undefined") {
1805
1805
  reject(new Error("FileReader is not available in this environment"));
1806
1806
  return;
@@ -1811,7 +1811,7 @@ var require_browserApi = __commonJS((exports, module) => {
1811
1811
  try {
1812
1812
  const text = reader.result;
1813
1813
  const result = this.csvToJson.csvToJson(String(text));
1814
- resolve2(result);
1814
+ resolve(result);
1815
1815
  } catch (err) {
1816
1816
  reject(err);
1817
1817
  }
@@ -1941,7 +1941,7 @@ var require_convert_csv_to_json = __commonJS((exports) => {
1941
1941
  });
1942
1942
 
1943
1943
  // src/index.ts
1944
- import { dirname as dirname4, join as join7 } from "path";
1944
+ import { dirname, join as join3 } from "path";
1945
1945
  import { fileURLToPath } from "url";
1946
1946
 
1947
1947
  // src/utils/agentLoader.ts
@@ -16981,6 +16981,21 @@ var {$ } = globalThis.Bun;
16981
16981
  import * as path2 from "path";
16982
16982
  import * as fs2 from "fs";
16983
16983
 
16984
+ // src/utils/agentRestriction.ts
16985
+ function checkAccountantAgent(agent, toolPrompt, additionalFields) {
16986
+ if (agent === "accountant") {
16987
+ return null;
16988
+ }
16989
+ const errorResponse = {
16990
+ success: false,
16991
+ error: "This tool is restricted to the accountant agent only.",
16992
+ hint: `Use: Task(subagent_type='accountant', prompt='${toolPrompt}')`,
16993
+ caller: agent || "main assistant",
16994
+ ...additionalFields
16995
+ };
16996
+ return JSON.stringify(errorResponse);
16997
+ }
16998
+
16984
16999
  // src/utils/pricesConfig.ts
16985
17000
  import * as fs from "fs";
16986
17001
  import * as path from "path";
@@ -17090,12 +17105,9 @@ function updateJournalWithPrices(journalPath, newPriceLines) {
17090
17105
  `);
17091
17106
  }
17092
17107
  async function updatePricesCore(directory, agent, backfill, priceFetcher = defaultPriceFetcher, configLoader = loadPricesConfig) {
17093
- if (agent !== "accountant") {
17094
- return JSON.stringify({
17095
- error: "This tool is restricted to the accountant agent only.",
17096
- hint: "Use: Task(subagent_type='accountant', prompt='update prices')",
17097
- caller: agent || "main assistant"
17098
- });
17108
+ const restrictionError = checkAccountantAgent(agent, "update prices");
17109
+ if (restrictionError) {
17110
+ return restrictionError;
17099
17111
  }
17100
17112
  let config2;
17101
17113
  try {
@@ -17176,1130 +17188,20 @@ var update_prices_default = tool({
17176
17188
  return updatePricesCore(directory, agent, backfill || false);
17177
17189
  }
17178
17190
  });
17179
- // src/tools/classify-statements.ts
17180
- import * as path4 from "path";
17181
- import * as fs4 from "fs";
17182
-
17183
- // src/utils/importConfig.ts
17184
- import * as fs3 from "fs";
17185
- import * as path3 from "path";
17186
- var CONFIG_FILE2 = "config/import/providers.yaml";
17187
- var REQUIRED_PATH_FIELDS = [
17188
- "import",
17189
- "pending",
17190
- "done",
17191
- "unrecognized",
17192
- "rules"
17193
- ];
17194
- var REQUIRED_DETECTION_FIELDS = ["header", "currencyField"];
17195
- function validatePaths(paths) {
17196
- if (typeof paths !== "object" || paths === null) {
17197
- throw new Error("Invalid config: 'paths' must be an object");
17198
- }
17199
- const pathsObj = paths;
17200
- for (const field of REQUIRED_PATH_FIELDS) {
17201
- if (typeof pathsObj[field] !== "string" || pathsObj[field] === "") {
17202
- throw new Error(`Invalid config: 'paths.${field}' is required`);
17203
- }
17204
- }
17205
- return {
17206
- import: pathsObj.import,
17207
- pending: pathsObj.pending,
17208
- done: pathsObj.done,
17209
- unrecognized: pathsObj.unrecognized,
17210
- rules: pathsObj.rules
17211
- };
17212
- }
17213
- function validateDetectionRule(providerName, index, rule) {
17214
- if (typeof rule !== "object" || rule === null) {
17215
- throw new Error(`Invalid config: provider '${providerName}' detect[${index}] must be an object`);
17216
- }
17217
- const ruleObj = rule;
17218
- for (const field of REQUIRED_DETECTION_FIELDS) {
17219
- if (typeof ruleObj[field] !== "string" || ruleObj[field] === "") {
17220
- throw new Error(`Invalid config: provider '${providerName}' detect[${index}].${field} is required`);
17221
- }
17222
- }
17223
- if (ruleObj.filenamePattern !== undefined) {
17224
- if (typeof ruleObj.filenamePattern !== "string") {
17225
- throw new Error(`Invalid config: provider '${providerName}' detect[${index}].filenamePattern must be a string`);
17226
- }
17227
- try {
17228
- new RegExp(ruleObj.filenamePattern);
17229
- } catch {
17230
- throw new Error(`Invalid config: provider '${providerName}' detect[${index}].filenamePattern is not a valid regex`);
17231
- }
17232
- }
17233
- if (ruleObj.skipRows !== undefined) {
17234
- if (typeof ruleObj.skipRows !== "number" || ruleObj.skipRows < 0) {
17235
- throw new Error(`Invalid config: provider '${providerName}' detect[${index}].skipRows must be a non-negative number`);
17236
- }
17237
- }
17238
- if (ruleObj.delimiter !== undefined) {
17239
- if (typeof ruleObj.delimiter !== "string" || ruleObj.delimiter.length !== 1) {
17240
- throw new Error(`Invalid config: provider '${providerName}' detect[${index}].delimiter must be a single character`);
17241
- }
17242
- }
17243
- if (ruleObj.renamePattern !== undefined) {
17244
- if (typeof ruleObj.renamePattern !== "string") {
17245
- throw new Error(`Invalid config: provider '${providerName}' detect[${index}].renamePattern must be a string`);
17246
- }
17247
- }
17248
- if (ruleObj.metadata !== undefined) {
17249
- if (!Array.isArray(ruleObj.metadata)) {
17250
- throw new Error(`Invalid config: provider '${providerName}' detect[${index}].metadata must be an array`);
17251
- }
17252
- for (let i2 = 0;i2 < ruleObj.metadata.length; i2++) {
17253
- const meta = ruleObj.metadata[i2];
17254
- if (typeof meta.field !== "string" || meta.field === "") {
17255
- throw new Error(`Invalid config: provider '${providerName}' detect[${index}].metadata[${i2}].field is required`);
17256
- }
17257
- if (typeof meta.row !== "number" || meta.row < 0) {
17258
- throw new Error(`Invalid config: provider '${providerName}' detect[${index}].metadata[${i2}].row must be a non-negative number`);
17259
- }
17260
- if (typeof meta.column !== "number" || meta.column < 0) {
17261
- throw new Error(`Invalid config: provider '${providerName}' detect[${index}].metadata[${i2}].column must be a non-negative number`);
17262
- }
17263
- if (meta.normalize !== undefined && meta.normalize !== "spaces-to-dashes") {
17264
- throw new Error(`Invalid config: provider '${providerName}' detect[${index}].metadata[${i2}].normalize must be 'spaces-to-dashes'`);
17265
- }
17266
- }
17267
- }
17268
- return {
17269
- filenamePattern: ruleObj.filenamePattern,
17270
- header: ruleObj.header,
17271
- currencyField: ruleObj.currencyField,
17272
- skipRows: ruleObj.skipRows,
17273
- delimiter: ruleObj.delimiter,
17274
- renamePattern: ruleObj.renamePattern,
17275
- metadata: ruleObj.metadata
17276
- };
17277
- }
17278
- function validateProviderConfig(name, config2) {
17279
- if (typeof config2 !== "object" || config2 === null) {
17280
- throw new Error(`Invalid config for provider '${name}': expected an object`);
17281
- }
17282
- const configObj = config2;
17283
- if (!Array.isArray(configObj.detect) || configObj.detect.length === 0) {
17284
- throw new Error(`Invalid config for provider '${name}': 'detect' must be a non-empty array`);
17285
- }
17286
- const detect = [];
17287
- for (let i2 = 0;i2 < configObj.detect.length; i2++) {
17288
- detect.push(validateDetectionRule(name, i2, configObj.detect[i2]));
17289
- }
17290
- if (typeof configObj.currencies !== "object" || configObj.currencies === null) {
17291
- throw new Error(`Invalid config for provider '${name}': 'currencies' must be an object`);
17292
- }
17293
- const currenciesObj = configObj.currencies;
17294
- const currencies = {};
17295
- for (const [key, value] of Object.entries(currenciesObj)) {
17296
- if (typeof value !== "string") {
17297
- throw new Error(`Invalid config for provider '${name}': currencies.${key} must be a string`);
17298
- }
17299
- currencies[key] = value;
17300
- }
17301
- if (Object.keys(currencies).length === 0) {
17302
- throw new Error(`Invalid config for provider '${name}': 'currencies' must contain at least one mapping`);
17303
- }
17304
- return { detect, currencies };
17305
- }
17306
- function loadImportConfig(directory) {
17307
- const configPath = path3.join(directory, CONFIG_FILE2);
17308
- if (!fs3.existsSync(configPath)) {
17309
- throw new Error(`Configuration file not found: ${CONFIG_FILE2}. Please create this file to configure statement imports.`);
17310
- }
17311
- let parsed;
17312
- try {
17313
- const content = fs3.readFileSync(configPath, "utf-8");
17314
- parsed = jsYaml.load(content);
17315
- } catch (err) {
17316
- if (err instanceof jsYaml.YAMLException) {
17317
- throw new Error(`Failed to parse ${CONFIG_FILE2}: ${err.message}`);
17318
- }
17319
- throw err;
17320
- }
17321
- if (typeof parsed !== "object" || parsed === null) {
17322
- throw new Error(`Invalid config: ${CONFIG_FILE2} must contain a YAML object`);
17323
- }
17324
- const parsedObj = parsed;
17325
- if (!parsedObj.paths) {
17326
- throw new Error("Invalid config: 'paths' section is required");
17327
- }
17328
- const paths = validatePaths(parsedObj.paths);
17329
- if (!parsedObj.providers || typeof parsedObj.providers !== "object") {
17330
- throw new Error("Invalid config: 'providers' section is required");
17331
- }
17332
- const providersObj = parsedObj.providers;
17333
- if (Object.keys(providersObj).length === 0) {
17334
- throw new Error("Invalid config: 'providers' section must contain at least one provider");
17335
- }
17336
- const providers = {};
17337
- for (const [name, config2] of Object.entries(providersObj)) {
17338
- providers[name] = validateProviderConfig(name, config2);
17339
- }
17340
- return { paths, providers };
17341
- }
17342
-
17343
17191
  // src/utils/providerDetector.ts
17344
17192
  var import_papaparse = __toESM(require_papaparse(), 1);
17345
- function extractMetadata(content, skipRows, delimiter, metadataConfig) {
17346
- if (!metadataConfig || metadataConfig.length === 0 || skipRows === 0) {
17347
- return {};
17348
- }
17349
- const lines = content.split(`
17350
- `).slice(0, skipRows);
17351
- const metadata = {};
17352
- for (const config2 of metadataConfig) {
17353
- if (config2.row >= lines.length)
17354
- continue;
17355
- const columns = lines[config2.row].split(delimiter);
17356
- if (config2.column >= columns.length)
17357
- continue;
17358
- let value = columns[config2.column].trim();
17359
- if (config2.normalize === "spaces-to-dashes") {
17360
- value = value.replace(/\s+/g, "-");
17361
- }
17362
- metadata[config2.field] = value;
17363
- }
17364
- return metadata;
17365
- }
17366
- function generateOutputFilename(renamePattern, metadata) {
17367
- if (!renamePattern) {
17368
- return;
17369
- }
17370
- let filename = renamePattern;
17371
- for (const [key, value] of Object.entries(metadata)) {
17372
- filename = filename.replace(`{${key}}`, value);
17373
- }
17374
- return filename;
17375
- }
17376
- function parseCSVPreview(content, skipRows = 0, delimiter = ",") {
17377
- let csvContent = content;
17378
- if (skipRows > 0) {
17379
- const lines = content.split(`
17380
- `);
17381
- csvContent = lines.slice(skipRows).join(`
17382
- `);
17383
- }
17384
- const result = import_papaparse.default.parse(csvContent, {
17385
- header: true,
17386
- preview: 1,
17387
- skipEmptyLines: true,
17388
- delimiter
17389
- });
17390
- return {
17391
- fields: result.meta.fields,
17392
- firstRow: result.data[0]
17393
- };
17394
- }
17395
- function normalizeHeader(fields) {
17396
- return fields.map((f) => f.trim()).join(",");
17397
- }
17398
- function detectProvider(filename, content, config2) {
17399
- for (const [providerName, providerConfig] of Object.entries(config2.providers)) {
17400
- for (const rule of providerConfig.detect) {
17401
- if (rule.filenamePattern !== undefined) {
17402
- const filenameRegex = new RegExp(rule.filenamePattern);
17403
- if (!filenameRegex.test(filename)) {
17404
- continue;
17405
- }
17406
- }
17407
- const skipRows = rule.skipRows ?? 0;
17408
- const delimiter = rule.delimiter ?? ",";
17409
- const { fields, firstRow } = parseCSVPreview(content, skipRows, delimiter);
17410
- if (!fields || fields.length === 0) {
17411
- continue;
17412
- }
17413
- const actualHeader = normalizeHeader(fields);
17414
- if (actualHeader !== rule.header) {
17415
- continue;
17416
- }
17417
- if (!firstRow) {
17418
- continue;
17419
- }
17420
- const rawCurrency = firstRow[rule.currencyField];
17421
- if (!rawCurrency) {
17422
- continue;
17423
- }
17424
- const metadata = extractMetadata(content, skipRows, delimiter, rule.metadata);
17425
- const outputFilename = generateOutputFilename(rule.renamePattern, metadata);
17426
- const normalizedCurrency = providerConfig.currencies[rawCurrency];
17427
- if (!normalizedCurrency) {
17428
- return {
17429
- provider: providerName,
17430
- currency: rawCurrency.toLowerCase(),
17431
- rule,
17432
- outputFilename,
17433
- metadata: Object.keys(metadata).length > 0 ? metadata : undefined
17434
- };
17435
- }
17436
- return {
17437
- provider: providerName,
17438
- currency: normalizedCurrency,
17439
- rule,
17440
- outputFilename,
17441
- metadata: Object.keys(metadata).length > 0 ? metadata : undefined
17442
- };
17443
- }
17444
- }
17445
- return null;
17446
- }
17447
-
17448
- // src/tools/classify-statements.ts
17449
- function findCSVFiles(importsDir) {
17450
- if (!fs4.existsSync(importsDir)) {
17451
- return [];
17452
- }
17453
- return fs4.readdirSync(importsDir).filter((file2) => file2.toLowerCase().endsWith(".csv")).filter((file2) => {
17454
- const fullPath = path4.join(importsDir, file2);
17455
- return fs4.statSync(fullPath).isFile();
17456
- });
17457
- }
17458
- function ensureDirectory(dirPath) {
17459
- if (!fs4.existsSync(dirPath)) {
17460
- fs4.mkdirSync(dirPath, { recursive: true });
17461
- }
17462
- }
17463
- async function classifyStatementsCore(directory, agent, configLoader = loadImportConfig) {
17464
- if (agent !== "accountant") {
17465
- return JSON.stringify({
17466
- success: false,
17467
- error: "This tool is restricted to the accountant agent only.",
17468
- hint: "Use: Task(subagent_type='accountant', prompt='classify statements')",
17469
- caller: agent || "main assistant",
17470
- classified: [],
17471
- unrecognized: []
17472
- });
17473
- }
17474
- let config2;
17475
- try {
17476
- config2 = configLoader(directory);
17477
- } catch (err) {
17478
- return JSON.stringify({
17479
- success: false,
17480
- error: err instanceof Error ? err.message : String(err),
17481
- classified: [],
17482
- unrecognized: []
17483
- });
17484
- }
17485
- const importsDir = path4.join(directory, config2.paths.import);
17486
- const pendingDir = path4.join(directory, config2.paths.pending);
17487
- const unrecognizedDir = path4.join(directory, config2.paths.unrecognized);
17488
- const csvFiles = findCSVFiles(importsDir);
17489
- if (csvFiles.length === 0) {
17490
- return JSON.stringify({
17491
- success: true,
17492
- classified: [],
17493
- unrecognized: [],
17494
- message: `No CSV files found in ${config2.paths.import}`
17495
- });
17496
- }
17497
- const plannedMoves = [];
17498
- const collisions = [];
17499
- for (const filename of csvFiles) {
17500
- const sourcePath = path4.join(importsDir, filename);
17501
- const content = fs4.readFileSync(sourcePath, "utf-8");
17502
- const detection = detectProvider(filename, content, config2);
17503
- let targetPath;
17504
- let targetFilename;
17505
- if (detection) {
17506
- targetFilename = detection.outputFilename || filename;
17507
- const targetDir = path4.join(pendingDir, detection.provider, detection.currency);
17508
- targetPath = path4.join(targetDir, targetFilename);
17509
- } else {
17510
- targetFilename = filename;
17511
- targetPath = path4.join(unrecognizedDir, filename);
17512
- }
17513
- if (fs4.existsSync(targetPath)) {
17514
- collisions.push({
17515
- filename,
17516
- existingPath: targetPath
17517
- });
17518
- }
17519
- plannedMoves.push({
17520
- filename,
17521
- sourcePath,
17522
- targetPath,
17523
- targetFilename,
17524
- detection
17525
- });
17526
- }
17527
- if (collisions.length > 0) {
17528
- return JSON.stringify({
17529
- success: false,
17530
- error: `Cannot classify: ${collisions.length} file(s) would overwrite existing pending files.`,
17531
- collisions,
17532
- classified: [],
17533
- unrecognized: []
17534
- });
17535
- }
17536
- const classified = [];
17537
- const unrecognized = [];
17538
- for (const move of plannedMoves) {
17539
- if (move.detection) {
17540
- const targetDir = path4.dirname(move.targetPath);
17541
- ensureDirectory(targetDir);
17542
- fs4.renameSync(move.sourcePath, move.targetPath);
17543
- classified.push({
17544
- filename: move.targetFilename,
17545
- originalFilename: move.detection.outputFilename ? move.filename : undefined,
17546
- provider: move.detection.provider,
17547
- currency: move.detection.currency,
17548
- targetPath: path4.join(config2.paths.pending, move.detection.provider, move.detection.currency, move.targetFilename)
17549
- });
17550
- } else {
17551
- ensureDirectory(unrecognizedDir);
17552
- fs4.renameSync(move.sourcePath, move.targetPath);
17553
- unrecognized.push({
17554
- filename: move.filename,
17555
- targetPath: path4.join(config2.paths.unrecognized, move.filename)
17556
- });
17557
- }
17558
- }
17559
- return JSON.stringify({
17560
- success: true,
17561
- classified,
17562
- unrecognized,
17563
- summary: {
17564
- total: csvFiles.length,
17565
- classified: classified.length,
17566
- unrecognized: unrecognized.length
17567
- }
17568
- });
17569
- }
17570
- var classify_statements_default = tool({
17571
- 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.",
17572
- args: {},
17573
- async execute(_params, context) {
17574
- const { directory, agent } = context;
17575
- return classifyStatementsCore(directory, agent);
17576
- }
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
- function extractTransactionYears(hledgerOutput) {
17693
- const years = new Set;
17694
- const lines = hledgerOutput.split(`
17695
- `);
17696
- for (const line of lines) {
17697
- const match = line.match(/^(\d{4})-\d{2}-\d{2}\s+/);
17698
- if (match) {
17699
- years.add(parseInt(match[1], 10));
17700
- }
17701
- }
17702
- return years;
17703
- }
17704
- async function validateLedger(mainJournalPath, executor = defaultHledgerExecutor) {
17705
- const errors3 = [];
17706
- const checkResult = await executor(["check", "--strict", "-f", mainJournalPath]);
17707
- if (checkResult.exitCode !== 0) {
17708
- const errorMsg = checkResult.stderr.trim() || checkResult.stdout.trim();
17709
- errors3.push(`hledger check --strict failed: ${errorMsg}`);
17710
- }
17711
- const balResult = await executor(["bal", "-f", mainJournalPath]);
17712
- if (balResult.exitCode !== 0) {
17713
- const errorMsg = balResult.stderr.trim() || balResult.stdout.trim();
17714
- errors3.push(`hledger bal failed: ${errorMsg}`);
17715
- }
17716
- return { valid: errors3.length === 0, errors: errors3 };
17717
- }
17718
-
17719
- // src/utils/rulesParser.ts
17720
- function parseSkipRows(rulesContent) {
17721
- const match = rulesContent.match(/^skip\s+(\d+)/m);
17722
- return match ? parseInt(match[1], 10) : 0;
17723
- }
17724
- function parseSeparator(rulesContent) {
17725
- const match = rulesContent.match(/^separator\s+(.)/m);
17726
- return match ? match[1] : ",";
17727
- }
17728
- function parseFieldNames(rulesContent) {
17729
- const match = rulesContent.match(/^fields\s+(.+)$/m);
17730
- if (!match) {
17731
- return [];
17732
- }
17733
- return match[1].split(",").map((field) => field.trim());
17734
- }
17735
- function parseDateFormat(rulesContent) {
17736
- const match = rulesContent.match(/^date-format\s+(.+)$/m);
17737
- return match ? match[1].trim() : "%Y-%m-%d";
17738
- }
17739
- function parseDateField(rulesContent, fieldNames) {
17740
- const match = rulesContent.match(/^date\s+%(\w+|\d+)/m);
17741
- if (!match) {
17742
- return fieldNames[0] || "date";
17743
- }
17744
- const value = match[1];
17745
- if (/^\d+$/.test(value)) {
17746
- const index = parseInt(value, 10) - 1;
17747
- return fieldNames[index] || value;
17748
- }
17749
- return value;
17750
- }
17751
- function parseAmountFields(rulesContent, fieldNames) {
17752
- const result = {};
17753
- const simpleMatch = rulesContent.match(/^amount\s+(-?)%(\w+|\d+)/m);
17754
- if (simpleMatch) {
17755
- const fieldRef = simpleMatch[2];
17756
- if (/^\d+$/.test(fieldRef)) {
17757
- const index = parseInt(fieldRef, 10) - 1;
17758
- result.single = fieldNames[index] || fieldRef;
17759
- } else {
17760
- result.single = fieldRef;
17761
- }
17762
- }
17763
- const debitMatch = rulesContent.match(/if\s+%(\w+)\s+\.\s*\n\s*amount\s+-?%\1/m);
17764
- if (debitMatch) {
17765
- result.debit = debitMatch[1];
17766
- }
17767
- const creditMatch = rulesContent.match(/if\s+%(\w+)\s+\.\s*\n\s*amount\s+%\1(?!\w)/m);
17768
- if (creditMatch && creditMatch[1] !== result.debit) {
17769
- result.credit = creditMatch[1];
17770
- }
17771
- if (result.debit || result.credit) {
17772
- delete result.single;
17773
- }
17774
- if (!result.single && !result.debit && !result.credit) {
17775
- result.single = "amount";
17776
- }
17777
- return result;
17778
- }
17779
- function parseRulesFile(rulesContent) {
17780
- const fieldNames = parseFieldNames(rulesContent);
17781
- return {
17782
- skipRows: parseSkipRows(rulesContent),
17783
- separator: parseSeparator(rulesContent),
17784
- fieldNames,
17785
- dateFormat: parseDateFormat(rulesContent),
17786
- dateField: parseDateField(rulesContent, fieldNames),
17787
- amountFields: parseAmountFields(rulesContent, fieldNames)
17788
- };
17789
- }
17790
-
17791
17193
  // src/utils/csvParser.ts
17792
17194
  var import_convert_csv_to_json = __toESM(require_convert_csv_to_json(), 1);
17793
- import * as fs6 from "fs";
17794
- function parseCsvFile(csvPath, config2) {
17795
- const csvContent = fs6.readFileSync(csvPath, "utf-8");
17796
- const lines = csvContent.split(`
17797
- `);
17798
- const headerIndex = config2.skipRows;
17799
- if (headerIndex >= lines.length) {
17800
- return [];
17801
- }
17802
- const headerLine = lines[headerIndex];
17803
- const dataLines = lines.slice(headerIndex + 1).filter((line) => line.trim() !== "");
17804
- const csvWithHeader = [headerLine, ...dataLines].join(`
17805
- `);
17806
- const rawRows = import_convert_csv_to_json.default.indexHeader(0).fieldDelimiter(config2.separator).supportQuotedField(true).csvStringToJson(csvWithHeader);
17807
- const fieldNames = config2.fieldNames.length > 0 ? config2.fieldNames : Object.keys(rawRows[0] || {});
17808
- const mappedRows = [];
17809
- for (const parsedRow of rawRows) {
17810
- const row = {};
17811
- const values = Object.values(parsedRow);
17812
- for (let i2 = 0;i2 < fieldNames.length && i2 < values.length; i2++) {
17813
- row[fieldNames[i2]] = values[i2];
17814
- }
17815
- mappedRows.push(row);
17816
- }
17817
- return mappedRows;
17818
- }
17819
- function parseAmountValue(amountStr) {
17820
- const cleaned = amountStr.replace(/[A-Z]{3}\s*/g, "").trim();
17821
- return parseFloat(cleaned) || 0;
17822
- }
17823
- function getRowAmount(row, amountFields) {
17824
- if (amountFields.single) {
17825
- return parseAmountValue(row[amountFields.single] || "0");
17826
- }
17827
- const debitValue = amountFields.debit ? parseAmountValue(row[amountFields.debit] || "0") : 0;
17828
- const creditValue = amountFields.credit ? parseAmountValue(row[amountFields.credit] || "0") : 0;
17829
- if (debitValue !== 0) {
17830
- return -Math.abs(debitValue);
17831
- }
17832
- if (creditValue !== 0) {
17833
- return Math.abs(creditValue);
17834
- }
17835
- return 0;
17836
- }
17837
- function parseDateToIso(dateStr, dateFormat) {
17838
- if (!dateStr)
17839
- return "";
17840
- if (dateFormat === "%Y-%m-%d" || dateFormat === "%F") {
17841
- return dateStr.trim();
17842
- }
17843
- if (dateFormat === "%d.%m.%Y") {
17844
- const parts = dateStr.split(".");
17845
- if (parts.length === 3) {
17846
- return `${parts[2]}-${parts[1].padStart(2, "0")}-${parts[0].padStart(2, "0")}`;
17847
- }
17848
- }
17849
- if (dateFormat === "%m/%d/%Y") {
17850
- const parts = dateStr.split("/");
17851
- if (parts.length === 3) {
17852
- return `${parts[2]}-${parts[0].padStart(2, "0")}-${parts[1].padStart(2, "0")}`;
17853
- }
17854
- }
17855
- if (dateFormat === "%d/%m/%Y") {
17856
- const parts = dateStr.split("/");
17857
- if (parts.length === 3) {
17858
- return `${parts[2]}-${parts[1].padStart(2, "0")}-${parts[0].padStart(2, "0")}`;
17859
- }
17860
- }
17861
- return dateStr.trim();
17862
- }
17863
- function looksLikeTransactionId(fieldName, value) {
17864
- if (!value || value.trim() === "")
17865
- return false;
17866
- const idFieldPatterns = [
17867
- /transaction/i,
17868
- /trans_?no/i,
17869
- /trans_?id/i,
17870
- /reference/i,
17871
- /ref_?no/i,
17872
- /ref_?id/i,
17873
- /booking_?id/i,
17874
- /payment_?id/i,
17875
- /order_?id/i
17876
- ];
17877
- const nameMatches = idFieldPatterns.some((pattern) => pattern.test(fieldName));
17878
- if (!nameMatches)
17879
- return false;
17880
- const trimmedValue = value.trim();
17881
- const looksLikeId = /^[A-Za-z0-9_-]+$/.test(trimmedValue) && trimmedValue.length >= 3;
17882
- return looksLikeId;
17883
- }
17884
- function findTransactionId(row) {
17885
- for (const [field, value] of Object.entries(row)) {
17886
- if (looksLikeTransactionId(field, value)) {
17887
- return { field, value: value.trim() };
17888
- }
17889
- }
17890
- return null;
17891
- }
17892
- function findMatchingCsvRow(posting, csvRows, config2) {
17893
- const postingAmount = parseAmountValue(posting.amount);
17894
- let candidates = csvRows.filter((row) => {
17895
- const rowDate = parseDateToIso(row[config2.dateField] || "", config2.dateFormat);
17896
- const rowAmount = getRowAmount(row, config2.amountFields);
17897
- if (rowDate !== posting.date)
17898
- return false;
17899
- if (Math.abs(rowAmount - postingAmount) > 0.001)
17900
- return false;
17901
- return true;
17902
- });
17903
- if (candidates.length === 1) {
17904
- return candidates[0];
17905
- }
17906
- if (candidates.length === 0) {
17907
- 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.`);
17908
- }
17909
- for (const candidate of candidates) {
17910
- const txId = findTransactionId(candidate);
17911
- if (txId) {
17912
- const withSameTxId = candidates.filter((row) => row[txId.field] === txId.value);
17913
- if (withSameTxId.length === 1) {
17914
- return withSameTxId[0];
17915
- }
17916
- }
17917
- }
17918
- const descriptionLower = posting.description.toLowerCase();
17919
- const descMatches = candidates.filter((row) => {
17920
- return Object.values(row).some((value) => value && value.toLowerCase().includes(descriptionLower));
17921
- });
17922
- if (descMatches.length === 1) {
17923
- return descMatches[0];
17924
- }
17925
- if (descMatches.length > 1) {
17926
- return descMatches[0];
17927
- }
17928
- return candidates[0];
17929
- }
17930
-
17931
- // src/tools/import-statements.ts
17932
- function ensureYearJournalExists(directory, year) {
17933
- const ledgerDir = path6.join(directory, "ledger");
17934
- const yearJournalPath = path6.join(ledgerDir, `${year}.journal`);
17935
- const mainJournalPath = path6.join(directory, ".hledger.journal");
17936
- if (!fs7.existsSync(ledgerDir)) {
17937
- fs7.mkdirSync(ledgerDir, { recursive: true });
17938
- }
17939
- if (!fs7.existsSync(yearJournalPath)) {
17940
- fs7.writeFileSync(yearJournalPath, `; ${year} transactions
17941
- `);
17942
- }
17943
- if (!fs7.existsSync(mainJournalPath)) {
17944
- throw new Error(`.hledger.journal not found at ${mainJournalPath}. Create it first with appropriate includes.`);
17945
- }
17946
- const mainJournalContent = fs7.readFileSync(mainJournalPath, "utf-8");
17947
- const includeDirective = `include ledger/${year}.journal`;
17948
- const lines = mainJournalContent.split(`
17949
- `);
17950
- const includeExists = lines.some((line) => {
17951
- const trimmed = line.trim();
17952
- return trimmed === includeDirective || trimmed.startsWith(includeDirective + " ");
17953
- });
17954
- if (!includeExists) {
17955
- const newContent = mainJournalContent.trimEnd() + `
17956
- ` + includeDirective + `
17957
- `;
17958
- fs7.writeFileSync(mainJournalPath, newContent);
17959
- }
17960
- return yearJournalPath;
17961
- }
17962
- function findPendingCsvFiles(pendingDir, provider, currency) {
17963
- const csvFiles = [];
17964
- if (!fs7.existsSync(pendingDir)) {
17965
- return csvFiles;
17966
- }
17967
- let searchPath = pendingDir;
17968
- if (provider) {
17969
- searchPath = path6.join(searchPath, provider);
17970
- if (currency) {
17971
- searchPath = path6.join(searchPath, currency);
17972
- }
17973
- }
17974
- if (!fs7.existsSync(searchPath)) {
17975
- return csvFiles;
17976
- }
17977
- function scanDirectory(directory) {
17978
- const entries = fs7.readdirSync(directory, { withFileTypes: true });
17979
- for (const entry of entries) {
17980
- const fullPath = path6.join(directory, entry.name);
17981
- if (entry.isDirectory()) {
17982
- scanDirectory(fullPath);
17983
- } else if (entry.isFile() && entry.name.endsWith(".csv")) {
17984
- csvFiles.push(fullPath);
17985
- }
17986
- }
17987
- }
17988
- scanDirectory(searchPath);
17989
- return csvFiles.sort();
17990
- }
17991
- async function importStatementsCore(directory, agent, options, configLoader = loadImportConfig, hledgerExecutor = defaultHledgerExecutor) {
17992
- if (agent !== "accountant") {
17993
- return JSON.stringify({
17994
- success: false,
17995
- error: `This tool is restricted to the accountant agent only. Called by: ${agent || "main assistant"}`,
17996
- hint: "Use: Task(subagent_type='accountant', prompt='import statements')"
17997
- });
17998
- }
17999
- let config2;
18000
- try {
18001
- config2 = configLoader(directory);
18002
- } catch (error45) {
18003
- return JSON.stringify({
18004
- success: false,
18005
- error: `Failed to load configuration: ${error45 instanceof Error ? error45.message : String(error45)}`,
18006
- hint: 'Ensure config/import/providers.yaml exists with required paths including "rules"'
18007
- });
18008
- }
18009
- const pendingDir = path6.join(directory, config2.paths.pending);
18010
- const rulesDir = path6.join(directory, config2.paths.rules);
18011
- const doneDir = path6.join(directory, config2.paths.done);
18012
- const rulesMapping = loadRulesMapping(rulesDir);
18013
- const csvFiles = findPendingCsvFiles(pendingDir, options.provider, options.currency);
18014
- if (csvFiles.length === 0) {
18015
- return JSON.stringify({
18016
- success: true,
18017
- files: [],
18018
- summary: {
18019
- filesProcessed: 0,
18020
- filesWithErrors: 0,
18021
- filesWithoutRules: 0,
18022
- totalTransactions: 0,
18023
- matched: 0,
18024
- unknown: 0
18025
- },
18026
- message: "No CSV files found to process"
18027
- });
18028
- }
18029
- const fileResults = [];
18030
- let totalTransactions = 0;
18031
- let totalMatched = 0;
18032
- let totalUnknown = 0;
18033
- let filesWithErrors = 0;
18034
- let filesWithoutRules = 0;
18035
- for (const csvFile of csvFiles) {
18036
- const rulesFile = findRulesForCsv(csvFile, rulesMapping);
18037
- if (!rulesFile) {
18038
- filesWithoutRules++;
18039
- fileResults.push({
18040
- csv: path6.relative(directory, csvFile),
18041
- rulesFile: null,
18042
- totalTransactions: 0,
18043
- matchedTransactions: 0,
18044
- unknownPostings: [],
18045
- error: "No matching rules file found"
18046
- });
18047
- continue;
18048
- }
18049
- const result = await hledgerExecutor(["print", "-f", csvFile, "--rules-file", rulesFile]);
18050
- if (result.exitCode !== 0) {
18051
- filesWithErrors++;
18052
- fileResults.push({
18053
- csv: path6.relative(directory, csvFile),
18054
- rulesFile: path6.relative(directory, rulesFile),
18055
- totalTransactions: 0,
18056
- matchedTransactions: 0,
18057
- unknownPostings: [],
18058
- error: `hledger error: ${result.stderr.trim() || "Unknown error"}`
18059
- });
18060
- continue;
18061
- }
18062
- const unknownPostings = parseUnknownPostings(result.stdout);
18063
- const transactionCount = countTransactions(result.stdout);
18064
- const matchedCount = transactionCount - unknownPostings.length;
18065
- const years = extractTransactionYears(result.stdout);
18066
- if (years.size > 1) {
18067
- const yearList = Array.from(years).sort().join(", ");
18068
- filesWithErrors++;
18069
- fileResults.push({
18070
- csv: path6.relative(directory, csvFile),
18071
- rulesFile: path6.relative(directory, rulesFile),
18072
- totalTransactions: transactionCount,
18073
- matchedTransactions: matchedCount,
18074
- unknownPostings: [],
18075
- error: `CSV contains transactions from multiple years (${yearList}). Split the CSV by year before importing.`
18076
- });
18077
- continue;
18078
- }
18079
- const transactionYear = years.size === 1 ? Array.from(years)[0] : undefined;
18080
- if (unknownPostings.length > 0) {
18081
- try {
18082
- const rulesContent = fs7.readFileSync(rulesFile, "utf-8");
18083
- const rulesConfig = parseRulesFile(rulesContent);
18084
- const csvRows = parseCsvFile(csvFile, rulesConfig);
18085
- for (const posting of unknownPostings) {
18086
- posting.csvRow = findMatchingCsvRow({
18087
- date: posting.date,
18088
- description: posting.description,
18089
- amount: posting.amount
18090
- }, csvRows, rulesConfig);
18091
- }
18092
- } catch {
18093
- for (const posting of unknownPostings) {
18094
- posting.csvRow = undefined;
18095
- }
18096
- }
18097
- }
18098
- totalTransactions += transactionCount;
18099
- totalMatched += matchedCount;
18100
- totalUnknown += unknownPostings.length;
18101
- fileResults.push({
18102
- csv: path6.relative(directory, csvFile),
18103
- rulesFile: path6.relative(directory, rulesFile),
18104
- totalTransactions: transactionCount,
18105
- matchedTransactions: matchedCount,
18106
- unknownPostings,
18107
- transactionYear
18108
- });
18109
- }
18110
- const hasUnknowns = totalUnknown > 0;
18111
- const hasErrors = filesWithErrors > 0 || filesWithoutRules > 0;
18112
- if (options.checkOnly !== false) {
18113
- const result = {
18114
- success: !hasUnknowns && !hasErrors,
18115
- files: fileResults,
18116
- summary: {
18117
- filesProcessed: csvFiles.length,
18118
- filesWithErrors,
18119
- filesWithoutRules,
18120
- totalTransactions,
18121
- matched: totalMatched,
18122
- unknown: totalUnknown
18123
- }
18124
- };
18125
- if (hasUnknowns) {
18126
- result.message = `Found ${totalUnknown} transaction(s) with unknown accounts. Add rules to categorize them.`;
18127
- } else if (hasErrors) {
18128
- result.message = `Some files had errors. Check the file results for details.`;
18129
- } else {
18130
- result.message = "All transactions matched. Ready to import with checkOnly: false";
18131
- }
18132
- return JSON.stringify(result);
18133
- }
18134
- if (hasUnknowns || hasErrors) {
18135
- return JSON.stringify({
18136
- success: false,
18137
- files: fileResults,
18138
- summary: {
18139
- filesProcessed: csvFiles.length,
18140
- filesWithErrors,
18141
- filesWithoutRules,
18142
- totalTransactions,
18143
- matched: totalMatched,
18144
- unknown: totalUnknown
18145
- },
18146
- error: "Cannot import: some transactions have unknown accounts or files have errors",
18147
- hint: "Run with checkOnly: true to see details, then add missing rules"
18148
- });
18149
- }
18150
- const importedFiles = [];
18151
- for (const fileResult of fileResults) {
18152
- const csvFile = path6.join(directory, fileResult.csv);
18153
- const rulesFile = fileResult.rulesFile ? path6.join(directory, fileResult.rulesFile) : null;
18154
- if (!rulesFile)
18155
- continue;
18156
- const year = fileResult.transactionYear;
18157
- if (!year) {
18158
- return JSON.stringify({
18159
- success: false,
18160
- files: fileResults,
18161
- summary: {
18162
- filesProcessed: csvFiles.length,
18163
- filesWithErrors: 1,
18164
- filesWithoutRules,
18165
- totalTransactions,
18166
- matched: totalMatched,
18167
- unknown: totalUnknown
18168
- },
18169
- error: `No transactions found in ${fileResult.csv}`
18170
- });
18171
- }
18172
- let yearJournalPath;
18173
- try {
18174
- yearJournalPath = ensureYearJournalExists(directory, year);
18175
- } catch (error45) {
18176
- return JSON.stringify({
18177
- success: false,
18178
- files: fileResults,
18179
- summary: {
18180
- filesProcessed: csvFiles.length,
18181
- filesWithErrors: 1,
18182
- filesWithoutRules,
18183
- totalTransactions,
18184
- matched: totalMatched,
18185
- unknown: totalUnknown
18186
- },
18187
- error: error45 instanceof Error ? error45.message : String(error45)
18188
- });
18189
- }
18190
- const result = await hledgerExecutor([
18191
- "import",
18192
- "-f",
18193
- yearJournalPath,
18194
- csvFile,
18195
- "--rules-file",
18196
- rulesFile
18197
- ]);
18198
- if (result.exitCode !== 0) {
18199
- return JSON.stringify({
18200
- success: false,
18201
- files: fileResults,
18202
- summary: {
18203
- filesProcessed: csvFiles.length,
18204
- filesWithErrors: 1,
18205
- filesWithoutRules,
18206
- totalTransactions,
18207
- matched: totalMatched,
18208
- unknown: totalUnknown
18209
- },
18210
- error: `Import failed for ${fileResult.csv}: ${result.stderr.trim()}`
18211
- });
18212
- }
18213
- importedFiles.push(csvFile);
18214
- }
18215
- const mainJournalPath = path6.join(directory, ".hledger.journal");
18216
- const validationResult = await validateLedger(mainJournalPath, hledgerExecutor);
18217
- if (!validationResult.valid) {
18218
- return JSON.stringify({
18219
- success: false,
18220
- files: fileResults,
18221
- summary: {
18222
- filesProcessed: csvFiles.length,
18223
- filesWithErrors: 1,
18224
- filesWithoutRules,
18225
- totalTransactions,
18226
- matched: totalMatched,
18227
- unknown: totalUnknown
18228
- },
18229
- error: `Ledger validation failed after import: ${validationResult.errors.join("; ")}`,
18230
- hint: "The import created invalid transactions. Check your rules file configuration (e.g., balance vs balance2 for balance assertions). CSV files have NOT been moved to done."
18231
- });
18232
- }
18233
- for (const csvFile of importedFiles) {
18234
- const relativePath = path6.relative(pendingDir, csvFile);
18235
- const destPath = path6.join(doneDir, relativePath);
18236
- const destDir = path6.dirname(destPath);
18237
- if (!fs7.existsSync(destDir)) {
18238
- fs7.mkdirSync(destDir, { recursive: true });
18239
- }
18240
- fs7.renameSync(csvFile, destPath);
18241
- }
18242
- return JSON.stringify({
18243
- success: true,
18244
- files: fileResults.map((f) => ({
18245
- ...f,
18246
- imported: true
18247
- })),
18248
- summary: {
18249
- filesProcessed: csvFiles.length,
18250
- filesWithErrors: 0,
18251
- filesWithoutRules: 0,
18252
- totalTransactions,
18253
- matched: totalMatched,
18254
- unknown: 0
18255
- },
18256
- message: `Successfully imported ${totalTransactions} transaction(s) from ${importedFiles.length} file(s)`
18257
- });
18258
- }
18259
- var import_statements_default = tool({
18260
- description: `Import classified bank statement CSVs into hledger using rules files.
18261
-
18262
- This tool processes CSV files in the pending import directory and uses hledger's CSV import capabilities with matching rules files.
18263
-
18264
- **Check Mode (checkOnly: true, default):**
18265
- - Runs hledger print to preview transactions
18266
- - Identifies transactions with 'income:unknown' or 'expenses:unknown' accounts
18267
- - These indicate missing rules that need to be added
18268
-
18269
- **Import Mode (checkOnly: false):**
18270
- - First validates all transactions have known accounts
18271
- - If any unknowns exist, aborts and reports them
18272
- - If all clean, imports transactions and moves CSVs to done directory
18273
-
18274
- **Workflow:**
18275
- 1. Run with checkOnly: true (or no args)
18276
- 2. If unknowns found, add rules to the appropriate .rules file
18277
- 3. Repeat until no unknowns
18278
- 4. Run with checkOnly: false to import`,
18279
- args: {
18280
- provider: tool.schema.string().optional().describe('Filter by provider (e.g., "revolut", "ubs"). If omitted, process all providers.'),
18281
- currency: tool.schema.string().optional().describe('Filter by currency (e.g., "chf", "eur"). If omitted, process all currencies for the provider.'),
18282
- checkOnly: tool.schema.boolean().optional().describe("If true (default), only check for unknown accounts without importing. Set to false to perform actual import.")
18283
- },
18284
- async execute(params, context) {
18285
- const { directory, agent } = context;
18286
- return importStatementsCore(directory, agent, {
18287
- provider: params.provider,
18288
- currency: params.currency,
18289
- checkOnly: params.checkOnly
18290
- });
18291
- }
18292
- });
18293
17195
  // src/index.ts
18294
- var __dirname2 = dirname4(fileURLToPath(import.meta.url));
18295
- var AGENT_FILE = join7(__dirname2, "..", "agent", "accountant.md");
17196
+ var __dirname2 = dirname(fileURLToPath(import.meta.url));
17197
+ var AGENT_FILE = join3(__dirname2, "..", "agent", "accountant.md");
18296
17198
  var AccountantPlugin = async () => {
18297
17199
  const agent = loadAgent(AGENT_FILE);
18298
17200
  return {
18299
17201
  tool: {
18300
17202
  "update-prices": update_prices_default,
18301
- "classify-statements": classify_statements_default,
18302
- "import-statements": import_statements_default
17203
+ "classify-statements": classifyStatements,
17204
+ "import-statements": importStatements
18303
17205
  },
18304
17206
  config: async (config2) => {
18305
17207
  if (agent) {