@fuzzle/opencode-accountant 0.5.2 → 0.5.4
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/agent/accountant.md +9 -9
- package/dist/index.js +86 -59
- package/package.json +1 -1
package/agent/accountant.md
CHANGED
|
@@ -3,13 +3,13 @@ description: Specialized agent for hledger accounting tasks and transaction mana
|
|
|
3
3
|
prompt: You are an accounting specialist with expertise in hledger and double-entry bookkeeping. Your role is to help maintain accurate financial records following the user's conventions.
|
|
4
4
|
mode: subagent
|
|
5
5
|
temperature: 0.1
|
|
6
|
-
|
|
6
|
+
maxSteps: 8
|
|
7
7
|
tools:
|
|
8
8
|
bash: true
|
|
9
9
|
edit: true
|
|
10
10
|
write: true
|
|
11
11
|
# MCP tools available: import-pipeline, fetch-currency-prices
|
|
12
|
-
|
|
12
|
+
permissions:
|
|
13
13
|
bash: allow
|
|
14
14
|
edit: allow
|
|
15
15
|
glob: allow
|
|
@@ -151,13 +151,13 @@ The following are MCP tools available to you. Always call these tools directly -
|
|
|
151
151
|
|
|
152
152
|
**Arguments:**
|
|
153
153
|
|
|
154
|
-
| Argument | Type | Default | Description
|
|
155
|
-
| ---------------- | ------- | ------- |
|
|
156
|
-
| `provider` | string | - | Logger metadata (does not filter CSV selection — use classify step for that)
|
|
157
|
-
| `currency` | string | - | Logger metadata (does not filter CSV selection — use classify step for that)
|
|
158
|
-
| `skipClassify` | boolean | `false` | Skip classification step
|
|
159
|
-
| `closingBalance` | string | - | Manual closing balance for reconciliation
|
|
160
|
-
| `account` | string | - | Manual account override (auto-detected from rules)
|
|
154
|
+
| Argument | Type | Default | Description |
|
|
155
|
+
| ---------------- | ------- | ------- | ---------------------------------------------------------------------------- |
|
|
156
|
+
| `provider` | string | - | Logger metadata (does not filter CSV selection — use classify step for that) |
|
|
157
|
+
| `currency` | string | - | Logger metadata (does not filter CSV selection — use classify step for that) |
|
|
158
|
+
| `skipClassify` | boolean | `false` | Skip classification step |
|
|
159
|
+
| `closingBalance` | string | - | Manual closing balance for reconciliation |
|
|
160
|
+
| `account` | string | - | Manual account override (auto-detected from rules) |
|
|
161
161
|
|
|
162
162
|
**Behavior:**
|
|
163
163
|
|
package/dist/index.js
CHANGED
|
@@ -2677,6 +2677,12 @@ var init_js_yaml = __esm(() => {
|
|
|
2677
2677
|
|
|
2678
2678
|
// src/utils/agentLoader.ts
|
|
2679
2679
|
import * as fs from "fs";
|
|
2680
|
+
function validateAgentFields(data, filePath) {
|
|
2681
|
+
const unknownFields = Object.keys(data).filter((key) => !VALID_AGENT_FIELDS.has(key));
|
|
2682
|
+
if (unknownFields.length > 0) {
|
|
2683
|
+
console.warn(`Warning: Unknown fields in ${filePath} frontmatter: ${unknownFields.join(", ")}. ` + `Valid fields are: ${Array.from(VALID_AGENT_FIELDS).join(", ")}`);
|
|
2684
|
+
}
|
|
2685
|
+
}
|
|
2680
2686
|
function loadAgent(filePath) {
|
|
2681
2687
|
if (!fs.existsSync(filePath)) {
|
|
2682
2688
|
return null;
|
|
@@ -2686,7 +2692,12 @@ function loadAgent(filePath) {
|
|
|
2686
2692
|
if (!match) {
|
|
2687
2693
|
throw new Error(`Invalid frontmatter format in ${filePath}`);
|
|
2688
2694
|
}
|
|
2689
|
-
const
|
|
2695
|
+
const raw = jsYaml.load(match[1]);
|
|
2696
|
+
if (typeof raw !== "object" || raw === null || typeof raw.description !== "string") {
|
|
2697
|
+
throw new Error(`Invalid frontmatter in ${filePath}: must be an object with a "description" string`);
|
|
2698
|
+
}
|
|
2699
|
+
const data = raw;
|
|
2700
|
+
validateAgentFields(data, filePath);
|
|
2690
2701
|
const { description, ...optional } = data;
|
|
2691
2702
|
return {
|
|
2692
2703
|
description,
|
|
@@ -2694,8 +2705,19 @@ function loadAgent(filePath) {
|
|
|
2694
2705
|
...Object.fromEntries(Object.entries(optional).filter(([, v]) => v !== undefined))
|
|
2695
2706
|
};
|
|
2696
2707
|
}
|
|
2708
|
+
var VALID_AGENT_FIELDS;
|
|
2697
2709
|
var init_agentLoader = __esm(() => {
|
|
2698
2710
|
init_js_yaml();
|
|
2711
|
+
VALID_AGENT_FIELDS = new Set([
|
|
2712
|
+
"description",
|
|
2713
|
+
"mode",
|
|
2714
|
+
"model",
|
|
2715
|
+
"temperature",
|
|
2716
|
+
"maxSteps",
|
|
2717
|
+
"disable",
|
|
2718
|
+
"tools",
|
|
2719
|
+
"permissions"
|
|
2720
|
+
]);
|
|
2699
2721
|
});
|
|
2700
2722
|
|
|
2701
2723
|
// node_modules/papaparse/papaparse.js
|
|
@@ -16943,6 +16965,13 @@ function findCsvFiles(baseDir, options = {}) {
|
|
|
16943
16965
|
}
|
|
16944
16966
|
return csvFiles.sort();
|
|
16945
16967
|
}
|
|
16968
|
+
function sortByMtimeNewestFirst(files) {
|
|
16969
|
+
return [...files].sort((a, b) => {
|
|
16970
|
+
const aStat = fs3.statSync(a);
|
|
16971
|
+
const bStat = fs3.statSync(b);
|
|
16972
|
+
return bStat.mtime.getTime() - aStat.mtime.getTime();
|
|
16973
|
+
});
|
|
16974
|
+
}
|
|
16946
16975
|
function ensureDirectory(dirPath) {
|
|
16947
16976
|
if (!fs3.existsSync(dirPath)) {
|
|
16948
16977
|
fs3.mkdirSync(dirPath, { recursive: true });
|
|
@@ -23238,6 +23267,10 @@ function calculateDifference(expected, actual) {
|
|
|
23238
23267
|
const currency = expectedParsed.currency || actualParsed.currency;
|
|
23239
23268
|
return currency ? `${currency} ${sign}${diff.toFixed(2)}` : `${sign}${diff.toFixed(2)}`;
|
|
23240
23269
|
}
|
|
23270
|
+
function formatBalance(amount, currency) {
|
|
23271
|
+
const formattedAmount = amount.toFixed(2);
|
|
23272
|
+
return currency ? `${currency} ${formattedAmount}` : formattedAmount;
|
|
23273
|
+
}
|
|
23241
23274
|
function balancesMatch(balance1, balance2) {
|
|
23242
23275
|
const parsed1 = parseBalance(balance1);
|
|
23243
23276
|
const parsed2 = parseBalance(balance2);
|
|
@@ -23261,13 +23294,13 @@ function parseCsvFile(csvPath, config2) {
|
|
|
23261
23294
|
const csvWithHeader = lines.slice(headerIndex).join(`
|
|
23262
23295
|
`);
|
|
23263
23296
|
const useFieldNames = config2.fieldNames.length > 0;
|
|
23264
|
-
const result = import_papaparse2.default.parse(csvWithHeader, {
|
|
23265
|
-
header: !useFieldNames,
|
|
23266
|
-
delimiter: config2.separator,
|
|
23267
|
-
skipEmptyLines: true
|
|
23268
|
-
});
|
|
23269
23297
|
if (useFieldNames) {
|
|
23270
|
-
const
|
|
23298
|
+
const result2 = import_papaparse2.default.parse(csvWithHeader, {
|
|
23299
|
+
header: false,
|
|
23300
|
+
delimiter: config2.separator,
|
|
23301
|
+
skipEmptyLines: true
|
|
23302
|
+
});
|
|
23303
|
+
const rawRows = result2.data;
|
|
23271
23304
|
const dataRows = rawRows.slice(1);
|
|
23272
23305
|
return dataRows.map((values) => {
|
|
23273
23306
|
const row = {};
|
|
@@ -23277,6 +23310,11 @@ function parseCsvFile(csvPath, config2) {
|
|
|
23277
23310
|
return row;
|
|
23278
23311
|
});
|
|
23279
23312
|
}
|
|
23313
|
+
const result = import_papaparse2.default.parse(csvWithHeader, {
|
|
23314
|
+
header: true,
|
|
23315
|
+
delimiter: config2.separator,
|
|
23316
|
+
skipEmptyLines: true
|
|
23317
|
+
});
|
|
23280
23318
|
return result.data;
|
|
23281
23319
|
}
|
|
23282
23320
|
function getRowAmount(row, amountFields) {
|
|
@@ -23400,23 +23438,16 @@ function buildSuccessResult3(files, summary, message) {
|
|
|
23400
23438
|
}
|
|
23401
23439
|
function findCsvFromRulesFile(rulesFile) {
|
|
23402
23440
|
const content = fs10.readFileSync(rulesFile, "utf-8");
|
|
23403
|
-
const
|
|
23404
|
-
if (!
|
|
23441
|
+
const sourcePath = parseSourceDirective(content);
|
|
23442
|
+
if (!sourcePath) {
|
|
23405
23443
|
return null;
|
|
23406
23444
|
}
|
|
23407
|
-
const
|
|
23408
|
-
const rulesDir = path9.dirname(rulesFile);
|
|
23409
|
-
const absolutePattern = path9.resolve(rulesDir, sourcePath);
|
|
23445
|
+
const absolutePattern = resolveSourcePath(sourcePath, rulesFile);
|
|
23410
23446
|
const matches = glob.sync(absolutePattern);
|
|
23411
23447
|
if (matches.length === 0) {
|
|
23412
23448
|
return null;
|
|
23413
23449
|
}
|
|
23414
|
-
matches
|
|
23415
|
-
const aStat = fs10.statSync(a);
|
|
23416
|
-
const bStat = fs10.statSync(b);
|
|
23417
|
-
return bStat.mtime.getTime() - aStat.mtime.getTime();
|
|
23418
|
-
});
|
|
23419
|
-
return matches[0];
|
|
23450
|
+
return sortByMtimeNewestFirst(matches)[0];
|
|
23420
23451
|
}
|
|
23421
23452
|
async function executeImports(fileResults, directory, pendingDir, doneDir, hledgerExecutor) {
|
|
23422
23453
|
const importedFiles = [];
|
|
@@ -23592,12 +23623,7 @@ async function importStatements(directory, agent, options, configLoader = loadIm
|
|
|
23592
23623
|
totalUnknown += fileResult.unknownPostings.length;
|
|
23593
23624
|
}
|
|
23594
23625
|
for (const [_rulesFile, matchingCSVs] of rulesFileToCSVs.entries()) {
|
|
23595
|
-
|
|
23596
|
-
const aStat = fs10.statSync(a);
|
|
23597
|
-
const bStat = fs10.statSync(b);
|
|
23598
|
-
return bStat.mtime.getTime() - aStat.mtime.getTime();
|
|
23599
|
-
});
|
|
23600
|
-
const newestCSV = matchingCSVs[0];
|
|
23626
|
+
const newestCSV = sortByMtimeNewestFirst(matchingCSVs)[0];
|
|
23601
23627
|
const fileResult = await processCsvFile(newestCSV, rulesMapping, directory, hledgerExecutor);
|
|
23602
23628
|
fileResults.push(fileResult);
|
|
23603
23629
|
if (fileResult.error) {
|
|
@@ -23898,7 +23924,7 @@ function tryExtractClosingBalanceFromCSV(csvFile, rulesDir) {
|
|
|
23898
23924
|
}
|
|
23899
23925
|
}
|
|
23900
23926
|
}
|
|
23901
|
-
const balanceStr =
|
|
23927
|
+
const balanceStr = formatBalance(numericValue, currency || undefined);
|
|
23902
23928
|
return {
|
|
23903
23929
|
balance: balanceStr,
|
|
23904
23930
|
confidence: "high",
|
|
@@ -24435,7 +24461,8 @@ async function executeAccountDeclarationsStep(context, contextId, logger) {
|
|
|
24435
24461
|
break;
|
|
24436
24462
|
}
|
|
24437
24463
|
}
|
|
24438
|
-
} catch {
|
|
24464
|
+
} catch (error45) {
|
|
24465
|
+
logger?.debug(`Failed to extract year from rules file ${rulesFile}: ${error45 instanceof Error ? error45.message : String(error45)}`);
|
|
24439
24466
|
continue;
|
|
24440
24467
|
}
|
|
24441
24468
|
}
|
|
@@ -24473,6 +24500,37 @@ async function executeAccountDeclarationsStep(context, contextId, logger) {
|
|
|
24473
24500
|
});
|
|
24474
24501
|
logger?.endSection();
|
|
24475
24502
|
}
|
|
24503
|
+
async function buildSuggestionContext(context, contextId, logger) {
|
|
24504
|
+
const { loadExistingAccounts: loadExistingAccounts2, extractRulePatternsFromFile: extractRulePatternsFromFile2 } = await Promise.resolve().then(() => (init_accountSuggester(), exports_accountSuggester));
|
|
24505
|
+
const config2 = context.configLoader(context.directory);
|
|
24506
|
+
const rulesDir = path12.join(context.directory, config2.paths.rules);
|
|
24507
|
+
const importCtx = loadContext(context.directory, contextId);
|
|
24508
|
+
const csvPath = path12.join(context.directory, importCtx.filePath);
|
|
24509
|
+
const rulesMapping = loadRulesMapping(rulesDir);
|
|
24510
|
+
const rulesFile = findRulesForCsv(csvPath, rulesMapping);
|
|
24511
|
+
if (!rulesFile) {
|
|
24512
|
+
return { existingAccounts: [], logger };
|
|
24513
|
+
}
|
|
24514
|
+
let yearJournalPath;
|
|
24515
|
+
try {
|
|
24516
|
+
const result = await context.hledgerExecutor(["print", "-f", rulesFile]);
|
|
24517
|
+
if (result.exitCode === 0) {
|
|
24518
|
+
const years = extractTransactionYears(result.stdout);
|
|
24519
|
+
if (years.size > 0) {
|
|
24520
|
+
yearJournalPath = ensureYearJournalExists(context.directory, Array.from(years)[0]);
|
|
24521
|
+
}
|
|
24522
|
+
}
|
|
24523
|
+
} catch (error45) {
|
|
24524
|
+
logger?.debug(`Could not determine year journal: ${error45 instanceof Error ? error45.message : String(error45)}`);
|
|
24525
|
+
}
|
|
24526
|
+
return {
|
|
24527
|
+
existingAccounts: yearJournalPath ? loadExistingAccounts2(yearJournalPath) : [],
|
|
24528
|
+
rulesFilePath: rulesFile,
|
|
24529
|
+
existingRules: extractRulePatternsFromFile2(rulesFile),
|
|
24530
|
+
yearJournalPath,
|
|
24531
|
+
logger
|
|
24532
|
+
};
|
|
24533
|
+
}
|
|
24476
24534
|
async function executeDryRunStep(context, contextId, logger) {
|
|
24477
24535
|
logger?.startSection("Step 3: Dry Run Import");
|
|
24478
24536
|
logger?.logStep("Dry Run", "start");
|
|
@@ -24496,39 +24554,8 @@ async function executeDryRunStep(context, contextId, logger) {
|
|
|
24496
24554
|
}
|
|
24497
24555
|
if (allUnknownPostings.length > 0) {
|
|
24498
24556
|
try {
|
|
24499
|
-
const {
|
|
24500
|
-
|
|
24501
|
-
loadExistingAccounts: loadExistingAccounts2,
|
|
24502
|
-
extractRulePatternsFromFile: extractRulePatternsFromFile2
|
|
24503
|
-
} = await Promise.resolve().then(() => (init_accountSuggester(), exports_accountSuggester));
|
|
24504
|
-
const config2 = context.configLoader(context.directory);
|
|
24505
|
-
const rulesDir = path12.join(context.directory, config2.paths.rules);
|
|
24506
|
-
const importCtx = loadContext(context.directory, contextId);
|
|
24507
|
-
const csvPath = path12.join(context.directory, importCtx.filePath);
|
|
24508
|
-
const rulesMapping = loadRulesMapping(rulesDir);
|
|
24509
|
-
let yearJournalPath;
|
|
24510
|
-
let firstRulesFile;
|
|
24511
|
-
const rulesFile = findRulesForCsv(csvPath, rulesMapping);
|
|
24512
|
-
if (rulesFile) {
|
|
24513
|
-
firstRulesFile = rulesFile;
|
|
24514
|
-
try {
|
|
24515
|
-
const result = await context.hledgerExecutor(["print", "-f", rulesFile]);
|
|
24516
|
-
if (result.exitCode === 0) {
|
|
24517
|
-
const years = extractTransactionYears(result.stdout);
|
|
24518
|
-
if (years.size > 0) {
|
|
24519
|
-
const transactionYear = Array.from(years)[0];
|
|
24520
|
-
yearJournalPath = ensureYearJournalExists(context.directory, transactionYear);
|
|
24521
|
-
}
|
|
24522
|
-
}
|
|
24523
|
-
} catch {}
|
|
24524
|
-
}
|
|
24525
|
-
const suggestionContext = {
|
|
24526
|
-
existingAccounts: yearJournalPath ? loadExistingAccounts2(yearJournalPath) : [],
|
|
24527
|
-
rulesFilePath: firstRulesFile,
|
|
24528
|
-
existingRules: firstRulesFile ? extractRulePatternsFromFile2(firstRulesFile) : undefined,
|
|
24529
|
-
yearJournalPath,
|
|
24530
|
-
logger
|
|
24531
|
-
};
|
|
24557
|
+
const { suggestAccountsForPostingsBatch: suggestAccountsForPostingsBatch2 } = await Promise.resolve().then(() => (init_accountSuggester(), exports_accountSuggester));
|
|
24558
|
+
const suggestionContext = await buildSuggestionContext(context, contextId, logger);
|
|
24532
24559
|
postingsWithSuggestions = await suggestAccountsForPostingsBatch2(allUnknownPostings, suggestionContext);
|
|
24533
24560
|
} catch (error45) {
|
|
24534
24561
|
logger?.error(`[ERROR] Failed to generate account suggestions: ${error45 instanceof Error ? error45.message : String(error45)}`);
|