feat: done vibing
This commit is contained in:
27 files changed
+1922
-13
No files matched your search
@@ -0,0 +1,132 @@
|
||||
// Package config loads and represents stonks' YAML configuration.
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// Targets lists the assets to fetch data for, grouped by asset class.
|
||||
type Targets struct {
|
||||
Crypto []string `yaml:"crypto"`
|
||||
Equities []string `yaml:"equities"`
|
||||
Futures map[string]string `yaml:"futures"`
|
||||
// Forex holds currency pairs as bare base+quote codes (e.g. "USDTWD"),
|
||||
// without any provider-specific suffix — fetchers append whatever
|
||||
// suffix their API needs (Yahoo Finance uses "=X").
|
||||
Forex []string `yaml:"forex"`
|
||||
}
|
||||
|
||||
// PluginConfig is the config for one named, registered fetcher or output.
|
||||
// Params is decoded lazily via DecodeParams so each plugin owns its own
|
||||
// params shape without the top-level config needing to know it.
|
||||
type PluginConfig struct {
|
||||
Name string `yaml:"name"`
|
||||
Params map[string]any `yaml:"params"`
|
||||
}
|
||||
|
||||
// DecodeParams re-marshals Params into out, which should be a pointer to a
|
||||
// plugin-specific params struct with `yaml` tags.
|
||||
func (p PluginConfig) DecodeParams(out any) error {
|
||||
if p.Params == nil {
|
||||
return nil
|
||||
}
|
||||
raw, err := yaml.Marshal(p.Params)
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshal params for %q: %w", p.Name, err)
|
||||
}
|
||||
if err := yaml.Unmarshal(raw, out); err != nil {
|
||||
return fmt.Errorf("decode params for %q: %w", p.Name, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AnalyzerConfig configures which Claude invocation mode to use.
|
||||
type AnalyzerConfig struct {
|
||||
// Mode is "subscription" (shells out to the `claude` CLI, billed
|
||||
// against a Claude Pro/Max subscription) or "api" (uses ANTHROPIC_API_KEY
|
||||
// via the Messages API, billed per token).
|
||||
Mode string `yaml:"mode"`
|
||||
// Model is only used in "api" mode.
|
||||
Model string `yaml:"model"`
|
||||
// Effort is only used in "api" mode: low|medium|high|xhigh|max.
|
||||
Effort string `yaml:"effort"`
|
||||
// Advisors are additional CLI-based AI analysts (e.g. gemini, codex)
|
||||
// consulted before the final Claude call — multi-agent mode. Their
|
||||
// output is appended to Claude's prompt as extra context; Claude still
|
||||
// produces the one final report and recommendations. Empty/omitted
|
||||
// disables multi-agent mode entirely.
|
||||
Advisors []AdvisorConfig `yaml:"advisors"`
|
||||
}
|
||||
|
||||
// AdvisorConfig defines one additional CLI-based AI analyst consulted in
|
||||
// multi-agent mode. The same fetched-data prompt is piped to its stdin;
|
||||
// Command must be a non-interactive/scriptable invocation (e.g. `gemini`
|
||||
// reads a piped prompt from stdin one-shot; `codex exec -` explicitly reads
|
||||
// from stdin).
|
||||
type AdvisorConfig struct {
|
||||
// Name labels this advisor's section in Claude's final prompt (e.g. "Gemini").
|
||||
Name string `yaml:"name"`
|
||||
// Command is the CLI binary to invoke (looked up on PATH).
|
||||
Command string `yaml:"command"`
|
||||
// Args are extra arguments passed before the prompt is piped via stdin.
|
||||
Args []string `yaml:"args"`
|
||||
}
|
||||
|
||||
// ArchiveConfig controls saving a local copy of each run's fetched data and
|
||||
// analysis — useful for debugging prompt/parsing issues without needing to
|
||||
// reproduce a live run.
|
||||
type ArchiveConfig struct {
|
||||
Enabled bool `yaml:"enabled"`
|
||||
// Dir is the directory each run's files are written under, one
|
||||
// timestamped subdirectory per run. Supports a leading "~". Defaults to
|
||||
// "~/.local/state/stonks/runs" if enabled with Dir left empty.
|
||||
Dir string `yaml:"dir"`
|
||||
}
|
||||
|
||||
// Config is the full stonks configuration.
|
||||
type Config struct {
|
||||
Targets Targets `yaml:"targets"`
|
||||
Fetchers []PluginConfig `yaml:"fetchers"`
|
||||
Analyzer AnalyzerConfig `yaml:"analyzer"`
|
||||
Outputs []PluginConfig `yaml:"outputs"`
|
||||
Archive ArchiveConfig `yaml:"archive"`
|
||||
}
|
||||
|
||||
// Load reads and parses the config file at path.
|
||||
func Load(path string) (*Config, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read config %q: %w", path, err)
|
||||
}
|
||||
|
||||
var cfg Config
|
||||
if err := yaml.Unmarshal(data, &cfg); err != nil {
|
||||
return nil, fmt.Errorf("parse config %q: %w", path, err)
|
||||
}
|
||||
|
||||
if err := cfg.validate(); err != nil {
|
||||
return nil, fmt.Errorf("invalid config %q: %w", path, err)
|
||||
}
|
||||
|
||||
return &cfg, nil
|
||||
}
|
||||
|
||||
func (c *Config) validate() error {
|
||||
switch c.Analyzer.Mode {
|
||||
case "subscription", "api":
|
||||
case "":
|
||||
return fmt.Errorf("analyzer.mode is required (\"subscription\" or \"api\")")
|
||||
default:
|
||||
return fmt.Errorf("analyzer.mode %q must be \"subscription\" or \"api\"", c.Analyzer.Mode)
|
||||
}
|
||||
if len(c.Fetchers) == 0 {
|
||||
return fmt.Errorf("at least one fetcher must be configured")
|
||||
}
|
||||
if len(c.Outputs) == 0 {
|
||||
return fmt.Errorf("at least one output must be configured")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in new issue
Block a user