@fuzzle/opencode-accountant 0.16.6-next.1 → 0.16.7-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 +283 -218
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -17225,6 +17225,35 @@ stocks:
|
|
|
17225
17225
|
}
|
|
17226
17226
|
|
|
17227
17227
|
// src/utils/dateUtils.ts
|
|
17228
|
+
var PROVIDER_YEAR_PATTERNS = {
|
|
17229
|
+
swissquote: { pattern: /to-\d{4}(\d{4})\./, group: 1 },
|
|
17230
|
+
ibkr: { pattern: /(\d{4})\d{4}\./, group: 1 }
|
|
17231
|
+
};
|
|
17232
|
+
var DEFAULT_YEAR_PATTERN = /(\d{4})-\d{2}-\d{2}/;
|
|
17233
|
+
function getTransactionYear(ctx) {
|
|
17234
|
+
if (ctx.untilDate) {
|
|
17235
|
+
const year = parseInt(ctx.untilDate.slice(0, 4), 10);
|
|
17236
|
+
if (!isNaN(year))
|
|
17237
|
+
return year;
|
|
17238
|
+
}
|
|
17239
|
+
if (ctx.fromDate) {
|
|
17240
|
+
const year = parseInt(ctx.fromDate.slice(0, 4), 10);
|
|
17241
|
+
if (!isNaN(year))
|
|
17242
|
+
return year;
|
|
17243
|
+
}
|
|
17244
|
+
const providerPattern = PROVIDER_YEAR_PATTERNS[ctx.provider];
|
|
17245
|
+
if (providerPattern) {
|
|
17246
|
+
const match = ctx.filename.match(providerPattern.pattern);
|
|
17247
|
+
if (match) {
|
|
17248
|
+
return parseInt(match[providerPattern.group], 10);
|
|
17249
|
+
}
|
|
17250
|
+
}
|
|
17251
|
+
const defaultMatch = ctx.filePath.match(DEFAULT_YEAR_PATTERN);
|
|
17252
|
+
if (defaultMatch) {
|
|
17253
|
+
return parseInt(defaultMatch[1], 10);
|
|
17254
|
+
}
|
|
17255
|
+
return new Date().getFullYear();
|
|
17256
|
+
}
|
|
17228
17257
|
function formatDateISO(date5) {
|
|
17229
17258
|
return date5.toISOString().split("T")[0];
|
|
17230
17259
|
}
|
|
@@ -17741,10 +17770,10 @@ function getContextPath(directory, contextId) {
|
|
|
17741
17770
|
function createContext(directory, params) {
|
|
17742
17771
|
const now = new Date().toISOString();
|
|
17743
17772
|
const context = {
|
|
17773
|
+
...params,
|
|
17744
17774
|
id: randomUUID(),
|
|
17745
17775
|
createdAt: now,
|
|
17746
|
-
updatedAt: now
|
|
17747
|
-
...params
|
|
17776
|
+
updatedAt: now
|
|
17748
17777
|
};
|
|
17749
17778
|
ensureDirectory(path5.join(directory, ".memory"));
|
|
17750
17779
|
const contextPath = getContextPath(directory, context.id);
|
|
@@ -17760,7 +17789,7 @@ function validateContext(context, contextId) {
|
|
|
17760
17789
|
"currency"
|
|
17761
17790
|
];
|
|
17762
17791
|
for (const field of requiredFields) {
|
|
17763
|
-
if (
|
|
17792
|
+
if (typeof context[field] !== "string" || context[field] === "") {
|
|
17764
17793
|
throw new Error(`Invalid context ${contextId}: missing required field '${field}'`);
|
|
17765
17794
|
}
|
|
17766
17795
|
}
|
|
@@ -24453,6 +24482,7 @@ class MarkdownLogger {
|
|
|
24453
24482
|
autoFlush;
|
|
24454
24483
|
sectionDepth = 0;
|
|
24455
24484
|
pendingFlush = null;
|
|
24485
|
+
flushedOnce = false;
|
|
24456
24486
|
constructor(config2) {
|
|
24457
24487
|
this.autoFlush = config2.autoFlush ?? true;
|
|
24458
24488
|
this.context = config2.context || {};
|
|
@@ -24548,8 +24578,16 @@ class MarkdownLogger {
|
|
|
24548
24578
|
return;
|
|
24549
24579
|
try {
|
|
24550
24580
|
await fs13.mkdir(path11.dirname(this.logPath), { recursive: true });
|
|
24551
|
-
|
|
24581
|
+
if (this.flushedOnce) {
|
|
24582
|
+
await fs13.appendFile(this.logPath, `
|
|
24583
|
+
` + this.buffer.join(`
|
|
24552
24584
|
`), "utf-8");
|
|
24585
|
+
} else {
|
|
24586
|
+
await fs13.writeFile(this.logPath, this.buffer.join(`
|
|
24587
|
+
`), "utf-8");
|
|
24588
|
+
this.flushedOnce = true;
|
|
24589
|
+
}
|
|
24590
|
+
this.buffer = [];
|
|
24553
24591
|
} catch {}
|
|
24554
24592
|
}
|
|
24555
24593
|
getLogPath() {
|
|
@@ -27336,6 +27374,19 @@ class NoTransactionsError extends Error {
|
|
|
27336
27374
|
this.name = "NoTransactionsError";
|
|
27337
27375
|
}
|
|
27338
27376
|
}
|
|
27377
|
+
function cachedLoadContext(pipeCtx, contextId) {
|
|
27378
|
+
const cached2 = pipeCtx.contextCache.get(contextId);
|
|
27379
|
+
if (cached2)
|
|
27380
|
+
return cached2;
|
|
27381
|
+
const ctx = loadContext(pipeCtx.directory, contextId);
|
|
27382
|
+
pipeCtx.contextCache.set(contextId, ctx);
|
|
27383
|
+
return ctx;
|
|
27384
|
+
}
|
|
27385
|
+
function cachedUpdateContext(pipeCtx, contextId, updates) {
|
|
27386
|
+
const updated = updateContext(pipeCtx.directory, contextId, updates);
|
|
27387
|
+
pipeCtx.contextCache.set(contextId, updated);
|
|
27388
|
+
return updated;
|
|
27389
|
+
}
|
|
27339
27390
|
function buildStepResult(success2, message, details) {
|
|
27340
27391
|
const result = { success: success2, message };
|
|
27341
27392
|
if (details !== undefined) {
|
|
@@ -27399,7 +27450,7 @@ async function executeClassifyStep(context, logger) {
|
|
|
27399
27450
|
function executePreprocessFiatStep(context, contextIds, logger) {
|
|
27400
27451
|
const fiatCsvPaths = [];
|
|
27401
27452
|
for (const contextId of contextIds) {
|
|
27402
|
-
const importCtx =
|
|
27453
|
+
const importCtx = cachedLoadContext(context, contextId);
|
|
27403
27454
|
if (importCtx.provider === "revolut" && importCtx.currency !== "btc") {
|
|
27404
27455
|
fiatCsvPaths.push(path18.join(context.directory, importCtx.filePath));
|
|
27405
27456
|
}
|
|
@@ -27443,7 +27494,7 @@ function executePreprocessFiatStep(context, contextIds, logger) {
|
|
|
27443
27494
|
function executePreprocessBtcStep(context, contextIds, logger) {
|
|
27444
27495
|
let btcCsvPath;
|
|
27445
27496
|
for (const contextId of contextIds) {
|
|
27446
|
-
const importCtx =
|
|
27497
|
+
const importCtx = cachedLoadContext(context, contextId);
|
|
27447
27498
|
if (importCtx.provider === "revolut" && importCtx.currency === "btc") {
|
|
27448
27499
|
btcCsvPath = path18.join(context.directory, importCtx.filePath);
|
|
27449
27500
|
break;
|
|
@@ -27476,7 +27527,7 @@ async function executeBtcPurchaseStep(context, contextIds, logger) {
|
|
|
27476
27527
|
const fiatCsvPaths = [];
|
|
27477
27528
|
let btcCsvPath;
|
|
27478
27529
|
for (const contextId of contextIds) {
|
|
27479
|
-
const importCtx =
|
|
27530
|
+
const importCtx = cachedLoadContext(context, contextId);
|
|
27480
27531
|
if (importCtx.provider !== "revolut")
|
|
27481
27532
|
continue;
|
|
27482
27533
|
const csvPath = path18.join(context.directory, importCtx.filePath);
|
|
@@ -27492,29 +27543,39 @@ async function executeBtcPurchaseStep(context, contextIds, logger) {
|
|
|
27492
27543
|
}
|
|
27493
27544
|
logger?.startSection("Step 1b: Generate BTC Purchase Entries");
|
|
27494
27545
|
logger?.logStep("BTC Purchases", "start");
|
|
27495
|
-
|
|
27496
|
-
|
|
27497
|
-
|
|
27498
|
-
|
|
27499
|
-
|
|
27500
|
-
|
|
27501
|
-
|
|
27502
|
-
|
|
27503
|
-
|
|
27504
|
-
|
|
27505
|
-
|
|
27506
|
-
|
|
27507
|
-
|
|
27508
|
-
|
|
27509
|
-
|
|
27510
|
-
|
|
27511
|
-
|
|
27512
|
-
})
|
|
27546
|
+
try {
|
|
27547
|
+
const btcCtx = contextIds.map((id) => cachedLoadContext(context, id)).find((c) => c.provider === "revolut" && c.currency === "btc");
|
|
27548
|
+
const year = getTransactionYear(btcCtx);
|
|
27549
|
+
const yearJournalPath = ensureYearJournalExists(context.directory, year);
|
|
27550
|
+
const result = generateBtcPurchaseJournal(fiatCsvPaths, btcCsvPath, yearJournalPath, logger);
|
|
27551
|
+
for (const account of result.accountsUsed) {
|
|
27552
|
+
context.generatedAccounts.add(account);
|
|
27553
|
+
}
|
|
27554
|
+
const message = result.entriesAdded > 0 ? `Generated ${result.entriesAdded} BTC purchase entries (${result.matchCount} matched, ${result.skippedDuplicates} skipped)` : `No new BTC purchase entries (${result.matchCount} matched, ${result.skippedDuplicates} duplicates)`;
|
|
27555
|
+
logger?.logStep("BTC Purchases", "success", message);
|
|
27556
|
+
context.result.steps.btcPurchases = buildStepResult(true, message, {
|
|
27557
|
+
matchCount: result.matchCount,
|
|
27558
|
+
entriesAdded: result.entriesAdded,
|
|
27559
|
+
skippedDuplicates: result.skippedDuplicates,
|
|
27560
|
+
unmatchedFiat: result.unmatchedFiat.length,
|
|
27561
|
+
unmatchedBtc: result.unmatchedBtc.length
|
|
27562
|
+
});
|
|
27563
|
+
} catch (error45) {
|
|
27564
|
+
const errorMessage = `BTC purchase generation failed: ${error45 instanceof Error ? error45.message : String(error45)}`;
|
|
27565
|
+
logger?.error(errorMessage);
|
|
27566
|
+
logger?.logStep("BTC Purchases", "error", errorMessage);
|
|
27567
|
+
context.result.steps.btcPurchases = buildStepResult(false, errorMessage);
|
|
27568
|
+
context.result.error = errorMessage;
|
|
27569
|
+
context.result.hint = "Check the Revolut fiat and BTC CSV formats";
|
|
27570
|
+
throw new Error(errorMessage);
|
|
27571
|
+
} finally {
|
|
27572
|
+
logger?.endSection();
|
|
27573
|
+
}
|
|
27513
27574
|
}
|
|
27514
27575
|
async function executeCurrencyExchangeStep(context, contextIds, logger) {
|
|
27515
27576
|
const fiatContexts = [];
|
|
27516
27577
|
for (const contextId of contextIds) {
|
|
27517
|
-
const importCtx =
|
|
27578
|
+
const importCtx = cachedLoadContext(context, contextId);
|
|
27518
27579
|
if (importCtx.provider !== "revolut")
|
|
27519
27580
|
continue;
|
|
27520
27581
|
if (importCtx.currency === "btc")
|
|
@@ -27526,49 +27587,55 @@ async function executeCurrencyExchangeStep(context, contextIds, logger) {
|
|
|
27526
27587
|
logger?.info("Need at least 2 Revolut fiat CSVs for exchange matching, skipping");
|
|
27527
27588
|
return;
|
|
27528
27589
|
}
|
|
27529
|
-
logger?.startSection("Step
|
|
27590
|
+
logger?.startSection("Step 1d: Generate Currency Exchange Entries");
|
|
27530
27591
|
logger?.logStep("Currency Exchanges", "start");
|
|
27531
|
-
|
|
27532
|
-
|
|
27533
|
-
|
|
27534
|
-
|
|
27535
|
-
|
|
27536
|
-
|
|
27537
|
-
|
|
27538
|
-
|
|
27539
|
-
|
|
27540
|
-
|
|
27541
|
-
|
|
27542
|
-
|
|
27543
|
-
|
|
27544
|
-
|
|
27545
|
-
|
|
27546
|
-
|
|
27547
|
-
|
|
27548
|
-
|
|
27549
|
-
|
|
27592
|
+
try {
|
|
27593
|
+
const firstFiatCtx = cachedLoadContext(context, fiatContexts[0].contextId);
|
|
27594
|
+
const year = getTransactionYear(firstFiatCtx);
|
|
27595
|
+
const yearJournalPath = ensureYearJournalExists(context.directory, year);
|
|
27596
|
+
const fiatCsvPaths = fiatContexts.map((c) => c.csvPath);
|
|
27597
|
+
const result = generateCurrencyExchangeJournal(fiatCsvPaths, yearJournalPath, logger);
|
|
27598
|
+
for (const account of result.accountsUsed) {
|
|
27599
|
+
context.generatedAccounts.add(account);
|
|
27600
|
+
}
|
|
27601
|
+
if (result.matchedRowIndices.size > 0) {
|
|
27602
|
+
for (const { contextId, csvPath } of fiatContexts) {
|
|
27603
|
+
const indices = result.matchedRowIndices.get(csvPath);
|
|
27604
|
+
if (!indices || indices.size === 0)
|
|
27605
|
+
continue;
|
|
27606
|
+
const filteredCsvPath = filterExchangeRows(csvPath, indices, logger);
|
|
27607
|
+
cachedUpdateContext(context, contextId, {
|
|
27608
|
+
filePath: path18.relative(context.directory, filteredCsvPath)
|
|
27609
|
+
});
|
|
27610
|
+
logger?.info(`Updated context ${contextId} to use filtered CSV: ${path18.basename(filteredCsvPath)}`);
|
|
27611
|
+
}
|
|
27550
27612
|
}
|
|
27613
|
+
const message = result.entriesAdded > 0 ? `Generated ${result.entriesAdded} currency exchange entries (${result.matchCount} matched, ${result.skippedDuplicates} skipped)` : `No new currency exchange entries (${result.matchCount} matched, ${result.skippedDuplicates} duplicates)`;
|
|
27614
|
+
logger?.logStep("Currency Exchanges", "success", message);
|
|
27615
|
+
context.result.steps.currencyExchanges = buildStepResult(true, message, {
|
|
27616
|
+
matchCount: result.matchCount,
|
|
27617
|
+
entriesAdded: result.entriesAdded,
|
|
27618
|
+
skippedDuplicates: result.skippedDuplicates
|
|
27619
|
+
});
|
|
27620
|
+
} catch (error45) {
|
|
27621
|
+
const errorMessage = `Currency exchange generation failed: ${error45 instanceof Error ? error45.message : String(error45)}`;
|
|
27622
|
+
logger?.error(errorMessage);
|
|
27623
|
+
logger?.logStep("Currency Exchanges", "error", errorMessage);
|
|
27624
|
+
context.result.steps.currencyExchanges = buildStepResult(false, errorMessage);
|
|
27625
|
+
context.result.error = errorMessage;
|
|
27626
|
+
context.result.hint = "Check the Revolut fiat CSV formats";
|
|
27627
|
+
throw new Error(errorMessage);
|
|
27628
|
+
} finally {
|
|
27629
|
+
logger?.endSection();
|
|
27551
27630
|
}
|
|
27552
|
-
const message = result.entriesAdded > 0 ? `Generated ${result.entriesAdded} currency exchange entries (${result.matchCount} matched, ${result.skippedDuplicates} skipped)` : `No new currency exchange entries (${result.matchCount} matched, ${result.skippedDuplicates} duplicates)`;
|
|
27553
|
-
logger?.logStep("Currency Exchanges", "success", message);
|
|
27554
|
-
logger?.endSection();
|
|
27555
|
-
context.result.steps.currencyExchanges = buildStepResult(true, message, {
|
|
27556
|
-
matchCount: result.matchCount,
|
|
27557
|
-
entriesAdded: result.entriesAdded,
|
|
27558
|
-
skippedDuplicates: result.skippedDuplicates
|
|
27559
|
-
});
|
|
27560
27631
|
}
|
|
27561
27632
|
async function executeSwissquotePreprocessStep(context, contextIds, logger) {
|
|
27562
27633
|
const swissquoteContexts = [];
|
|
27563
27634
|
for (const contextId of contextIds) {
|
|
27564
|
-
const importCtx =
|
|
27635
|
+
const importCtx = cachedLoadContext(context, contextId);
|
|
27565
27636
|
if (importCtx.provider === "swissquote") {
|
|
27566
27637
|
const csvPath = path18.join(context.directory, importCtx.filePath);
|
|
27567
|
-
const
|
|
27568
|
-
let year = new Date().getFullYear();
|
|
27569
|
-
if (toDateMatch) {
|
|
27570
|
-
year = parseInt(toDateMatch[1], 10);
|
|
27571
|
-
}
|
|
27638
|
+
const year = getTransactionYear(importCtx);
|
|
27572
27639
|
swissquoteContexts.push({
|
|
27573
27640
|
contextId,
|
|
27574
27641
|
csvPath,
|
|
@@ -27581,7 +27648,7 @@ async function executeSwissquotePreprocessStep(context, contextIds, logger) {
|
|
|
27581
27648
|
logger?.info("No Swissquote CSV found, skipping preprocessing");
|
|
27582
27649
|
return;
|
|
27583
27650
|
}
|
|
27584
|
-
logger?.startSection("Step
|
|
27651
|
+
logger?.startSection("Step 1e: Preprocess Swissquote CSV");
|
|
27585
27652
|
const config2 = context.configLoader(context.directory);
|
|
27586
27653
|
const swissquoteProvider = config2.providers?.["swissquote"];
|
|
27587
27654
|
const lotInventoryPath = swissquoteProvider?.lotInventoryPath ?? DEFAULT_LOT_INVENTORY_PATH;
|
|
@@ -27629,7 +27696,7 @@ async function executeSwissquotePreprocessStep(context, contextIds, logger) {
|
|
|
27629
27696
|
lastJournalFile = result.journalFile;
|
|
27630
27697
|
}
|
|
27631
27698
|
if (result.simpleTransactionsCsv) {
|
|
27632
|
-
|
|
27699
|
+
cachedUpdateContext(context, sqCtx.contextId, {
|
|
27633
27700
|
filePath: path18.relative(context.directory, result.simpleTransactionsCsv)
|
|
27634
27701
|
});
|
|
27635
27702
|
logger?.info(`Updated context ${sqCtx.contextId} to use filtered CSV: ${path18.basename(result.simpleTransactionsCsv)}`);
|
|
@@ -27671,14 +27738,10 @@ async function executeSwissquotePreprocessStep(context, contextIds, logger) {
|
|
|
27671
27738
|
async function executeIbkrPreprocessStep(context, contextIds, logger) {
|
|
27672
27739
|
const ibkrContexts = [];
|
|
27673
27740
|
for (const contextId of contextIds) {
|
|
27674
|
-
const importCtx =
|
|
27741
|
+
const importCtx = cachedLoadContext(context, contextId);
|
|
27675
27742
|
if (importCtx.provider === "ibkr") {
|
|
27676
27743
|
const csvPath = path18.join(context.directory, importCtx.filePath);
|
|
27677
|
-
const
|
|
27678
|
-
let year = new Date().getFullYear();
|
|
27679
|
-
if (dateMatch) {
|
|
27680
|
-
year = parseInt(dateMatch[1], 10);
|
|
27681
|
-
}
|
|
27744
|
+
const year = getTransactionYear(importCtx);
|
|
27682
27745
|
ibkrContexts.push({
|
|
27683
27746
|
contextId,
|
|
27684
27747
|
csvPath,
|
|
@@ -27723,7 +27786,7 @@ async function executeIbkrPreprocessStep(context, contextIds, logger) {
|
|
|
27723
27786
|
lastJournalFile = result.journalFile;
|
|
27724
27787
|
}
|
|
27725
27788
|
if (result.simpleTransactionsCsv) {
|
|
27726
|
-
|
|
27789
|
+
cachedUpdateContext(context, ibkrCtx.contextId, {
|
|
27727
27790
|
filePath: path18.relative(context.directory, result.simpleTransactionsCsv)
|
|
27728
27791
|
});
|
|
27729
27792
|
logger?.info(`Updated context ${ibkrCtx.contextId} to use filtered CSV: ${path18.basename(result.simpleTransactionsCsv)}`);
|
|
@@ -27752,7 +27815,7 @@ async function executeAccountDeclarationsStep(context, contextId, logger) {
|
|
|
27752
27815
|
logger?.logStep("Check Accounts", "start");
|
|
27753
27816
|
const config2 = context.configLoader(context.directory);
|
|
27754
27817
|
const rulesDir = path18.join(context.directory, config2.paths.rules);
|
|
27755
|
-
const importCtx =
|
|
27818
|
+
const importCtx = cachedLoadContext(context, contextId);
|
|
27756
27819
|
const csvPath = path18.join(context.directory, importCtx.filePath);
|
|
27757
27820
|
const csvFiles = [csvPath];
|
|
27758
27821
|
const rulesMapping = loadRulesMapping(rulesDir);
|
|
@@ -27834,7 +27897,7 @@ async function buildSuggestionContext(context, contextId, logger) {
|
|
|
27834
27897
|
const { loadExistingAccounts: loadExistingAccounts2, extractRulePatternsFromFile: extractRulePatternsFromFile2 } = await Promise.resolve().then(() => (init_accountSuggester(), exports_accountSuggester));
|
|
27835
27898
|
const config2 = context.configLoader(context.directory);
|
|
27836
27899
|
const rulesDir = path18.join(context.directory, config2.paths.rules);
|
|
27837
|
-
const importCtx =
|
|
27900
|
+
const importCtx = cachedLoadContext(context, contextId);
|
|
27838
27901
|
const csvPath = path18.join(context.directory, importCtx.filePath);
|
|
27839
27902
|
const rulesMapping = loadRulesMapping(rulesDir);
|
|
27840
27903
|
const rulesFile = findRulesForCsv(csvPath, rulesMapping);
|
|
@@ -27896,7 +27959,7 @@ function formatUnknownPostingsLog(postings) {
|
|
|
27896
27959
|
return log;
|
|
27897
27960
|
}
|
|
27898
27961
|
async function executeImportStep(context, contextId, logger) {
|
|
27899
|
-
const importContext =
|
|
27962
|
+
const importContext = cachedLoadContext(context, contextId);
|
|
27900
27963
|
logger?.startSection(`Step 3: Import Transactions (${importContext.accountNumber || contextId})`);
|
|
27901
27964
|
logger?.logStep("Import", "start");
|
|
27902
27965
|
const importResult = await importStatements(context.directory, context.agent, {
|
|
@@ -27934,7 +27997,7 @@ async function executeImportStep(context, contextId, logger) {
|
|
|
27934
27997
|
error: importParsed.error
|
|
27935
27998
|
});
|
|
27936
27999
|
if (importParsed.success) {
|
|
27937
|
-
|
|
28000
|
+
cachedUpdateContext(context, contextId, {
|
|
27938
28001
|
rulesFile: importParsed.files?.[0]?.rulesFile,
|
|
27939
28002
|
yearJournal: importParsed.files?.[0]?.yearJournal,
|
|
27940
28003
|
transactionCount: importParsed.summary?.totalTransactions
|
|
@@ -27962,7 +28025,7 @@ async function executeImportStep(context, contextId, logger) {
|
|
|
27962
28025
|
logger?.endSection();
|
|
27963
28026
|
}
|
|
27964
28027
|
async function executeReconcileStep(context, contextId, logger) {
|
|
27965
|
-
const importContext =
|
|
28028
|
+
const importContext = cachedLoadContext(context, contextId);
|
|
27966
28029
|
logger?.startSection(`Step 4: Reconcile Balance (${importContext.accountNumber || contextId})`);
|
|
27967
28030
|
logger?.logStep("Reconcile", "start");
|
|
27968
28031
|
const reconcileResult = await reconcileStatement(context.directory, context.agent, {
|
|
@@ -27991,7 +28054,7 @@ async function executeReconcileStep(context, contextId, logger) {
|
|
|
27991
28054
|
error: reconcileParsed.error
|
|
27992
28055
|
});
|
|
27993
28056
|
if (reconcileParsed.success) {
|
|
27994
|
-
|
|
28057
|
+
cachedUpdateContext(context, contextId, {
|
|
27995
28058
|
reconciledAccount: reconcileParsed.account,
|
|
27996
28059
|
actualBalance: reconcileParsed.actualBalance,
|
|
27997
28060
|
lastTransactionDate: reconcileParsed.lastTransactionDate,
|
|
@@ -28038,7 +28101,8 @@ async function importPipeline(directory, agent, options, configLoader = loadImpo
|
|
|
28038
28101
|
configLoader,
|
|
28039
28102
|
hledgerExecutor,
|
|
28040
28103
|
result,
|
|
28041
|
-
generatedAccounts: new Set
|
|
28104
|
+
generatedAccounts: new Set,
|
|
28105
|
+
contextCache: new Map
|
|
28042
28106
|
};
|
|
28043
28107
|
try {
|
|
28044
28108
|
const contextIds = await executeClassifyStep(context, logger);
|
|
@@ -28053,10 +28117,9 @@ async function importPipeline(directory, agent, options, configLoader = loadImpo
|
|
|
28053
28117
|
await executeSwissquotePreprocessStep(context, contextIds, logger);
|
|
28054
28118
|
await executeIbkrPreprocessStep(context, contextIds, logger);
|
|
28055
28119
|
if (context.generatedAccounts.size > 0) {
|
|
28056
|
-
const firstCtx = contextIds.map((id) =>
|
|
28120
|
+
const firstCtx = contextIds.map((id) => cachedLoadContext(context, id)).find((c) => c.provider === "revolut");
|
|
28057
28121
|
if (firstCtx) {
|
|
28058
|
-
const
|
|
28059
|
-
const year = yearMatch ? parseInt(yearMatch[1], 10) : new Date().getFullYear();
|
|
28122
|
+
const year = getTransactionYear(firstCtx);
|
|
28060
28123
|
const yearJournalPath = ensureYearJournalExists(context.directory, year);
|
|
28061
28124
|
const declResult = ensureAccountDeclarations(yearJournalPath, context.generatedAccounts);
|
|
28062
28125
|
if (declResult.added.length > 0) {
|
|
@@ -28064,12 +28127,12 @@ async function importPipeline(directory, agent, options, configLoader = loadImpo
|
|
|
28064
28127
|
}
|
|
28065
28128
|
}
|
|
28066
28129
|
}
|
|
28067
|
-
const importConfig =
|
|
28130
|
+
const importConfig = context.configLoader(context.directory);
|
|
28068
28131
|
const pendingDir = path18.join(context.directory, importConfig.paths.pending);
|
|
28069
28132
|
const doneDir = path18.join(context.directory, importConfig.paths.done);
|
|
28070
28133
|
const orderedContextIds = [...contextIds].sort((a, b) => {
|
|
28071
|
-
const ctxA =
|
|
28072
|
-
const ctxB =
|
|
28134
|
+
const ctxA = cachedLoadContext(context, a);
|
|
28135
|
+
const ctxB = cachedLoadContext(context, b);
|
|
28073
28136
|
const orderA = importConfig.providers[ctxA.provider]?.importOrder ?? 0;
|
|
28074
28137
|
const orderB = importConfig.providers[ctxB.provider]?.importOrder ?? 0;
|
|
28075
28138
|
if (orderA !== orderB)
|
|
@@ -28080,7 +28143,7 @@ async function importPipeline(directory, agent, options, configLoader = loadImpo
|
|
|
28080
28143
|
});
|
|
28081
28144
|
let totalTransactions = 0;
|
|
28082
28145
|
for (const contextId of orderedContextIds) {
|
|
28083
|
-
const importContext =
|
|
28146
|
+
const importContext = cachedLoadContext(context, contextId);
|
|
28084
28147
|
logger.info(`Processing: ${importContext.filename} (${importContext.accountNumber || "unknown account"})`);
|
|
28085
28148
|
try {
|
|
28086
28149
|
await executeAccountDeclarationsStep(context, contextId, logger);
|
|
@@ -28170,17 +28233,137 @@ This tool orchestrates the full import workflow:
|
|
|
28170
28233
|
});
|
|
28171
28234
|
}
|
|
28172
28235
|
});
|
|
28173
|
-
// src/tools/
|
|
28174
|
-
import * as fs25 from "fs";
|
|
28236
|
+
// src/tools/generate-btc-purchases.ts
|
|
28175
28237
|
import * as path19 from "path";
|
|
28238
|
+
import * as fs25 from "fs";
|
|
28239
|
+
function findFiatCsvPaths(directory, pendingDir, provider) {
|
|
28240
|
+
const providerDir = path19.join(directory, pendingDir, provider);
|
|
28241
|
+
if (!fs25.existsSync(providerDir))
|
|
28242
|
+
return [];
|
|
28243
|
+
const csvPaths = [];
|
|
28244
|
+
const entries = fs25.readdirSync(providerDir, { withFileTypes: true });
|
|
28245
|
+
for (const entry of entries) {
|
|
28246
|
+
if (!entry.isDirectory())
|
|
28247
|
+
continue;
|
|
28248
|
+
if (entry.name === "btc")
|
|
28249
|
+
continue;
|
|
28250
|
+
const csvFiles = findCsvFiles(path19.join(providerDir, entry.name), { fullPaths: true });
|
|
28251
|
+
csvPaths.push(...csvFiles);
|
|
28252
|
+
}
|
|
28253
|
+
return csvPaths;
|
|
28254
|
+
}
|
|
28255
|
+
function findBtcCsvPath(directory, pendingDir, provider) {
|
|
28256
|
+
const btcDir = path19.join(directory, pendingDir, provider, "btc");
|
|
28257
|
+
const csvFiles = findCsvFiles(btcDir, { fullPaths: true });
|
|
28258
|
+
return csvFiles[0];
|
|
28259
|
+
}
|
|
28260
|
+
function determineYear(csvPaths) {
|
|
28261
|
+
for (const csvPath of csvPaths) {
|
|
28262
|
+
const match2 = path19.basename(csvPath).match(/(\d{4})-\d{2}-\d{2}/);
|
|
28263
|
+
if (match2)
|
|
28264
|
+
return parseInt(match2[1], 10);
|
|
28265
|
+
}
|
|
28266
|
+
return new Date().getFullYear();
|
|
28267
|
+
}
|
|
28268
|
+
async function generateBtcPurchases(directory, agent, options = {}, configLoader = loadImportConfig) {
|
|
28269
|
+
const restrictionError = checkAccountantAgent(agent, "generate BTC purchases");
|
|
28270
|
+
if (restrictionError)
|
|
28271
|
+
return restrictionError;
|
|
28272
|
+
const logger = createImportLogger(directory, undefined, "btc-purchases");
|
|
28273
|
+
try {
|
|
28274
|
+
logger.startSection("Generate BTC Purchase Entries", 1);
|
|
28275
|
+
let config2;
|
|
28276
|
+
try {
|
|
28277
|
+
config2 = configLoader(directory);
|
|
28278
|
+
} catch (err) {
|
|
28279
|
+
return buildToolErrorResult(err instanceof Error ? err.message : String(err));
|
|
28280
|
+
}
|
|
28281
|
+
const provider = options.provider || "revolut";
|
|
28282
|
+
const fiatCsvPaths = findFiatCsvPaths(directory, config2.paths.pending, provider);
|
|
28283
|
+
if (fiatCsvPaths.length === 0) {
|
|
28284
|
+
logger.info("No fiat CSV files found in pending directories");
|
|
28285
|
+
return buildToolSuccessResult({
|
|
28286
|
+
message: "No fiat CSV files found in pending directories",
|
|
28287
|
+
matchCount: 0,
|
|
28288
|
+
entriesAdded: 0
|
|
28289
|
+
});
|
|
28290
|
+
}
|
|
28291
|
+
logger.info(`Found ${fiatCsvPaths.length} fiat CSV file(s)`);
|
|
28292
|
+
const btcCsvPath = findBtcCsvPath(directory, config2.paths.pending, provider);
|
|
28293
|
+
if (!btcCsvPath) {
|
|
28294
|
+
logger.info("No BTC CSV file found in pending btc directory");
|
|
28295
|
+
return buildToolSuccessResult({
|
|
28296
|
+
message: "No BTC CSV file found in pending btc directory",
|
|
28297
|
+
matchCount: 0,
|
|
28298
|
+
entriesAdded: 0
|
|
28299
|
+
});
|
|
28300
|
+
}
|
|
28301
|
+
logger.info(`Found BTC CSV: ${btcCsvPath}`);
|
|
28302
|
+
const allCsvPaths = [...fiatCsvPaths, btcCsvPath];
|
|
28303
|
+
const year = determineYear(allCsvPaths);
|
|
28304
|
+
const yearJournalPath = ensureYearJournalExists(directory, year);
|
|
28305
|
+
logger.info(`Year journal: ${yearJournalPath}`);
|
|
28306
|
+
const result = generateBtcPurchaseJournal(fiatCsvPaths, btcCsvPath, yearJournalPath, logger);
|
|
28307
|
+
logger.logStep("Generate BTC Purchases", "success", `${result.entriesAdded} entries added`);
|
|
28308
|
+
logger.endSection();
|
|
28309
|
+
return buildToolSuccessResult({
|
|
28310
|
+
matchCount: result.matchCount,
|
|
28311
|
+
entriesAdded: result.entriesAdded,
|
|
28312
|
+
skippedDuplicates: result.skippedDuplicates,
|
|
28313
|
+
unmatchedFiat: result.unmatchedFiat.length,
|
|
28314
|
+
unmatchedBtc: result.unmatchedBtc.length,
|
|
28315
|
+
yearJournal: path19.relative(directory, yearJournalPath)
|
|
28316
|
+
});
|
|
28317
|
+
} catch (err) {
|
|
28318
|
+
logger.error("Failed to generate BTC purchases", err);
|
|
28319
|
+
return buildToolErrorResult(err instanceof Error ? err.message : String(err));
|
|
28320
|
+
} finally {
|
|
28321
|
+
logger.endSection();
|
|
28322
|
+
await logger.flush();
|
|
28323
|
+
}
|
|
28324
|
+
}
|
|
28325
|
+
var generate_btc_purchases_default = tool({
|
|
28326
|
+
description: `ACCOUNTANT AGENT ONLY: Generate BTC purchase journal entries from Revolut CSVs.
|
|
28327
|
+
|
|
28328
|
+
Cross-references Revolut fiat account CSVs (CHF/EUR/USD) with BTC crypto CSV
|
|
28329
|
+
to produce equity conversion entries for Bitcoin purchases.
|
|
28330
|
+
|
|
28331
|
+
**What it does:**
|
|
28332
|
+
- Scans pending directories for fiat and BTC CSV files
|
|
28333
|
+
- Matches fiat "Transfer to Digital Assets" rows with BTC "Buy" rows by timestamp
|
|
28334
|
+
- Handles REVX_TRANSFER \u2192 Other \u2192 Buy multi-step purchase flows
|
|
28335
|
+
- Generates hledger journal entries with equity conversion postings
|
|
28336
|
+
- Deduplicates to prevent double entries on re-run
|
|
28337
|
+
- Appends entries to the year journal
|
|
28338
|
+
|
|
28339
|
+
**Prerequisites:**
|
|
28340
|
+
- CSV files must be classified (in import/pending/revolut/{currency}/)
|
|
28341
|
+
- hledger rules files should have skip directives for BTC-related rows
|
|
28342
|
+
|
|
28343
|
+
**Usage:**
|
|
28344
|
+
- generate-btc-purchases (scans revolut pending dirs)
|
|
28345
|
+
- generate-btc-purchases --provider revolut`,
|
|
28346
|
+
args: {
|
|
28347
|
+
provider: tool.schema.string().optional().describe('Provider name (default: "revolut")')
|
|
28348
|
+
},
|
|
28349
|
+
async execute(params, context) {
|
|
28350
|
+
const { directory, agent } = context;
|
|
28351
|
+
return generateBtcPurchases(directory, agent, {
|
|
28352
|
+
provider: params.provider
|
|
28353
|
+
});
|
|
28354
|
+
}
|
|
28355
|
+
});
|
|
28356
|
+
// src/tools/init-directories.ts
|
|
28357
|
+
import * as fs26 from "fs";
|
|
28358
|
+
import * as path20 from "path";
|
|
28176
28359
|
async function initDirectories(directory) {
|
|
28177
28360
|
try {
|
|
28178
28361
|
const config2 = loadImportConfig(directory);
|
|
28179
28362
|
const directoriesCreated = [];
|
|
28180
28363
|
const gitkeepFiles = [];
|
|
28181
|
-
const importBase =
|
|
28182
|
-
if (!
|
|
28183
|
-
|
|
28364
|
+
const importBase = path20.join(directory, "import");
|
|
28365
|
+
if (!fs26.existsSync(importBase)) {
|
|
28366
|
+
fs26.mkdirSync(importBase, { recursive: true });
|
|
28184
28367
|
directoriesCreated.push("import");
|
|
28185
28368
|
}
|
|
28186
28369
|
const pathsToCreate = [
|
|
@@ -28190,20 +28373,20 @@ async function initDirectories(directory) {
|
|
|
28190
28373
|
{ key: "unrecognized", path: config2.paths.unrecognized }
|
|
28191
28374
|
];
|
|
28192
28375
|
for (const { path: dirPath } of pathsToCreate) {
|
|
28193
|
-
const fullPath =
|
|
28194
|
-
if (!
|
|
28195
|
-
|
|
28376
|
+
const fullPath = path20.join(directory, dirPath);
|
|
28377
|
+
if (!fs26.existsSync(fullPath)) {
|
|
28378
|
+
fs26.mkdirSync(fullPath, { recursive: true });
|
|
28196
28379
|
directoriesCreated.push(dirPath);
|
|
28197
28380
|
}
|
|
28198
|
-
const gitkeepPath =
|
|
28199
|
-
if (!
|
|
28200
|
-
|
|
28201
|
-
gitkeepFiles.push(
|
|
28381
|
+
const gitkeepPath = path20.join(fullPath, ".gitkeep");
|
|
28382
|
+
if (!fs26.existsSync(gitkeepPath)) {
|
|
28383
|
+
fs26.writeFileSync(gitkeepPath, "");
|
|
28384
|
+
gitkeepFiles.push(path20.join(dirPath, ".gitkeep"));
|
|
28202
28385
|
}
|
|
28203
28386
|
}
|
|
28204
|
-
const gitignorePath =
|
|
28387
|
+
const gitignorePath = path20.join(importBase, ".gitignore");
|
|
28205
28388
|
let gitignoreCreated = false;
|
|
28206
|
-
if (!
|
|
28389
|
+
if (!fs26.existsSync(gitignorePath)) {
|
|
28207
28390
|
const gitignoreContent = `# Ignore CSV/PDF files in temporary directories
|
|
28208
28391
|
/incoming/*.csv
|
|
28209
28392
|
/incoming/*.pdf
|
|
@@ -28221,7 +28404,7 @@ async function initDirectories(directory) {
|
|
|
28221
28404
|
.DS_Store
|
|
28222
28405
|
Thumbs.db
|
|
28223
28406
|
`;
|
|
28224
|
-
|
|
28407
|
+
fs26.writeFileSync(gitignorePath, gitignoreContent);
|
|
28225
28408
|
gitignoreCreated = true;
|
|
28226
28409
|
}
|
|
28227
28410
|
const parts = [];
|
|
@@ -28296,126 +28479,7 @@ You can now drop CSV files into import/incoming/ and run import-pipeline.`);
|
|
|
28296
28479
|
`);
|
|
28297
28480
|
}
|
|
28298
28481
|
});
|
|
28299
|
-
// src/tools/generate-btc-purchases.ts
|
|
28300
|
-
import * as path20 from "path";
|
|
28301
|
-
import * as fs26 from "fs";
|
|
28302
|
-
function findFiatCsvPaths(directory, pendingDir, provider) {
|
|
28303
|
-
const providerDir = path20.join(directory, pendingDir, provider);
|
|
28304
|
-
if (!fs26.existsSync(providerDir))
|
|
28305
|
-
return [];
|
|
28306
|
-
const csvPaths = [];
|
|
28307
|
-
const entries = fs26.readdirSync(providerDir, { withFileTypes: true });
|
|
28308
|
-
for (const entry of entries) {
|
|
28309
|
-
if (!entry.isDirectory())
|
|
28310
|
-
continue;
|
|
28311
|
-
if (entry.name === "btc")
|
|
28312
|
-
continue;
|
|
28313
|
-
const csvFiles = findCsvFiles(path20.join(providerDir, entry.name), { fullPaths: true });
|
|
28314
|
-
csvPaths.push(...csvFiles);
|
|
28315
|
-
}
|
|
28316
|
-
return csvPaths;
|
|
28317
|
-
}
|
|
28318
|
-
function findBtcCsvPath(directory, pendingDir, provider) {
|
|
28319
|
-
const btcDir = path20.join(directory, pendingDir, provider, "btc");
|
|
28320
|
-
const csvFiles = findCsvFiles(btcDir, { fullPaths: true });
|
|
28321
|
-
return csvFiles[0];
|
|
28322
|
-
}
|
|
28323
|
-
function determineYear(csvPaths) {
|
|
28324
|
-
for (const csvPath of csvPaths) {
|
|
28325
|
-
const match2 = path20.basename(csvPath).match(/(\d{4})-\d{2}-\d{2}/);
|
|
28326
|
-
if (match2)
|
|
28327
|
-
return parseInt(match2[1], 10);
|
|
28328
|
-
}
|
|
28329
|
-
return new Date().getFullYear();
|
|
28330
|
-
}
|
|
28331
|
-
async function generateBtcPurchases(directory, agent, options = {}, configLoader = loadImportConfig) {
|
|
28332
|
-
const restrictionError = checkAccountantAgent(agent, "generate BTC purchases");
|
|
28333
|
-
if (restrictionError)
|
|
28334
|
-
return restrictionError;
|
|
28335
|
-
const logger = createImportLogger(directory, undefined, "btc-purchases");
|
|
28336
|
-
try {
|
|
28337
|
-
logger.startSection("Generate BTC Purchase Entries", 1);
|
|
28338
|
-
let config2;
|
|
28339
|
-
try {
|
|
28340
|
-
config2 = configLoader(directory);
|
|
28341
|
-
} catch (err) {
|
|
28342
|
-
return buildToolErrorResult(err instanceof Error ? err.message : String(err));
|
|
28343
|
-
}
|
|
28344
|
-
const provider = options.provider || "revolut";
|
|
28345
|
-
const fiatCsvPaths = findFiatCsvPaths(directory, config2.paths.pending, provider);
|
|
28346
|
-
if (fiatCsvPaths.length === 0) {
|
|
28347
|
-
logger.info("No fiat CSV files found in pending directories");
|
|
28348
|
-
return buildToolSuccessResult({
|
|
28349
|
-
message: "No fiat CSV files found in pending directories",
|
|
28350
|
-
matchCount: 0,
|
|
28351
|
-
entriesAdded: 0
|
|
28352
|
-
});
|
|
28353
|
-
}
|
|
28354
|
-
logger.info(`Found ${fiatCsvPaths.length} fiat CSV file(s)`);
|
|
28355
|
-
const btcCsvPath = findBtcCsvPath(directory, config2.paths.pending, provider);
|
|
28356
|
-
if (!btcCsvPath) {
|
|
28357
|
-
logger.info("No BTC CSV file found in pending btc directory");
|
|
28358
|
-
return buildToolSuccessResult({
|
|
28359
|
-
message: "No BTC CSV file found in pending btc directory",
|
|
28360
|
-
matchCount: 0,
|
|
28361
|
-
entriesAdded: 0
|
|
28362
|
-
});
|
|
28363
|
-
}
|
|
28364
|
-
logger.info(`Found BTC CSV: ${btcCsvPath}`);
|
|
28365
|
-
const allCsvPaths = [...fiatCsvPaths, btcCsvPath];
|
|
28366
|
-
const year = determineYear(allCsvPaths);
|
|
28367
|
-
const yearJournalPath = ensureYearJournalExists(directory, year);
|
|
28368
|
-
logger.info(`Year journal: ${yearJournalPath}`);
|
|
28369
|
-
const result = generateBtcPurchaseJournal(fiatCsvPaths, btcCsvPath, yearJournalPath, logger);
|
|
28370
|
-
logger.logStep("Generate BTC Purchases", "success", `${result.entriesAdded} entries added`);
|
|
28371
|
-
logger.endSection();
|
|
28372
|
-
return buildToolSuccessResult({
|
|
28373
|
-
matchCount: result.matchCount,
|
|
28374
|
-
entriesAdded: result.entriesAdded,
|
|
28375
|
-
skippedDuplicates: result.skippedDuplicates,
|
|
28376
|
-
unmatchedFiat: result.unmatchedFiat.length,
|
|
28377
|
-
unmatchedBtc: result.unmatchedBtc.length,
|
|
28378
|
-
yearJournal: path20.relative(directory, yearJournalPath)
|
|
28379
|
-
});
|
|
28380
|
-
} catch (err) {
|
|
28381
|
-
logger.error("Failed to generate BTC purchases", err);
|
|
28382
|
-
return buildToolErrorResult(err instanceof Error ? err.message : String(err));
|
|
28383
|
-
} finally {
|
|
28384
|
-
logger.endSection();
|
|
28385
|
-
await logger.flush();
|
|
28386
|
-
}
|
|
28387
|
-
}
|
|
28388
|
-
var generate_btc_purchases_default = tool({
|
|
28389
|
-
description: `ACCOUNTANT AGENT ONLY: Generate BTC purchase journal entries from Revolut CSVs.
|
|
28390
|
-
|
|
28391
|
-
Cross-references Revolut fiat account CSVs (CHF/EUR/USD) with BTC crypto CSV
|
|
28392
|
-
to produce equity conversion entries for Bitcoin purchases.
|
|
28393
|
-
|
|
28394
|
-
**What it does:**
|
|
28395
|
-
- Scans pending directories for fiat and BTC CSV files
|
|
28396
|
-
- Matches fiat "Transfer to Digital Assets" rows with BTC "Buy" rows by timestamp
|
|
28397
|
-
- Handles REVX_TRANSFER \u2192 Other \u2192 Buy multi-step purchase flows
|
|
28398
|
-
- Generates hledger journal entries with equity conversion postings
|
|
28399
|
-
- Deduplicates to prevent double entries on re-run
|
|
28400
|
-
- Appends entries to the year journal
|
|
28401
28482
|
|
|
28402
|
-
**Prerequisites:**
|
|
28403
|
-
- CSV files must be classified (in import/pending/revolut/{currency}/)
|
|
28404
|
-
- hledger rules files should have skip directives for BTC-related rows
|
|
28405
|
-
|
|
28406
|
-
**Usage:**
|
|
28407
|
-
- generate-btc-purchases (scans revolut pending dirs)
|
|
28408
|
-
- generate-btc-purchases --provider revolut`,
|
|
28409
|
-
args: {
|
|
28410
|
-
provider: tool.schema.string().optional().describe('Provider name (default: "revolut")')
|
|
28411
|
-
},
|
|
28412
|
-
async execute(params, context) {
|
|
28413
|
-
const { directory, agent } = context;
|
|
28414
|
-
return generateBtcPurchases(directory, agent, {
|
|
28415
|
-
provider: params.provider
|
|
28416
|
-
});
|
|
28417
|
-
}
|
|
28418
|
-
});
|
|
28419
28483
|
// src/index.ts
|
|
28420
28484
|
var __dirname2 = dirname10(fileURLToPath3(import.meta.url));
|
|
28421
28485
|
var AGENT_FILE = join18(__dirname2, "..", "agent", "accountant.md");
|
|
@@ -28428,7 +28492,8 @@ var AccountantPlugin = async () => {
|
|
|
28428
28492
|
"import-statements": import_statements_default,
|
|
28429
28493
|
"reconcile-statements": reconcile_statement_default,
|
|
28430
28494
|
"import-pipeline": import_pipeline_default,
|
|
28431
|
-
"generate-btc-purchases": generate_btc_purchases_default
|
|
28495
|
+
"generate-btc-purchases": generate_btc_purchases_default,
|
|
28496
|
+
"init-directories": init_directories_default
|
|
28432
28497
|
},
|
|
28433
28498
|
config: async (config2) => {
|
|
28434
28499
|
if (agent) {
|
package/package.json
CHANGED