diff --git a/Makefile b/Makefile index c329f20..7679d94 100644 --- a/Makefile +++ b/Makefile @@ -161,6 +161,12 @@ deps: run: build @$(BUILD_DIR)/$(BINARY_NAME) $(ARGS) +## test: Build and compile-check in Docker (Dockerfile.test) +test: + @echo "Running Docker compile test..." + docker build -f Dockerfile.test -t clawgo:test . + @echo "Docker compile test passed" + ## help: Show this help message help: @echo "clawgo Makefile" diff --git a/pkg/agent/loop.go b/pkg/agent/loop.go index 4cd2c6c..40c997d 100644 --- a/pkg/agent/loop.go +++ b/pkg/agent/loop.go @@ -80,6 +80,7 @@ func NewAgentLoop(cfg *config.Config, msgBus *bus.MessageBus, provider providers // Register memory tools memorySearchTool := tools.NewMemorySearchTool(workspace) toolsRegistry.Register(memorySearchTool) + toolsRegistry.Register(tools.NewMemoryGetTool(workspace)) toolsRegistry.Register(tools.NewMemoryWriteTool(workspace)) // Register parallel execution tool (leveraging Go's concurrency) diff --git a/pkg/tools/memory_get.go b/pkg/tools/memory_get.go new file mode 100644 index 0000000..0f7d455 --- /dev/null +++ b/pkg/tools/memory_get.go @@ -0,0 +1,123 @@ +package tools + +import ( + "bufio" + "context" + "fmt" + "os" + "path/filepath" + "strings" +) + +type MemoryGetTool struct { + workspace string +} + +func NewMemoryGetTool(workspace string) *MemoryGetTool { + return &MemoryGetTool{workspace: workspace} +} + +func (t *MemoryGetTool) Name() string { + return "memory_get" +} + +func (t *MemoryGetTool) Description() string { + return "Read safe snippets from MEMORY.md or memory/*.md using optional line range." +} + +func (t *MemoryGetTool) Parameters() map[string]interface{} { + return map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "path": map[string]interface{}{ + "type": "string", + "description": "Relative path to MEMORY.md or memory/*.md", + }, + "from": map[string]interface{}{ + "type": "integer", + "description": "Start line (1-indexed)", + "default": 1, + }, + "lines": map[string]interface{}{ + "type": "integer", + "description": "Number of lines to read", + "default": 80, + }, + }, + "required": []string{"path"}, + } +} + +func (t *MemoryGetTool) Execute(ctx context.Context, args map[string]interface{}) (string, error) { + rawPath, _ := args["path"].(string) + rawPath = strings.TrimSpace(rawPath) + if rawPath == "" { + return "", fmt.Errorf("path is required") + } + + from := 1 + if v, ok := args["from"].(float64); ok && int(v) > 0 { + from = int(v) + } + lines := 80 + if v, ok := args["lines"].(float64); ok && int(v) > 0 { + lines = int(v) + } + if lines > 500 { + lines = 500 + } + + fullPath := filepath.Clean(filepath.Join(t.workspace, rawPath)) + if !t.isAllowedMemoryPath(fullPath) { + return "", fmt.Errorf("path not allowed: %s", rawPath) + } + + f, err := os.Open(fullPath) + if err != nil { + return "", err + } + defer f.Close() + + scanner := bufio.NewScanner(f) + lineNo := 0 + end := from + lines - 1 + var out strings.Builder + for scanner.Scan() { + lineNo++ + if lineNo < from { + continue + } + if lineNo > end { + break + } + out.WriteString(fmt.Sprintf("%d: %s\n", lineNo, scanner.Text())) + } + if err := scanner.Err(); err != nil { + return "", err + } + + content := strings.TrimSpace(out.String()) + if content == "" { + return fmt.Sprintf("No content in range for %s (from=%d, lines=%d)", rawPath, from, lines), nil + } + + rel, _ := filepath.Rel(t.workspace, fullPath) + return fmt.Sprintf("Source: %s#L%d-L%d\n%s", rel, from, end, content), nil +} + +func (t *MemoryGetTool) isAllowedMemoryPath(fullPath string) bool { + workspaceMemory := filepath.Join(t.workspace, "MEMORY.md") + if fullPath == workspaceMemory { + return true + } + + memoryDir := filepath.Join(t.workspace, "memory") + rel, err := filepath.Rel(memoryDir, fullPath) + if err != nil { + return false + } + if strings.HasPrefix(rel, "..") { + return false + } + return strings.HasSuffix(strings.ToLower(fullPath), ".md") +}