diff --git a/pkg/agentic/config.go b/pkg/agentic/config.go index ef3d9395..a0b2f992 100644 --- a/pkg/agentic/config.go +++ b/pkg/agentic/config.go @@ -1,26 +1,25 @@ package agentic import ( - "bufio" "os" "path/filepath" "strings" - "github.com/host-uk/core/pkg/config" + "github.com/host-uk/core/pkg/errors" "github.com/host-uk/core/pkg/io" - "github.com/host-uk/core/pkg/log" + "gopkg.in/yaml.v3" ) // Config holds the configuration for connecting to the core-agentic service. type Config struct { // BaseURL is the URL of the core-agentic API server. - BaseURL string `yaml:"base_url" json:"base_url" mapstructure:"base_url"` + BaseURL string `yaml:"base_url" json:"base_url"` // Token is the authentication token for API requests. - Token string `yaml:"token" json:"token" mapstructure:"token"` + Token string `yaml:"token" json:"token"` // DefaultProject is the project to use when none is specified. - DefaultProject string `yaml:"default_project" json:"default_project" mapstructure:"default_project"` + DefaultProject string `yaml:"default_project" json:"default_project"` // AgentID is the identifier for this agent (optional, used for claiming tasks). - AgentID string `yaml:"agent_id" json:"agent_id" mapstructure:"agent_id"` + AgentID string `yaml:"agent_id" json:"agent_id"` } // configFileName is the name of the YAML config file. @@ -33,9 +32,10 @@ const envFileName = ".env" const DefaultBaseURL = "https://api.core-agentic.dev" // LoadConfig loads the agentic configuration from the specified directory. -// It uses the centralized config service. +// It first checks for a .env file, then falls back to ~/.core/agentic.yaml. +// If dir is empty, it checks the current directory first. // -// Environment variables take precedence (prefix: AGENTIC_): +// Environment variables take precedence: // - AGENTIC_BASE_URL: API base URL // - AGENTIC_TOKEN: Authentication token // - AGENTIC_PROJECT: Default project @@ -58,13 +58,15 @@ func LoadConfig(dir string) (*Config, error) { } // Try loading from current directory .env - cwd, err := os.Getwd() - if err == nil { - envPath := filepath.Join(cwd, envFileName) - if err := loadEnvFile(envPath, cfg); err == nil { - applyEnvOverrides(cfg) - if cfg.Token != "" { - return cfg, nil + if dir == "" { + cwd, err := os.Getwd() + if err == nil { + envPath := filepath.Join(cwd, envFileName) + if err := loadEnvFile(envPath, cfg); err == nil { + applyEnvOverrides(cfg) + if cfg.Token != "" { + return cfg, nil + } } } } @@ -72,19 +74,12 @@ func LoadConfig(dir string) (*Config, error) { // Try loading from ~/.core/agentic.yaml homeDir, err := os.UserHomeDir() if err != nil { - return nil, log.E("agentic.LoadConfig", "failed to get home directory", err) + return nil, errors.E("agentic.LoadConfig", "failed to get home directory", err) } configPath := filepath.Join(homeDir, ".core", configFileName) - if io.Local.IsFile(configPath) { - // Use centralized config service to load the YAML file - c, err := config.New(config.WithPath(configPath)) - if err != nil { - return nil, log.E("agentic.LoadConfig", "failed to initialize config", err) - } - if err := c.Get("", cfg); err != nil { - return nil, log.E("agentic.LoadConfig", "failed to load config", err) - } + if err := loadYAMLConfig(configPath, cfg); err != nil && !os.IsNotExist(err) { + return nil, errors.E("agentic.LoadConfig", "failed to load config", err) } // Apply environment variable overrides @@ -92,25 +87,21 @@ func LoadConfig(dir string) (*Config, error) { // Validate configuration if cfg.Token == "" { - log.Security("agentic authentication failed: no token configured", "user", log.Username()) - return nil, log.E("agentic.LoadConfig", "no authentication token configured", nil) + return nil, errors.E("agentic.LoadConfig", "no authentication token configured", nil) } - log.Security("agentic configuration loaded", "user", log.Username(), "baseURL", cfg.BaseURL) return cfg, nil } // loadEnvFile reads a .env file and extracts agentic configuration. func loadEnvFile(path string, cfg *Config) error { - file, err := os.Open(path) + content, err := io.Local.Read(path) if err != nil { return err } - defer func() { _ = file.Close() }() - scanner := bufio.NewScanner(file) - for scanner.Scan() { - line := strings.TrimSpace(scanner.Text()) + for _, line := range strings.Split(content, "\n") { + line = strings.TrimSpace(line) // Skip empty lines and comments if line == "" || strings.HasPrefix(line, "#") { @@ -141,7 +132,17 @@ func loadEnvFile(path string, cfg *Config) error { } } - return scanner.Err() + return nil +} + +// loadYAMLConfig reads configuration from a YAML file. +func loadYAMLConfig(path string, cfg *Config) error { + content, err := io.Local.Read(path) + if err != nil { + return err + } + + return yaml.Unmarshal([]byte(content), cfg) } // applyEnvOverrides applies environment variable overrides to the config. @@ -162,25 +163,35 @@ func applyEnvOverrides(cfg *Config) { // SaveConfig saves the configuration to ~/.core/agentic.yaml. func SaveConfig(cfg *Config) error { - path, err := ConfigPath() + homeDir, err := os.UserHomeDir() if err != nil { - return err + return errors.E("agentic.SaveConfig", "failed to get home directory", err) } - data := make(map[string]any) - data["base_url"] = cfg.BaseURL - data["token"] = cfg.Token - data["default_project"] = cfg.DefaultProject - data["agent_id"] = cfg.AgentID + configDir := filepath.Join(homeDir, ".core") + if err := io.Local.EnsureDir(configDir); err != nil { + return errors.E("agentic.SaveConfig", "failed to create config directory", err) + } - return config.Save(io.Local, path, data) + configPath := filepath.Join(configDir, configFileName) + + data, err := yaml.Marshal(cfg) + if err != nil { + return errors.E("agentic.SaveConfig", "failed to marshal config", err) + } + + if err := io.Local.Write(configPath, string(data)); err != nil { + return errors.E("agentic.SaveConfig", "failed to write config file", err) + } + + return nil } // ConfigPath returns the path to the config file in the user's home directory. func ConfigPath() (string, error) { homeDir, err := os.UserHomeDir() if err != nil { - return "", log.E("agentic.ConfigPath", "failed to get home directory", err) + return "", errors.E("agentic.ConfigPath", "failed to get home directory", err) } return filepath.Join(homeDir, ".core", configFileName), nil } diff --git a/pkg/agentic/context.go b/pkg/agentic/context.go index bbf35919..7db8e517 100644 --- a/pkg/agentic/context.go +++ b/pkg/agentic/context.go @@ -3,20 +3,16 @@ package agentic import ( "bytes" - goio "io" "os" "os/exec" "path/filepath" "regexp" "strings" - "github.com/host-uk/core/pkg/ai" + "github.com/host-uk/core/pkg/errors" "github.com/host-uk/core/pkg/io" - "github.com/host-uk/core/pkg/log" ) -const maxContextBytes = 5000 - // FileContent represents the content of a file for AI context. type FileContent struct { // Path is the relative path to the file. @@ -39,8 +35,6 @@ type TaskContext struct { RecentCommits string `json:"recent_commits"` // RelatedCode contains code snippets related to the task. RelatedCode []FileContent `json:"related_code"` - // RAGContext contains relevant documentation from the vector database. - RAGContext string `json:"rag_context,omitempty"` } // BuildTaskContext gathers context for AI collaboration on a task. @@ -48,13 +42,13 @@ func BuildTaskContext(task *Task, dir string) (*TaskContext, error) { const op = "agentic.BuildTaskContext" if task == nil { - return nil, log.E(op, "task is required", nil) + return nil, errors.E(op, "task is required", nil) } if dir == "" { cwd, err := os.Getwd() if err != nil { - return nil, log.E(op, "failed to get working directory", err) + return nil, errors.E(op, "failed to get working directory", err) } dir = cwd } @@ -86,13 +80,6 @@ func BuildTaskContext(task *Task, dir string) (*TaskContext, error) { } ctx.RelatedCode = relatedCode - // Query RAG for relevant documentation (graceful degradation) - ragCtx := ai.QueryRAGForTask(ai.TaskInfo{ - Title: task.Title, - Description: task.Description, - }) - ctx.RAGContext = ragCtx - return ctx, nil } @@ -101,31 +88,24 @@ func GatherRelatedFiles(task *Task, dir string) ([]FileContent, error) { const op = "agentic.GatherRelatedFiles" if task == nil { - return nil, log.E(op, "task is required", nil) + return nil, errors.E(op, "task is required", nil) } var files []FileContent // Read files explicitly mentioned in the task for _, relPath := range task.Files { - fullPath := relPath - if !filepath.IsAbs(relPath) { - fullPath = filepath.Join(dir, relPath) - } + fullPath := filepath.Join(dir, relPath) - content, truncated, err := readAndTruncate(fullPath) + content, err := io.Local.Read(fullPath) if err != nil { + // Skip files that don't exist continue } - contentStr := string(content) - if truncated { - contentStr += "\n... (truncated)" - } - files = append(files, FileContent{ Path: relPath, - Content: contentStr, + Content: content, Language: detectLanguage(relPath), }) } @@ -138,7 +118,7 @@ func findRelatedCode(task *Task, dir string) ([]FileContent, error) { const op = "agentic.findRelatedCode" if task == nil { - return nil, log.E(op, "task is required", nil) + return nil, errors.E(op, "task is required", nil) } // Extract keywords from title and description @@ -174,24 +154,20 @@ func findRelatedCode(task *Task, dir string) ([]FileContent, error) { break } - fullPath := line - if !filepath.IsAbs(line) { - fullPath = filepath.Join(dir, line) - } - - content, truncated, err := readAndTruncate(fullPath) + fullPath := filepath.Join(dir, line) + content, err := io.Local.Read(fullPath) if err != nil { continue } - contentStr := string(content) - if truncated { - contentStr += "\n... (truncated)" + // Truncate large files + if len(content) > 5000 { + content = content[:5000] + "\n... (truncated)" } files = append(files, FileContent{ Path: line, - Content: contentStr, + Content: content, Language: detectLanguage(line), }) } @@ -286,30 +262,6 @@ func detectLanguage(path string) string { return "text" } -// readAndTruncate reads up to maxContextBytes from a file. -func readAndTruncate(path string) ([]byte, bool, error) { - f, err := io.Local.ReadStream(path) - if err != nil { - return nil, false, err - } - defer func() { _ = f.Close() }() - - // Read up to maxContextBytes + 1 to detect truncation - reader := goio.LimitReader(f, maxContextBytes+1) - content, err := goio.ReadAll(reader) - if err != nil { - return nil, false, err - } - - truncated := false - if len(content) > maxContextBytes { - content = content[:maxContextBytes] - truncated = true - } - - return content, truncated, nil -} - // runGitCommand runs a git command and returns the output. func runGitCommand(dir string, args ...string) (string, error) { cmd := exec.Command("git", args...) @@ -379,12 +331,5 @@ func (tc *TaskContext) FormatContext() string { } } - // Relevant documentation from RAG - if tc.RAGContext != "" { - sb.WriteString("## Relevant Documentation\n") - sb.WriteString(tc.RAGContext) - sb.WriteString("\n\n") - } - return sb.String() }