@fuzzle/opencode-accountant 0.16.6 → 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 +314 -222
- 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() {
|
|
@@ -26903,6 +26941,16 @@ function generateIbkrBuyEntry(trade, logger) {
|
|
|
26903
26941
|
account: `assets:broker:ibkr:${trade.currency.toLowerCase()}`,
|
|
26904
26942
|
amount: formatAmount(-cashOut, trade.currency)
|
|
26905
26943
|
});
|
|
26944
|
+
const computedNetCents = Math.round(totalCost * 100) + Math.round(fees * 100);
|
|
26945
|
+
const brokerNetCents = Math.round(cashOut * 100);
|
|
26946
|
+
const roundingCents = brokerNetCents - computedNetCents;
|
|
26947
|
+
if (roundingCents !== 0) {
|
|
26948
|
+
postings.push({
|
|
26949
|
+
account: "equity:rounding",
|
|
26950
|
+
amount: formatAmount(roundingCents / 100, trade.currency)
|
|
26951
|
+
});
|
|
26952
|
+
logger?.debug(`Rounding adjustment: ${roundingCents / 100} ${trade.currency}`);
|
|
26953
|
+
}
|
|
26906
26954
|
let entry = `${trade.date} ${description}
|
|
26907
26955
|
`;
|
|
26908
26956
|
entry += ` ; ibkr:account:${trade.account}
|
|
@@ -26952,6 +27000,16 @@ function generateIbkrSellEntry(trade, consumed, logger) {
|
|
|
26952
27000
|
amount: formatAmount(-capitalGain, trade.currency)
|
|
26953
27001
|
});
|
|
26954
27002
|
}
|
|
27003
|
+
const computedNetCents = Math.round(saleProceeds * 100) - Math.round(fees * 100);
|
|
27004
|
+
const brokerNetCents = Math.round(Math.abs(cashIn) * 100);
|
|
27005
|
+
const roundingCents = computedNetCents - brokerNetCents;
|
|
27006
|
+
if (roundingCents !== 0) {
|
|
27007
|
+
postings.push({
|
|
27008
|
+
account: "equity:rounding",
|
|
27009
|
+
amount: formatAmount(roundingCents / 100, trade.currency)
|
|
27010
|
+
});
|
|
27011
|
+
logger?.debug(`Rounding adjustment: ${roundingCents / 100} ${trade.currency}`);
|
|
27012
|
+
}
|
|
26955
27013
|
let entry = `${trade.date} ${description}
|
|
26956
27014
|
`;
|
|
26957
27015
|
entry += ` ; ibkr:account:${trade.account}
|
|
@@ -26981,6 +27039,16 @@ function generateIbkrDividendEntry(dividend, logger) {
|
|
|
26981
27039
|
account: `income:dividends:${dividend.symbol}`,
|
|
26982
27040
|
amount: formatAmount(-dividend.grossAmount, dividend.currency)
|
|
26983
27041
|
});
|
|
27042
|
+
const computedNetCents = Math.round(dividend.grossAmount * 100) - Math.round(dividend.withholdingTax * 100);
|
|
27043
|
+
const brokerNetCents = Math.round(dividend.netAmount * 100);
|
|
27044
|
+
const roundingCents = brokerNetCents - computedNetCents;
|
|
27045
|
+
if (roundingCents !== 0) {
|
|
27046
|
+
postings.push({
|
|
27047
|
+
account: "equity:rounding",
|
|
27048
|
+
amount: formatAmount(roundingCents / 100, dividend.currency)
|
|
27049
|
+
});
|
|
27050
|
+
logger?.debug(`Rounding adjustment: ${roundingCents / 100} ${dividend.currency}`);
|
|
27051
|
+
}
|
|
26984
27052
|
let entry = `${dividend.date} ${description}
|
|
26985
27053
|
`;
|
|
26986
27054
|
entry += ` ; ibkr:account:${dividend.account}
|
|
@@ -27131,8 +27199,6 @@ async function preprocessIbkr(csvPath, directory, currency, year, lotInventoryPa
|
|
|
27131
27199
|
const usedAccounts = new Set;
|
|
27132
27200
|
const journalEntries = [];
|
|
27133
27201
|
for (const trade of trades) {
|
|
27134
|
-
rawSum += trade.netAmount;
|
|
27135
|
-
roundedSum += round2(trade.netAmount);
|
|
27136
27202
|
const totalCost = Math.abs(trade.grossAmount);
|
|
27137
27203
|
const unitPrice = trade.quantity !== 0 ? totalCost / Math.abs(trade.quantity) : 0;
|
|
27138
27204
|
const tradeEntry = {
|
|
@@ -27185,8 +27251,6 @@ async function preprocessIbkr(csvPath, directory, currency, year, lotInventoryPa
|
|
|
27185
27251
|
const dividendGroups = groupDividends(dividendTxns);
|
|
27186
27252
|
for (const group of dividendGroups) {
|
|
27187
27253
|
const netAmount = group.grossAmount - group.withholdingTax;
|
|
27188
|
-
rawSum += netAmount;
|
|
27189
|
-
roundedSum += round2(netAmount);
|
|
27190
27254
|
const dividendEntry = {
|
|
27191
27255
|
date: group.date,
|
|
27192
27256
|
account: group.account,
|
|
@@ -27231,6 +27295,7 @@ async function preprocessIbkr(csvPath, directory, currency, year, lotInventoryPa
|
|
|
27231
27295
|
const commodityJournalPath = path17.join(directory, "ledger", "investments", "commodities.journal");
|
|
27232
27296
|
ensureCommodityDeclarations(commodityJournalPath, tradedSymbols, logger);
|
|
27233
27297
|
}
|
|
27298
|
+
usedAccounts.add("equity:rounding");
|
|
27234
27299
|
if (usedAccounts.size > 0) {
|
|
27235
27300
|
const accountJournalPath = path17.join(directory, "ledger", "investments", "accounts.journal");
|
|
27236
27301
|
ensureInvestmentAccountDeclarations(accountJournalPath, usedAccounts, logger);
|
|
@@ -27309,6 +27374,19 @@ class NoTransactionsError extends Error {
|
|
|
27309
27374
|
this.name = "NoTransactionsError";
|
|
27310
27375
|
}
|
|
27311
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
|
+
}
|
|
27312
27390
|
function buildStepResult(success2, message, details) {
|
|
27313
27391
|
const result = { success: success2, message };
|
|
27314
27392
|
if (details !== undefined) {
|
|
@@ -27372,7 +27450,7 @@ async function executeClassifyStep(context, logger) {
|
|
|
27372
27450
|
function executePreprocessFiatStep(context, contextIds, logger) {
|
|
27373
27451
|
const fiatCsvPaths = [];
|
|
27374
27452
|
for (const contextId of contextIds) {
|
|
27375
|
-
const importCtx =
|
|
27453
|
+
const importCtx = cachedLoadContext(context, contextId);
|
|
27376
27454
|
if (importCtx.provider === "revolut" && importCtx.currency !== "btc") {
|
|
27377
27455
|
fiatCsvPaths.push(path18.join(context.directory, importCtx.filePath));
|
|
27378
27456
|
}
|
|
@@ -27416,7 +27494,7 @@ function executePreprocessFiatStep(context, contextIds, logger) {
|
|
|
27416
27494
|
function executePreprocessBtcStep(context, contextIds, logger) {
|
|
27417
27495
|
let btcCsvPath;
|
|
27418
27496
|
for (const contextId of contextIds) {
|
|
27419
|
-
const importCtx =
|
|
27497
|
+
const importCtx = cachedLoadContext(context, contextId);
|
|
27420
27498
|
if (importCtx.provider === "revolut" && importCtx.currency === "btc") {
|
|
27421
27499
|
btcCsvPath = path18.join(context.directory, importCtx.filePath);
|
|
27422
27500
|
break;
|
|
@@ -27449,7 +27527,7 @@ async function executeBtcPurchaseStep(context, contextIds, logger) {
|
|
|
27449
27527
|
const fiatCsvPaths = [];
|
|
27450
27528
|
let btcCsvPath;
|
|
27451
27529
|
for (const contextId of contextIds) {
|
|
27452
|
-
const importCtx =
|
|
27530
|
+
const importCtx = cachedLoadContext(context, contextId);
|
|
27453
27531
|
if (importCtx.provider !== "revolut")
|
|
27454
27532
|
continue;
|
|
27455
27533
|
const csvPath = path18.join(context.directory, importCtx.filePath);
|
|
@@ -27465,29 +27543,39 @@ async function executeBtcPurchaseStep(context, contextIds, logger) {
|
|
|
27465
27543
|
}
|
|
27466
27544
|
logger?.startSection("Step 1b: Generate BTC Purchase Entries");
|
|
27467
27545
|
logger?.logStep("BTC Purchases", "start");
|
|
27468
|
-
|
|
27469
|
-
|
|
27470
|
-
|
|
27471
|
-
|
|
27472
|
-
|
|
27473
|
-
|
|
27474
|
-
|
|
27475
|
-
|
|
27476
|
-
|
|
27477
|
-
|
|
27478
|
-
|
|
27479
|
-
|
|
27480
|
-
|
|
27481
|
-
|
|
27482
|
-
|
|
27483
|
-
|
|
27484
|
-
|
|
27485
|
-
})
|
|
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
|
+
}
|
|
27486
27574
|
}
|
|
27487
27575
|
async function executeCurrencyExchangeStep(context, contextIds, logger) {
|
|
27488
27576
|
const fiatContexts = [];
|
|
27489
27577
|
for (const contextId of contextIds) {
|
|
27490
|
-
const importCtx =
|
|
27578
|
+
const importCtx = cachedLoadContext(context, contextId);
|
|
27491
27579
|
if (importCtx.provider !== "revolut")
|
|
27492
27580
|
continue;
|
|
27493
27581
|
if (importCtx.currency === "btc")
|
|
@@ -27499,49 +27587,55 @@ async function executeCurrencyExchangeStep(context, contextIds, logger) {
|
|
|
27499
27587
|
logger?.info("Need at least 2 Revolut fiat CSVs for exchange matching, skipping");
|
|
27500
27588
|
return;
|
|
27501
27589
|
}
|
|
27502
|
-
logger?.startSection("Step
|
|
27590
|
+
logger?.startSection("Step 1d: Generate Currency Exchange Entries");
|
|
27503
27591
|
logger?.logStep("Currency Exchanges", "start");
|
|
27504
|
-
|
|
27505
|
-
|
|
27506
|
-
|
|
27507
|
-
|
|
27508
|
-
|
|
27509
|
-
|
|
27510
|
-
|
|
27511
|
-
|
|
27512
|
-
|
|
27513
|
-
|
|
27514
|
-
|
|
27515
|
-
|
|
27516
|
-
|
|
27517
|
-
|
|
27518
|
-
|
|
27519
|
-
|
|
27520
|
-
|
|
27521
|
-
|
|
27522
|
-
|
|
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
|
+
}
|
|
27523
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();
|
|
27524
27630
|
}
|
|
27525
|
-
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)`;
|
|
27526
|
-
logger?.logStep("Currency Exchanges", "success", message);
|
|
27527
|
-
logger?.endSection();
|
|
27528
|
-
context.result.steps.currencyExchanges = buildStepResult(true, message, {
|
|
27529
|
-
matchCount: result.matchCount,
|
|
27530
|
-
entriesAdded: result.entriesAdded,
|
|
27531
|
-
skippedDuplicates: result.skippedDuplicates
|
|
27532
|
-
});
|
|
27533
27631
|
}
|
|
27534
27632
|
async function executeSwissquotePreprocessStep(context, contextIds, logger) {
|
|
27535
27633
|
const swissquoteContexts = [];
|
|
27536
27634
|
for (const contextId of contextIds) {
|
|
27537
|
-
const importCtx =
|
|
27635
|
+
const importCtx = cachedLoadContext(context, contextId);
|
|
27538
27636
|
if (importCtx.provider === "swissquote") {
|
|
27539
27637
|
const csvPath = path18.join(context.directory, importCtx.filePath);
|
|
27540
|
-
const
|
|
27541
|
-
let year = new Date().getFullYear();
|
|
27542
|
-
if (toDateMatch) {
|
|
27543
|
-
year = parseInt(toDateMatch[1], 10);
|
|
27544
|
-
}
|
|
27638
|
+
const year = getTransactionYear(importCtx);
|
|
27545
27639
|
swissquoteContexts.push({
|
|
27546
27640
|
contextId,
|
|
27547
27641
|
csvPath,
|
|
@@ -27554,7 +27648,7 @@ async function executeSwissquotePreprocessStep(context, contextIds, logger) {
|
|
|
27554
27648
|
logger?.info("No Swissquote CSV found, skipping preprocessing");
|
|
27555
27649
|
return;
|
|
27556
27650
|
}
|
|
27557
|
-
logger?.startSection("Step
|
|
27651
|
+
logger?.startSection("Step 1e: Preprocess Swissquote CSV");
|
|
27558
27652
|
const config2 = context.configLoader(context.directory);
|
|
27559
27653
|
const swissquoteProvider = config2.providers?.["swissquote"];
|
|
27560
27654
|
const lotInventoryPath = swissquoteProvider?.lotInventoryPath ?? DEFAULT_LOT_INVENTORY_PATH;
|
|
@@ -27602,7 +27696,7 @@ async function executeSwissquotePreprocessStep(context, contextIds, logger) {
|
|
|
27602
27696
|
lastJournalFile = result.journalFile;
|
|
27603
27697
|
}
|
|
27604
27698
|
if (result.simpleTransactionsCsv) {
|
|
27605
|
-
|
|
27699
|
+
cachedUpdateContext(context, sqCtx.contextId, {
|
|
27606
27700
|
filePath: path18.relative(context.directory, result.simpleTransactionsCsv)
|
|
27607
27701
|
});
|
|
27608
27702
|
logger?.info(`Updated context ${sqCtx.contextId} to use filtered CSV: ${path18.basename(result.simpleTransactionsCsv)}`);
|
|
@@ -27644,14 +27738,10 @@ async function executeSwissquotePreprocessStep(context, contextIds, logger) {
|
|
|
27644
27738
|
async function executeIbkrPreprocessStep(context, contextIds, logger) {
|
|
27645
27739
|
const ibkrContexts = [];
|
|
27646
27740
|
for (const contextId of contextIds) {
|
|
27647
|
-
const importCtx =
|
|
27741
|
+
const importCtx = cachedLoadContext(context, contextId);
|
|
27648
27742
|
if (importCtx.provider === "ibkr") {
|
|
27649
27743
|
const csvPath = path18.join(context.directory, importCtx.filePath);
|
|
27650
|
-
const
|
|
27651
|
-
let year = new Date().getFullYear();
|
|
27652
|
-
if (dateMatch) {
|
|
27653
|
-
year = parseInt(dateMatch[1], 10);
|
|
27654
|
-
}
|
|
27744
|
+
const year = getTransactionYear(importCtx);
|
|
27655
27745
|
ibkrContexts.push({
|
|
27656
27746
|
contextId,
|
|
27657
27747
|
csvPath,
|
|
@@ -27696,7 +27786,7 @@ async function executeIbkrPreprocessStep(context, contextIds, logger) {
|
|
|
27696
27786
|
lastJournalFile = result.journalFile;
|
|
27697
27787
|
}
|
|
27698
27788
|
if (result.simpleTransactionsCsv) {
|
|
27699
|
-
|
|
27789
|
+
cachedUpdateContext(context, ibkrCtx.contextId, {
|
|
27700
27790
|
filePath: path18.relative(context.directory, result.simpleTransactionsCsv)
|
|
27701
27791
|
});
|
|
27702
27792
|
logger?.info(`Updated context ${ibkrCtx.contextId} to use filtered CSV: ${path18.basename(result.simpleTransactionsCsv)}`);
|
|
@@ -27725,7 +27815,7 @@ async function executeAccountDeclarationsStep(context, contextId, logger) {
|
|
|
27725
27815
|
logger?.logStep("Check Accounts", "start");
|
|
27726
27816
|
const config2 = context.configLoader(context.directory);
|
|
27727
27817
|
const rulesDir = path18.join(context.directory, config2.paths.rules);
|
|
27728
|
-
const importCtx =
|
|
27818
|
+
const importCtx = cachedLoadContext(context, contextId);
|
|
27729
27819
|
const csvPath = path18.join(context.directory, importCtx.filePath);
|
|
27730
27820
|
const csvFiles = [csvPath];
|
|
27731
27821
|
const rulesMapping = loadRulesMapping(rulesDir);
|
|
@@ -27807,7 +27897,7 @@ async function buildSuggestionContext(context, contextId, logger) {
|
|
|
27807
27897
|
const { loadExistingAccounts: loadExistingAccounts2, extractRulePatternsFromFile: extractRulePatternsFromFile2 } = await Promise.resolve().then(() => (init_accountSuggester(), exports_accountSuggester));
|
|
27808
27898
|
const config2 = context.configLoader(context.directory);
|
|
27809
27899
|
const rulesDir = path18.join(context.directory, config2.paths.rules);
|
|
27810
|
-
const importCtx =
|
|
27900
|
+
const importCtx = cachedLoadContext(context, contextId);
|
|
27811
27901
|
const csvPath = path18.join(context.directory, importCtx.filePath);
|
|
27812
27902
|
const rulesMapping = loadRulesMapping(rulesDir);
|
|
27813
27903
|
const rulesFile = findRulesForCsv(csvPath, rulesMapping);
|
|
@@ -27869,7 +27959,7 @@ function formatUnknownPostingsLog(postings) {
|
|
|
27869
27959
|
return log;
|
|
27870
27960
|
}
|
|
27871
27961
|
async function executeImportStep(context, contextId, logger) {
|
|
27872
|
-
const importContext =
|
|
27962
|
+
const importContext = cachedLoadContext(context, contextId);
|
|
27873
27963
|
logger?.startSection(`Step 3: Import Transactions (${importContext.accountNumber || contextId})`);
|
|
27874
27964
|
logger?.logStep("Import", "start");
|
|
27875
27965
|
const importResult = await importStatements(context.directory, context.agent, {
|
|
@@ -27907,7 +27997,7 @@ async function executeImportStep(context, contextId, logger) {
|
|
|
27907
27997
|
error: importParsed.error
|
|
27908
27998
|
});
|
|
27909
27999
|
if (importParsed.success) {
|
|
27910
|
-
|
|
28000
|
+
cachedUpdateContext(context, contextId, {
|
|
27911
28001
|
rulesFile: importParsed.files?.[0]?.rulesFile,
|
|
27912
28002
|
yearJournal: importParsed.files?.[0]?.yearJournal,
|
|
27913
28003
|
transactionCount: importParsed.summary?.totalTransactions
|
|
@@ -27935,7 +28025,7 @@ async function executeImportStep(context, contextId, logger) {
|
|
|
27935
28025
|
logger?.endSection();
|
|
27936
28026
|
}
|
|
27937
28027
|
async function executeReconcileStep(context, contextId, logger) {
|
|
27938
|
-
const importContext =
|
|
28028
|
+
const importContext = cachedLoadContext(context, contextId);
|
|
27939
28029
|
logger?.startSection(`Step 4: Reconcile Balance (${importContext.accountNumber || contextId})`);
|
|
27940
28030
|
logger?.logStep("Reconcile", "start");
|
|
27941
28031
|
const reconcileResult = await reconcileStatement(context.directory, context.agent, {
|
|
@@ -27964,7 +28054,7 @@ async function executeReconcileStep(context, contextId, logger) {
|
|
|
27964
28054
|
error: reconcileParsed.error
|
|
27965
28055
|
});
|
|
27966
28056
|
if (reconcileParsed.success) {
|
|
27967
|
-
|
|
28057
|
+
cachedUpdateContext(context, contextId, {
|
|
27968
28058
|
reconciledAccount: reconcileParsed.account,
|
|
27969
28059
|
actualBalance: reconcileParsed.actualBalance,
|
|
27970
28060
|
lastTransactionDate: reconcileParsed.lastTransactionDate,
|
|
@@ -28011,7 +28101,8 @@ async function importPipeline(directory, agent, options, configLoader = loadImpo
|
|
|
28011
28101
|
configLoader,
|
|
28012
28102
|
hledgerExecutor,
|
|
28013
28103
|
result,
|
|
28014
|
-
generatedAccounts: new Set
|
|
28104
|
+
generatedAccounts: new Set,
|
|
28105
|
+
contextCache: new Map
|
|
28015
28106
|
};
|
|
28016
28107
|
try {
|
|
28017
28108
|
const contextIds = await executeClassifyStep(context, logger);
|
|
@@ -28026,10 +28117,9 @@ async function importPipeline(directory, agent, options, configLoader = loadImpo
|
|
|
28026
28117
|
await executeSwissquotePreprocessStep(context, contextIds, logger);
|
|
28027
28118
|
await executeIbkrPreprocessStep(context, contextIds, logger);
|
|
28028
28119
|
if (context.generatedAccounts.size > 0) {
|
|
28029
|
-
const firstCtx = contextIds.map((id) =>
|
|
28120
|
+
const firstCtx = contextIds.map((id) => cachedLoadContext(context, id)).find((c) => c.provider === "revolut");
|
|
28030
28121
|
if (firstCtx) {
|
|
28031
|
-
const
|
|
28032
|
-
const year = yearMatch ? parseInt(yearMatch[1], 10) : new Date().getFullYear();
|
|
28122
|
+
const year = getTransactionYear(firstCtx);
|
|
28033
28123
|
const yearJournalPath = ensureYearJournalExists(context.directory, year);
|
|
28034
28124
|
const declResult = ensureAccountDeclarations(yearJournalPath, context.generatedAccounts);
|
|
28035
28125
|
if (declResult.added.length > 0) {
|
|
@@ -28037,12 +28127,12 @@ async function importPipeline(directory, agent, options, configLoader = loadImpo
|
|
|
28037
28127
|
}
|
|
28038
28128
|
}
|
|
28039
28129
|
}
|
|
28040
|
-
const importConfig =
|
|
28130
|
+
const importConfig = context.configLoader(context.directory);
|
|
28041
28131
|
const pendingDir = path18.join(context.directory, importConfig.paths.pending);
|
|
28042
28132
|
const doneDir = path18.join(context.directory, importConfig.paths.done);
|
|
28043
28133
|
const orderedContextIds = [...contextIds].sort((a, b) => {
|
|
28044
|
-
const ctxA =
|
|
28045
|
-
const ctxB =
|
|
28134
|
+
const ctxA = cachedLoadContext(context, a);
|
|
28135
|
+
const ctxB = cachedLoadContext(context, b);
|
|
28046
28136
|
const orderA = importConfig.providers[ctxA.provider]?.importOrder ?? 0;
|
|
28047
28137
|
const orderB = importConfig.providers[ctxB.provider]?.importOrder ?? 0;
|
|
28048
28138
|
if (orderA !== orderB)
|
|
@@ -28053,7 +28143,7 @@ async function importPipeline(directory, agent, options, configLoader = loadImpo
|
|
|
28053
28143
|
});
|
|
28054
28144
|
let totalTransactions = 0;
|
|
28055
28145
|
for (const contextId of orderedContextIds) {
|
|
28056
|
-
const importContext =
|
|
28146
|
+
const importContext = cachedLoadContext(context, contextId);
|
|
28057
28147
|
logger.info(`Processing: ${importContext.filename} (${importContext.accountNumber || "unknown account"})`);
|
|
28058
28148
|
try {
|
|
28059
28149
|
await executeAccountDeclarationsStep(context, contextId, logger);
|
|
@@ -28143,17 +28233,137 @@ This tool orchestrates the full import workflow:
|
|
|
28143
28233
|
});
|
|
28144
28234
|
}
|
|
28145
28235
|
});
|
|
28146
|
-
// src/tools/
|
|
28147
|
-
import * as fs25 from "fs";
|
|
28236
|
+
// src/tools/generate-btc-purchases.ts
|
|
28148
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";
|
|
28149
28359
|
async function initDirectories(directory) {
|
|
28150
28360
|
try {
|
|
28151
28361
|
const config2 = loadImportConfig(directory);
|
|
28152
28362
|
const directoriesCreated = [];
|
|
28153
28363
|
const gitkeepFiles = [];
|
|
28154
|
-
const importBase =
|
|
28155
|
-
if (!
|
|
28156
|
-
|
|
28364
|
+
const importBase = path20.join(directory, "import");
|
|
28365
|
+
if (!fs26.existsSync(importBase)) {
|
|
28366
|
+
fs26.mkdirSync(importBase, { recursive: true });
|
|
28157
28367
|
directoriesCreated.push("import");
|
|
28158
28368
|
}
|
|
28159
28369
|
const pathsToCreate = [
|
|
@@ -28163,20 +28373,20 @@ async function initDirectories(directory) {
|
|
|
28163
28373
|
{ key: "unrecognized", path: config2.paths.unrecognized }
|
|
28164
28374
|
];
|
|
28165
28375
|
for (const { path: dirPath } of pathsToCreate) {
|
|
28166
|
-
const fullPath =
|
|
28167
|
-
if (!
|
|
28168
|
-
|
|
28376
|
+
const fullPath = path20.join(directory, dirPath);
|
|
28377
|
+
if (!fs26.existsSync(fullPath)) {
|
|
28378
|
+
fs26.mkdirSync(fullPath, { recursive: true });
|
|
28169
28379
|
directoriesCreated.push(dirPath);
|
|
28170
28380
|
}
|
|
28171
|
-
const gitkeepPath =
|
|
28172
|
-
if (!
|
|
28173
|
-
|
|
28174
|
-
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"));
|
|
28175
28385
|
}
|
|
28176
28386
|
}
|
|
28177
|
-
const gitignorePath =
|
|
28387
|
+
const gitignorePath = path20.join(importBase, ".gitignore");
|
|
28178
28388
|
let gitignoreCreated = false;
|
|
28179
|
-
if (!
|
|
28389
|
+
if (!fs26.existsSync(gitignorePath)) {
|
|
28180
28390
|
const gitignoreContent = `# Ignore CSV/PDF files in temporary directories
|
|
28181
28391
|
/incoming/*.csv
|
|
28182
28392
|
/incoming/*.pdf
|
|
@@ -28194,7 +28404,7 @@ async function initDirectories(directory) {
|
|
|
28194
28404
|
.DS_Store
|
|
28195
28405
|
Thumbs.db
|
|
28196
28406
|
`;
|
|
28197
|
-
|
|
28407
|
+
fs26.writeFileSync(gitignorePath, gitignoreContent);
|
|
28198
28408
|
gitignoreCreated = true;
|
|
28199
28409
|
}
|
|
28200
28410
|
const parts = [];
|
|
@@ -28269,126 +28479,7 @@ You can now drop CSV files into import/incoming/ and run import-pipeline.`);
|
|
|
28269
28479
|
`);
|
|
28270
28480
|
}
|
|
28271
28481
|
});
|
|
28272
|
-
// src/tools/generate-btc-purchases.ts
|
|
28273
|
-
import * as path20 from "path";
|
|
28274
|
-
import * as fs26 from "fs";
|
|
28275
|
-
function findFiatCsvPaths(directory, pendingDir, provider) {
|
|
28276
|
-
const providerDir = path20.join(directory, pendingDir, provider);
|
|
28277
|
-
if (!fs26.existsSync(providerDir))
|
|
28278
|
-
return [];
|
|
28279
|
-
const csvPaths = [];
|
|
28280
|
-
const entries = fs26.readdirSync(providerDir, { withFileTypes: true });
|
|
28281
|
-
for (const entry of entries) {
|
|
28282
|
-
if (!entry.isDirectory())
|
|
28283
|
-
continue;
|
|
28284
|
-
if (entry.name === "btc")
|
|
28285
|
-
continue;
|
|
28286
|
-
const csvFiles = findCsvFiles(path20.join(providerDir, entry.name), { fullPaths: true });
|
|
28287
|
-
csvPaths.push(...csvFiles);
|
|
28288
|
-
}
|
|
28289
|
-
return csvPaths;
|
|
28290
|
-
}
|
|
28291
|
-
function findBtcCsvPath(directory, pendingDir, provider) {
|
|
28292
|
-
const btcDir = path20.join(directory, pendingDir, provider, "btc");
|
|
28293
|
-
const csvFiles = findCsvFiles(btcDir, { fullPaths: true });
|
|
28294
|
-
return csvFiles[0];
|
|
28295
|
-
}
|
|
28296
|
-
function determineYear(csvPaths) {
|
|
28297
|
-
for (const csvPath of csvPaths) {
|
|
28298
|
-
const match2 = path20.basename(csvPath).match(/(\d{4})-\d{2}-\d{2}/);
|
|
28299
|
-
if (match2)
|
|
28300
|
-
return parseInt(match2[1], 10);
|
|
28301
|
-
}
|
|
28302
|
-
return new Date().getFullYear();
|
|
28303
|
-
}
|
|
28304
|
-
async function generateBtcPurchases(directory, agent, options = {}, configLoader = loadImportConfig) {
|
|
28305
|
-
const restrictionError = checkAccountantAgent(agent, "generate BTC purchases");
|
|
28306
|
-
if (restrictionError)
|
|
28307
|
-
return restrictionError;
|
|
28308
|
-
const logger = createImportLogger(directory, undefined, "btc-purchases");
|
|
28309
|
-
try {
|
|
28310
|
-
logger.startSection("Generate BTC Purchase Entries", 1);
|
|
28311
|
-
let config2;
|
|
28312
|
-
try {
|
|
28313
|
-
config2 = configLoader(directory);
|
|
28314
|
-
} catch (err) {
|
|
28315
|
-
return buildToolErrorResult(err instanceof Error ? err.message : String(err));
|
|
28316
|
-
}
|
|
28317
|
-
const provider = options.provider || "revolut";
|
|
28318
|
-
const fiatCsvPaths = findFiatCsvPaths(directory, config2.paths.pending, provider);
|
|
28319
|
-
if (fiatCsvPaths.length === 0) {
|
|
28320
|
-
logger.info("No fiat CSV files found in pending directories");
|
|
28321
|
-
return buildToolSuccessResult({
|
|
28322
|
-
message: "No fiat CSV files found in pending directories",
|
|
28323
|
-
matchCount: 0,
|
|
28324
|
-
entriesAdded: 0
|
|
28325
|
-
});
|
|
28326
|
-
}
|
|
28327
|
-
logger.info(`Found ${fiatCsvPaths.length} fiat CSV file(s)`);
|
|
28328
|
-
const btcCsvPath = findBtcCsvPath(directory, config2.paths.pending, provider);
|
|
28329
|
-
if (!btcCsvPath) {
|
|
28330
|
-
logger.info("No BTC CSV file found in pending btc directory");
|
|
28331
|
-
return buildToolSuccessResult({
|
|
28332
|
-
message: "No BTC CSV file found in pending btc directory",
|
|
28333
|
-
matchCount: 0,
|
|
28334
|
-
entriesAdded: 0
|
|
28335
|
-
});
|
|
28336
|
-
}
|
|
28337
|
-
logger.info(`Found BTC CSV: ${btcCsvPath}`);
|
|
28338
|
-
const allCsvPaths = [...fiatCsvPaths, btcCsvPath];
|
|
28339
|
-
const year = determineYear(allCsvPaths);
|
|
28340
|
-
const yearJournalPath = ensureYearJournalExists(directory, year);
|
|
28341
|
-
logger.info(`Year journal: ${yearJournalPath}`);
|
|
28342
|
-
const result = generateBtcPurchaseJournal(fiatCsvPaths, btcCsvPath, yearJournalPath, logger);
|
|
28343
|
-
logger.logStep("Generate BTC Purchases", "success", `${result.entriesAdded} entries added`);
|
|
28344
|
-
logger.endSection();
|
|
28345
|
-
return buildToolSuccessResult({
|
|
28346
|
-
matchCount: result.matchCount,
|
|
28347
|
-
entriesAdded: result.entriesAdded,
|
|
28348
|
-
skippedDuplicates: result.skippedDuplicates,
|
|
28349
|
-
unmatchedFiat: result.unmatchedFiat.length,
|
|
28350
|
-
unmatchedBtc: result.unmatchedBtc.length,
|
|
28351
|
-
yearJournal: path20.relative(directory, yearJournalPath)
|
|
28352
|
-
});
|
|
28353
|
-
} catch (err) {
|
|
28354
|
-
logger.error("Failed to generate BTC purchases", err);
|
|
28355
|
-
return buildToolErrorResult(err instanceof Error ? err.message : String(err));
|
|
28356
|
-
} finally {
|
|
28357
|
-
logger.endSection();
|
|
28358
|
-
await logger.flush();
|
|
28359
|
-
}
|
|
28360
|
-
}
|
|
28361
|
-
var generate_btc_purchases_default = tool({
|
|
28362
|
-
description: `ACCOUNTANT AGENT ONLY: Generate BTC purchase journal entries from Revolut CSVs.
|
|
28363
28482
|
|
|
28364
|
-
Cross-references Revolut fiat account CSVs (CHF/EUR/USD) with BTC crypto CSV
|
|
28365
|
-
to produce equity conversion entries for Bitcoin purchases.
|
|
28366
|
-
|
|
28367
|
-
**What it does:**
|
|
28368
|
-
- Scans pending directories for fiat and BTC CSV files
|
|
28369
|
-
- Matches fiat "Transfer to Digital Assets" rows with BTC "Buy" rows by timestamp
|
|
28370
|
-
- Handles REVX_TRANSFER \u2192 Other \u2192 Buy multi-step purchase flows
|
|
28371
|
-
- Generates hledger journal entries with equity conversion postings
|
|
28372
|
-
- Deduplicates to prevent double entries on re-run
|
|
28373
|
-
- Appends entries to the year journal
|
|
28374
|
-
|
|
28375
|
-
**Prerequisites:**
|
|
28376
|
-
- CSV files must be classified (in import/pending/revolut/{currency}/)
|
|
28377
|
-
- hledger rules files should have skip directives for BTC-related rows
|
|
28378
|
-
|
|
28379
|
-
**Usage:**
|
|
28380
|
-
- generate-btc-purchases (scans revolut pending dirs)
|
|
28381
|
-
- generate-btc-purchases --provider revolut`,
|
|
28382
|
-
args: {
|
|
28383
|
-
provider: tool.schema.string().optional().describe('Provider name (default: "revolut")')
|
|
28384
|
-
},
|
|
28385
|
-
async execute(params, context) {
|
|
28386
|
-
const { directory, agent } = context;
|
|
28387
|
-
return generateBtcPurchases(directory, agent, {
|
|
28388
|
-
provider: params.provider
|
|
28389
|
-
});
|
|
28390
|
-
}
|
|
28391
|
-
});
|
|
28392
28483
|
// src/index.ts
|
|
28393
28484
|
var __dirname2 = dirname10(fileURLToPath3(import.meta.url));
|
|
28394
28485
|
var AGENT_FILE = join18(__dirname2, "..", "agent", "accountant.md");
|
|
@@ -28401,7 +28492,8 @@ var AccountantPlugin = async () => {
|
|
|
28401
28492
|
"import-statements": import_statements_default,
|
|
28402
28493
|
"reconcile-statements": reconcile_statement_default,
|
|
28403
28494
|
"import-pipeline": import_pipeline_default,
|
|
28404
|
-
"generate-btc-purchases": generate_btc_purchases_default
|
|
28495
|
+
"generate-btc-purchases": generate_btc_purchases_default,
|
|
28496
|
+
"init-directories": init_directories_default
|
|
28405
28497
|
},
|
|
28406
28498
|
config: async (config2) => {
|
|
28407
28499
|
if (agent) {
|