@fuzzle/opencode-accountant 0.0.4 → 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +75 -0
- package/dist/index.js +143 -72
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,6 +12,81 @@ An OpenCode accounting agent, specialized in double-entry-bookkepping with hledg
|
|
|
12
12
|
|
|
13
13
|
<!-- Installation instructions go here -->
|
|
14
14
|
|
|
15
|
+
## Configuration
|
|
16
|
+
|
|
17
|
+
### Price Fetching Configuration
|
|
18
|
+
|
|
19
|
+
The `update-prices` tool requires a configuration file to specify which currency pairs to fetch. Create a `config/prices.yaml` file in your project directory with the following structure:
|
|
20
|
+
|
|
21
|
+
```yaml
|
|
22
|
+
currencies:
|
|
23
|
+
BTC:
|
|
24
|
+
source: coinmarketcap
|
|
25
|
+
pair: BTC/CHF
|
|
26
|
+
file: btc-chf.journal
|
|
27
|
+
backfill_date: '2025-12-31'
|
|
28
|
+
|
|
29
|
+
EUR:
|
|
30
|
+
source: ecb
|
|
31
|
+
pair: EUR/CHF
|
|
32
|
+
file: eur-chf.journal
|
|
33
|
+
backfill_date: '2025-12-31'
|
|
34
|
+
|
|
35
|
+
USD:
|
|
36
|
+
source: yahoo
|
|
37
|
+
pair: USDCHF=X
|
|
38
|
+
file: usd-chf.journal
|
|
39
|
+
fmt_base: USD
|
|
40
|
+
backfill_date: '2025-06-01'
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
#### Configuration Options
|
|
44
|
+
|
|
45
|
+
Each currency entry supports the following fields:
|
|
46
|
+
|
|
47
|
+
| Field | Required | Description |
|
|
48
|
+
| --------------- | -------- | -------------------------------------------------------------------------- |
|
|
49
|
+
| `source` | Yes | The pricehist data source (e.g., `coinmarketcap`, `ecb`, `yahoo`) |
|
|
50
|
+
| `pair` | Yes | The currency pair to fetch (e.g., `BTC/CHF`, `EUR/CHF`) |
|
|
51
|
+
| `file` | Yes | Output filename in `ledger/currencies/` (e.g., `btc-chf.journal`) |
|
|
52
|
+
| `fmt_base` | No | Base currency format override for pricehist (e.g., `USD` for yahoo source) |
|
|
53
|
+
| `backfill_date` | No | Start date for backfill mode. Defaults to January 1st of the current year |
|
|
54
|
+
|
|
55
|
+
#### Directory Structure
|
|
56
|
+
|
|
57
|
+
The plugin expects the following directory structure in your project:
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
your-project/
|
|
61
|
+
├── config/
|
|
62
|
+
│ └── prices.yaml # Price fetching configuration
|
|
63
|
+
└── ledger/
|
|
64
|
+
└── currencies/ # Where price journal files are written
|
|
65
|
+
├── btc-chf.journal
|
|
66
|
+
├── eur-chf.journal
|
|
67
|
+
└── usd-chf.journal
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
#### Base Currency
|
|
71
|
+
|
|
72
|
+
The `pair` field determines the currency conversion. While the examples above use CHF as the base currency, you can use any currency supported by your price source:
|
|
73
|
+
|
|
74
|
+
```yaml
|
|
75
|
+
currencies:
|
|
76
|
+
BTC:
|
|
77
|
+
source: coinmarketcap
|
|
78
|
+
pair: BTC/EUR # Prices in EUR instead of CHF
|
|
79
|
+
file: btc-eur.journal
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
#### Available Data Sources
|
|
83
|
+
|
|
84
|
+
The `source` field accepts any source supported by [pricehist](https://github.com/chrisberkhout/pricehist). Common sources include:
|
|
85
|
+
|
|
86
|
+
- `coinmarketcap` - Cryptocurrency prices
|
|
87
|
+
- `ecb` - European Central Bank exchange rates
|
|
88
|
+
- `yahoo` - Yahoo Finance (use `fmt_base` for proper formatting)
|
|
89
|
+
|
|
15
90
|
## Development
|
|
16
91
|
|
|
17
92
|
- `mise run build` - Build the module
|
package/dist/index.js
CHANGED
|
@@ -11,7 +11,7 @@ var __export = (target, all) => {
|
|
|
11
11
|
};
|
|
12
12
|
|
|
13
13
|
// src/index.ts
|
|
14
|
-
import { dirname, join as
|
|
14
|
+
import { dirname, join as join3 } from "path";
|
|
15
15
|
import { fileURLToPath } from "url";
|
|
16
16
|
|
|
17
17
|
// src/utils/agentLoader.ts
|
|
@@ -15048,13 +15048,74 @@ function tool(input) {
|
|
|
15048
15048
|
tool.schema = exports_external;
|
|
15049
15049
|
// src/tools/update-prices.ts
|
|
15050
15050
|
var {$ } = globalThis.Bun;
|
|
15051
|
-
import * as
|
|
15051
|
+
import * as path2 from "path";
|
|
15052
|
+
import * as fs2 from "fs";
|
|
15053
|
+
|
|
15054
|
+
// src/utils/pricesConfig.ts
|
|
15052
15055
|
import * as fs from "fs";
|
|
15053
|
-
|
|
15054
|
-
|
|
15055
|
-
|
|
15056
|
-
|
|
15057
|
-
|
|
15056
|
+
import * as path from "path";
|
|
15057
|
+
var CONFIG_FILE = "config/prices.yaml";
|
|
15058
|
+
var REQUIRED_CURRENCY_FIELDS = ["source", "pair", "file"];
|
|
15059
|
+
function getDefaultBackfillDate() {
|
|
15060
|
+
const year = new Date().getFullYear();
|
|
15061
|
+
return `${year}-01-01`;
|
|
15062
|
+
}
|
|
15063
|
+
function validateCurrencyConfig(name, config2) {
|
|
15064
|
+
if (typeof config2 !== "object" || config2 === null) {
|
|
15065
|
+
throw new Error(`Invalid config for currency '${name}': expected an object`);
|
|
15066
|
+
}
|
|
15067
|
+
const configObj = config2;
|
|
15068
|
+
for (const field of REQUIRED_CURRENCY_FIELDS) {
|
|
15069
|
+
if (typeof configObj[field] !== "string" || configObj[field] === "") {
|
|
15070
|
+
throw new Error(`Invalid config for currency '${name}': missing required field '${field}'`);
|
|
15071
|
+
}
|
|
15072
|
+
}
|
|
15073
|
+
return {
|
|
15074
|
+
source: configObj.source,
|
|
15075
|
+
pair: configObj.pair,
|
|
15076
|
+
file: configObj.file,
|
|
15077
|
+
fmt_base: typeof configObj.fmt_base === "string" ? configObj.fmt_base : undefined,
|
|
15078
|
+
backfill_date: typeof configObj.backfill_date === "string" ? configObj.backfill_date : undefined
|
|
15079
|
+
};
|
|
15080
|
+
}
|
|
15081
|
+
function loadPricesConfig(directory) {
|
|
15082
|
+
const configPath = path.join(directory, CONFIG_FILE);
|
|
15083
|
+
if (!fs.existsSync(configPath)) {
|
|
15084
|
+
throw new Error(`Configuration file not found: ${CONFIG_FILE}. Please refer to the plugin's GitHub repository for setup instructions.`);
|
|
15085
|
+
}
|
|
15086
|
+
let parsed;
|
|
15087
|
+
try {
|
|
15088
|
+
const content = fs.readFileSync(configPath, "utf-8");
|
|
15089
|
+
parsed = jsYaml.load(content);
|
|
15090
|
+
} catch (err) {
|
|
15091
|
+
if (err instanceof jsYaml.YAMLException) {
|
|
15092
|
+
throw new Error(`Failed to parse ${CONFIG_FILE}: ${err.message}`);
|
|
15093
|
+
}
|
|
15094
|
+
throw err;
|
|
15095
|
+
}
|
|
15096
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
15097
|
+
throw new Error(`Invalid config: ${CONFIG_FILE} must contain a YAML object`);
|
|
15098
|
+
}
|
|
15099
|
+
const parsedObj = parsed;
|
|
15100
|
+
if (!parsedObj.currencies || typeof parsedObj.currencies !== "object") {
|
|
15101
|
+
throw new Error(`Invalid config: 'currencies' section is required`);
|
|
15102
|
+
}
|
|
15103
|
+
const currenciesObj = parsedObj.currencies;
|
|
15104
|
+
if (Object.keys(currenciesObj).length === 0) {
|
|
15105
|
+
throw new Error(`Invalid config: 'currencies' section must contain at least one currency`);
|
|
15106
|
+
}
|
|
15107
|
+
const currencies = {};
|
|
15108
|
+
for (const [name, config2] of Object.entries(currenciesObj)) {
|
|
15109
|
+
currencies[name] = validateCurrencyConfig(name, config2);
|
|
15110
|
+
}
|
|
15111
|
+
return { currencies };
|
|
15112
|
+
}
|
|
15113
|
+
|
|
15114
|
+
// src/tools/update-prices.ts
|
|
15115
|
+
async function defaultPriceFetcher(cmdArgs) {
|
|
15116
|
+
const result = await $`pricehist ${cmdArgs}`.quiet();
|
|
15117
|
+
return result.stdout.toString().trim();
|
|
15118
|
+
}
|
|
15058
15119
|
function getYesterday() {
|
|
15059
15120
|
const d = new Date;
|
|
15060
15121
|
d.setDate(d.getDate() - 1);
|
|
@@ -15062,8 +15123,8 @@ function getYesterday() {
|
|
|
15062
15123
|
}
|
|
15063
15124
|
function updateJournalWithPrices(journalPath, newPriceLines) {
|
|
15064
15125
|
let existingLines = [];
|
|
15065
|
-
if (
|
|
15066
|
-
existingLines =
|
|
15126
|
+
if (fs2.existsSync(journalPath)) {
|
|
15127
|
+
existingLines = fs2.readFileSync(journalPath, "utf-8").split(`
|
|
15067
15128
|
`).filter((line) => line.trim() !== "");
|
|
15068
15129
|
}
|
|
15069
15130
|
const priceMap = new Map;
|
|
@@ -15078,82 +15139,92 @@ function updateJournalWithPrices(journalPath, newPriceLines) {
|
|
|
15078
15139
|
priceMap.set(date5, line);
|
|
15079
15140
|
}
|
|
15080
15141
|
const sortedLines = Array.from(priceMap.entries()).sort((a, b) => a[0].localeCompare(b[0])).map(([, line]) => line);
|
|
15081
|
-
|
|
15142
|
+
fs2.writeFileSync(journalPath, sortedLines.join(`
|
|
15082
15143
|
`) + `
|
|
15083
15144
|
`);
|
|
15084
15145
|
}
|
|
15085
|
-
|
|
15086
|
-
|
|
15087
|
-
|
|
15088
|
-
|
|
15089
|
-
|
|
15090
|
-
|
|
15091
|
-
|
|
15092
|
-
|
|
15093
|
-
|
|
15094
|
-
|
|
15095
|
-
|
|
15096
|
-
|
|
15097
|
-
|
|
15098
|
-
|
|
15099
|
-
}
|
|
15100
|
-
|
|
15101
|
-
|
|
15102
|
-
|
|
15103
|
-
|
|
15104
|
-
|
|
15105
|
-
|
|
15106
|
-
|
|
15107
|
-
|
|
15108
|
-
|
|
15109
|
-
|
|
15110
|
-
|
|
15111
|
-
|
|
15112
|
-
|
|
15113
|
-
|
|
15114
|
-
|
|
15115
|
-
|
|
15116
|
-
|
|
15117
|
-
|
|
15118
|
-
|
|
15119
|
-
|
|
15120
|
-
|
|
15121
|
-
|
|
15146
|
+
async function updatePricesCore(directory, agent, backfill, priceFetcher = defaultPriceFetcher, configLoader = loadPricesConfig) {
|
|
15147
|
+
if (agent !== "accountant") {
|
|
15148
|
+
return JSON.stringify({
|
|
15149
|
+
error: "This tool is restricted to the accountant agent only.",
|
|
15150
|
+
hint: "Use: Task(subagent_type='accountant', prompt='update prices')",
|
|
15151
|
+
caller: agent || "main assistant"
|
|
15152
|
+
});
|
|
15153
|
+
}
|
|
15154
|
+
let config2;
|
|
15155
|
+
try {
|
|
15156
|
+
config2 = configLoader(directory);
|
|
15157
|
+
} catch (err) {
|
|
15158
|
+
return JSON.stringify({
|
|
15159
|
+
error: err instanceof Error ? err.message : String(err)
|
|
15160
|
+
});
|
|
15161
|
+
}
|
|
15162
|
+
const endDate = getYesterday();
|
|
15163
|
+
const defaultBackfillDate = getDefaultBackfillDate();
|
|
15164
|
+
const results = [];
|
|
15165
|
+
for (const [ticker, currencyConfig] of Object.entries(config2.currencies)) {
|
|
15166
|
+
try {
|
|
15167
|
+
const startDate = backfill ? currencyConfig.backfill_date || defaultBackfillDate : endDate;
|
|
15168
|
+
const cmdArgs = [
|
|
15169
|
+
"fetch",
|
|
15170
|
+
"-o",
|
|
15171
|
+
"ledger",
|
|
15172
|
+
"-s",
|
|
15173
|
+
startDate,
|
|
15174
|
+
"-e",
|
|
15175
|
+
endDate,
|
|
15176
|
+
currencyConfig.source,
|
|
15177
|
+
currencyConfig.pair
|
|
15178
|
+
];
|
|
15179
|
+
if (currencyConfig.fmt_base) {
|
|
15180
|
+
cmdArgs.push("--fmt-base", currencyConfig.fmt_base);
|
|
15181
|
+
}
|
|
15182
|
+
const output = await priceFetcher(cmdArgs);
|
|
15183
|
+
const priceLines = output.split(`
|
|
15122
15184
|
`).filter((line) => line.startsWith("P "));
|
|
15123
|
-
|
|
15124
|
-
results.push({
|
|
15125
|
-
ticker,
|
|
15126
|
-
error: `No price lines in pricehist output: ${output}`
|
|
15127
|
-
});
|
|
15128
|
-
continue;
|
|
15129
|
-
}
|
|
15130
|
-
const journalPath = path.join(directory, "ledger", config2.file);
|
|
15131
|
-
updateJournalWithPrices(journalPath, priceLines);
|
|
15132
|
-
const latestPriceLine = priceLines[priceLines.length - 1];
|
|
15133
|
-
results.push({
|
|
15134
|
-
ticker,
|
|
15135
|
-
priceLine: latestPriceLine,
|
|
15136
|
-
file: config2.file
|
|
15137
|
-
});
|
|
15138
|
-
} catch (err) {
|
|
15185
|
+
if (priceLines.length === 0) {
|
|
15139
15186
|
results.push({
|
|
15140
15187
|
ticker,
|
|
15141
|
-
error:
|
|
15188
|
+
error: `No price lines in pricehist output: ${output}`
|
|
15142
15189
|
});
|
|
15190
|
+
continue;
|
|
15143
15191
|
}
|
|
15192
|
+
const journalPath = path2.join(directory, "ledger", "currencies", currencyConfig.file);
|
|
15193
|
+
updateJournalWithPrices(journalPath, priceLines);
|
|
15194
|
+
const latestPriceLine = priceLines[priceLines.length - 1];
|
|
15195
|
+
results.push({
|
|
15196
|
+
ticker,
|
|
15197
|
+
priceLine: latestPriceLine,
|
|
15198
|
+
file: currencyConfig.file
|
|
15199
|
+
});
|
|
15200
|
+
} catch (err) {
|
|
15201
|
+
results.push({
|
|
15202
|
+
ticker,
|
|
15203
|
+
error: String(err)
|
|
15204
|
+
});
|
|
15144
15205
|
}
|
|
15145
|
-
|
|
15146
|
-
|
|
15147
|
-
|
|
15148
|
-
|
|
15149
|
-
|
|
15150
|
-
|
|
15151
|
-
|
|
15206
|
+
}
|
|
15207
|
+
return JSON.stringify({
|
|
15208
|
+
success: results.every((r) => !("error" in r)),
|
|
15209
|
+
endDate,
|
|
15210
|
+
backfill: !!backfill,
|
|
15211
|
+
results
|
|
15212
|
+
});
|
|
15213
|
+
}
|
|
15214
|
+
var update_prices_default = tool({
|
|
15215
|
+
description: "ACCOUNTANT AGENT ONLY: Fetches end-of-day prices for all configured currencies (from config/prices.yaml) and appends them to the corresponding price journals in ledger/currencies/.",
|
|
15216
|
+
args: {
|
|
15217
|
+
backfill: tool.schema.boolean().optional().describe("If true, fetch history from each currency's configured backfill_date (or Jan 1 of current year if not specified)")
|
|
15218
|
+
},
|
|
15219
|
+
async execute(params, context) {
|
|
15220
|
+
const { directory, agent } = context;
|
|
15221
|
+
const { backfill } = params;
|
|
15222
|
+
return updatePricesCore(directory, agent, backfill || false);
|
|
15152
15223
|
}
|
|
15153
15224
|
});
|
|
15154
15225
|
// src/index.ts
|
|
15155
15226
|
var __dirname2 = dirname(fileURLToPath(import.meta.url));
|
|
15156
|
-
var AGENT_FILE =
|
|
15227
|
+
var AGENT_FILE = join3(__dirname2, "..", "agent", "accountant.md");
|
|
15157
15228
|
var AccountantPlugin = async () => {
|
|
15158
15229
|
const agent = loadAgent(AGENT_FILE);
|
|
15159
15230
|
return {
|