feat: done vibing

This commit is contained in:
ytshih committed 2026-09-05 09:55:24 +08:00
1 parent 3de31a57c3
commit 4352fd1ad1
27 files changed
+1922 -13

No files matched your search

+132
View File
@@ -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
}