diff options
| author | Grail Finder <wohilas@gmail.com> | 2026-03-14 10:28:04 +0300 |
|---|---|---|
| committer | Grail Finder <wohilas@gmail.com> | 2026-03-14 10:28:04 +0300 |
| commit | 2901208c800742cb7b5980e7e203655bf7dee4b4 (patch) | |
| tree | 63e2eefb34b9e4561ec5db8b4b15989affcc334b /tools | |
| parent | 13773bcc977a761ec2cef0a1d43f210634841548 (diff) | |
Feat: minimize top commands agent-clip style
Diffstat (limited to 'tools')
| -rw-r--r-- | tools/chain.go | 271 | ||||
| -rw-r--r-- | tools/fs.go | 679 |
2 files changed, 950 insertions, 0 deletions
diff --git a/tools/chain.go b/tools/chain.go new file mode 100644 index 0000000..fb7767e --- /dev/null +++ b/tools/chain.go @@ -0,0 +1,271 @@ +package tools + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" +) + +// Operator represents a chain operator between commands. +type Operator int + +const ( + OpNone Operator = iota + OpAnd // && + OpOr // || + OpSeq // ; + OpPipe // | +) + +// Segment is a single command in a chain. +type Segment struct { + Raw string + Op Operator // operator AFTER this segment +} + +// ParseChain splits a command string into segments by &&, ;, and |. +// Respects quoted strings (single and double quotes). +func ParseChain(input string) []Segment { + var segments []Segment + var current strings.Builder + runes := []rune(input) + n := len(runes) + + for i := 0; i < n; i++ { + ch := runes[i] + + // handle quotes + if ch == '\'' || ch == '"' { + quote := ch + current.WriteRune(ch) + i++ + for i < n && runes[i] != quote { + current.WriteRune(runes[i]) + i++ + } + if i < n { + current.WriteRune(runes[i]) + } + continue + } + + // && + if ch == '&' && i+1 < n && runes[i+1] == '&' { + segments = append(segments, Segment{ + Raw: strings.TrimSpace(current.String()), + Op: OpAnd, + }) + current.Reset() + i++ // skip second & + continue + } + + // ; + if ch == ';' { + segments = append(segments, Segment{ + Raw: strings.TrimSpace(current.String()), + Op: OpSeq, + }) + current.Reset() + continue + } + + // || + if ch == '|' && i+1 < n && runes[i+1] == '|' { + segments = append(segments, Segment{ + Raw: strings.TrimSpace(current.String()), + Op: OpOr, + }) + current.Reset() + i++ // skip second | + continue + } + + // | (single pipe) + if ch == '|' { + segments = append(segments, Segment{ + Raw: strings.TrimSpace(current.String()), + Op: OpPipe, + }) + current.Reset() + continue + } + + current.WriteRune(ch) + } + + // last segment + last := strings.TrimSpace(current.String()) + if last != "" { + segments = append(segments, Segment{Raw: last, Op: OpNone}) + } + + return segments +} + +// ExecChain executes a command string with pipe/chaining support. +// Returns the combined output of all commands. +func ExecChain(command string) string { + segments := ParseChain(command) + if len(segments) == 0 { + return "[error] empty command" + } + + var collected []string + var lastOutput string + var lastErr error + pipeInput := "" + + for i, seg := range segments { + if i > 0 { + prevOp := segments[i-1].Op + // && semantics: skip if previous failed + if prevOp == OpAnd && lastErr != nil { + continue + } + // || semantics: skip if previous succeeded + if prevOp == OpOr && lastErr == nil { + continue + } + } + + // determine stdin for this segment + segStdin := "" + if i == 0 { + segStdin = pipeInput + } else if segments[i-1].Op == OpPipe { + segStdin = lastOutput + } + + lastOutput, lastErr = execSingle(seg.Raw, segStdin) + + // pipe: output flows to next command's stdin + // && or ;: collect output + if i < len(segments)-1 && seg.Op == OpPipe { + continue + } + if lastOutput != "" { + collected = append(collected, lastOutput) + } + } + + return strings.Join(collected, "\n") +} + +// execSingle executes a single command (with arguments) and returns output and error. +func execSingle(command, stdin string) (string, error) { + parts := tokenize(command) + if len(parts) == 0 { + return "", fmt.Errorf("empty command") + } + + name := parts[0] + args := parts[1:] + + // Check if it's a built-in Go command + if result := execBuiltin(name, args, stdin); result != "" { + return result, nil + } + + // Otherwise execute as system command + cmd := exec.Command(name, args...) + if stdin != "" { + cmd.Stdin = strings.NewReader(stdin) + } + output, err := cmd.CombinedOutput() + if err != nil { + return string(output), err + } + return string(output), nil +} + +// tokenize splits a command string by whitespace, respecting quotes. +func tokenize(input string) []string { + var tokens []string + var current strings.Builder + inQuote := false + var quoteChar rune + + for _, ch := range input { + if inQuote { + if ch == quoteChar { + inQuote = false + } else { + current.WriteRune(ch) + } + continue + } + + if ch == '\'' || ch == '"' { + inQuote = true + quoteChar = ch + continue + } + + if ch == ' ' || ch == '\t' { + if current.Len() > 0 { + tokens = append(tokens, current.String()) + current.Reset() + } + continue + } + + current.WriteRune(ch) + } + + if current.Len() > 0 { + tokens = append(tokens, current.String()) + } + + return tokens +} + +// execBuiltin executes a built-in command if it exists. +func execBuiltin(name string, args []string, stdin string) string { + switch name { + case "echo": + if stdin != "" { + return stdin + } + return strings.Join(args, " ") + case "time": + return "2006-01-02 15:04:05 MST" + case "cat": + if len(args) == 0 { + if stdin != "" { + return stdin + } + return "" + } + data, err := os.ReadFile(args[0]) + if err != nil { + return fmt.Sprintf("[error] cat: %v", err) + } + return string(data) + case "pwd": + return fsRootDir + case "cd": + if len(args) == 0 { + return "[error] usage: cd <dir>" + } + dir := args[0] + // Resolve relative to fsRootDir + abs := dir + if !filepath.IsAbs(dir) { + abs = filepath.Join(fsRootDir, dir) + } + abs = filepath.Clean(abs) + info, err := os.Stat(abs) + if err != nil { + return fmt.Sprintf("[error] cd: %v", err) + } + if !info.IsDir() { + return fmt.Sprintf("[error] cd: not a directory: %s", dir) + } + fsRootDir = abs + return fmt.Sprintf("Changed directory to: %s", fsRootDir) + } + return "" +} diff --git a/tools/fs.go b/tools/fs.go new file mode 100644 index 0000000..50b8da2 --- /dev/null +++ b/tools/fs.go @@ -0,0 +1,679 @@ +package tools + +import ( + "encoding/base64" + "fmt" + "os" + "os/exec" + "path/filepath" + "sort" + "strconv" + "strings" + "time" +) + +var fsRootDir string +var memoryStore MemoryStore +var agentRole string + +type MemoryStore interface { + Memorise(agent, topic, data string) (string, error) + Recall(agent, topic string) (string, error) + RecallTopics(agent string) ([]string, error) + Forget(agent, topic string) error +} + +func SetMemoryStore(store MemoryStore, role string) { + memoryStore = store + agentRole = role +} + +func SetFSRoot(dir string) { + fsRootDir = dir +} + +func GetFSRoot() string { + return fsRootDir +} + +func SetFSCwd(dir string) error { + abs, err := filepath.Abs(dir) + if err != nil { + return err + } + info, err := os.Stat(abs) + if err != nil { + return err + } + if !info.IsDir() { + return fmt.Errorf("not a directory: %s", dir) + } + fsRootDir = abs + return nil +} + +func resolvePath(rel string) (string, error) { + if fsRootDir == "" { + return "", fmt.Errorf("fs root not set") + } + + if filepath.IsAbs(rel) { + abs := filepath.Clean(rel) + if !strings.HasPrefix(abs, fsRootDir+string(os.PathSeparator)) && abs != fsRootDir { + return "", fmt.Errorf("path escapes fs root: %s", rel) + } + return abs, nil + } + + abs := filepath.Join(fsRootDir, rel) + abs = filepath.Clean(abs) + if !strings.HasPrefix(abs, fsRootDir+string(os.PathSeparator)) && abs != fsRootDir { + return "", fmt.Errorf("path escapes fs root: %s", rel) + } + return abs, nil +} + +func humanSize(n int64) string { + switch { + case n >= 1<<20: + return fmt.Sprintf("%.1fMB", float64(n)/float64(1<<20)) + case n >= 1<<10: + return fmt.Sprintf("%.1fKB", float64(n)/float64(1<<10)) + default: + return fmt.Sprintf("%dB", n) + } +} + +func IsImageFile(path string) bool { + ext := strings.ToLower(filepath.Ext(path)) + return ext == ".png" || ext == ".jpg" || ext == ".jpeg" || ext == ".gif" || ext == ".webp" || ext == ".svg" +} + +func FsLs(args []string, stdin string) string { + dir := "" + if len(args) > 0 { + dir = args[0] + } + abs, err := resolvePath(dir) + if err != nil { + return fmt.Sprintf("[error] %v", err) + } + + entries, err := os.ReadDir(abs) + if err != nil { + return fmt.Sprintf("[error] ls: %v", err) + } + + var out strings.Builder + for _, e := range entries { + info, _ := e.Info() + if e.IsDir() { + fmt.Fprintf(&out, "d %-8s %s/\n", "-", e.Name()) + } else if info != nil { + fmt.Fprintf(&out, "f %-8s %s\n", humanSize(info.Size()), e.Name()) + } else { + fmt.Fprintf(&out, "f %-8s %s\n", "?", e.Name()) + } + } + if out.Len() == 0 { + return "(empty directory)" + } + return strings.TrimRight(out.String(), "\n") +} + +func FsCat(args []string, stdin string) string { + b64 := false + var path string + for _, a := range args { + if a == "-b" || a == "--base64" { + b64 = true + } else if path == "" { + path = a + } + } + if path == "" { + return "[error] usage: cat <path>" + } + + abs, err := resolvePath(path) + if err != nil { + return fmt.Sprintf("[error] %v", err) + } + + data, err := os.ReadFile(abs) + if err != nil { + return fmt.Sprintf("[error] cat: %v", err) + } + + if b64 { + result := base64.StdEncoding.EncodeToString(data) + if IsImageFile(path) { + result += fmt.Sprintf("\n", abs) + } + return result + } + return string(data) +} + +func FsSee(args []string, stdin string) string { + if len(args) == 0 { + return "[error] usage: see <image-path>" + } + path := args[0] + + abs, err := resolvePath(path) + if err != nil { + return fmt.Sprintf("[error] %v", err) + } + + info, err := os.Stat(abs) + if err != nil { + return fmt.Sprintf("[error] see: %v", err) + } + + if !IsImageFile(path) { + return fmt.Sprintf("[error] not an image file: %s (use cat to read text files)", path) + } + + return fmt.Sprintf("Image: %s (%s)\n", path, humanSize(info.Size()), abs) +} + +func FsWrite(args []string, stdin string) string { + b64 := false + var path string + var contentParts []string + for _, a := range args { + if a == "-b" || a == "--base64" { + b64 = true + } else if path == "" { + path = a + } else { + contentParts = append(contentParts, a) + } + } + if path == "" { + return "[error] usage: write <path> [content] or pipe stdin" + } + + abs, err := resolvePath(path) + if err != nil { + return fmt.Sprintf("[error] %v", err) + } + + if err := os.MkdirAll(filepath.Dir(abs), 0o755); err != nil { + return fmt.Sprintf("[error] mkdir: %v", err) + } + + var data []byte + if b64 { + src := stdin + if src == "" && len(contentParts) > 0 { + src = strings.Join(contentParts, " ") + } + src = strings.TrimSpace(src) + var err error + data, err = base64.StdEncoding.DecodeString(src) + if err != nil { + return fmt.Sprintf("[error] base64 decode: %v", err) + } + } else { + if len(contentParts) > 0 { + data = []byte(strings.Join(contentParts, " ")) + } else { + data = []byte(stdin) + } + } + + if err := os.WriteFile(abs, data, 0o644); err != nil { + return fmt.Sprintf("[error] write: %v", err) + } + + size := humanSize(int64(len(data))) + result := fmt.Sprintf("Written %s → %s", size, path) + + if IsImageFile(path) { + result += fmt.Sprintf("\n", abs) + } + + return result +} + +func FsStat(args []string, stdin string) string { + if len(args) == 0 { + return "[error] usage: stat <path>" + } + + abs, err := resolvePath(args[0]) + if err != nil { + return fmt.Sprintf("[error] %v", err) + } + + info, err := os.Stat(abs) + if err != nil { + return fmt.Sprintf("[error] stat: %v", err) + } + + mime := "application/octet-stream" + if IsImageFile(args[0]) { + ext := strings.ToLower(filepath.Ext(args[0])) + switch ext { + case ".png": + mime = "image/png" + case ".jpg", ".jpeg": + mime = "image/jpeg" + case ".gif": + mime = "image/gif" + case ".webp": + mime = "image/webp" + case ".svg": + mime = "image/svg+xml" + } + } + + var out strings.Builder + fmt.Fprintf(&out, "File: %s\n", args[0]) + fmt.Fprintf(&out, "Size: %s (%d bytes)\n", humanSize(info.Size()), info.Size()) + fmt.Fprintf(&out, "Type: %s\n", mime) + fmt.Fprintf(&out, "Modified: %s\n", info.ModTime().Format(time.RFC3339)) + if info.IsDir() { + fmt.Fprintf(&out, "Kind: directory\n") + } + return strings.TrimRight(out.String(), "\n") +} + +func FsRm(args []string, stdin string) string { + if len(args) == 0 { + return "[error] usage: rm <path>" + } + + abs, err := resolvePath(args[0]) + if err != nil { + return fmt.Sprintf("[error] %v", err) + } + + if err := os.RemoveAll(abs); err != nil { + return fmt.Sprintf("[error] rm: %v", err) + } + return fmt.Sprintf("Removed %s", args[0]) +} + +func FsCp(args []string, stdin string) string { + if len(args) < 2 { + return "[error] usage: cp <src> <dst>" + } + + srcAbs, err := resolvePath(args[0]) + if err != nil { + return fmt.Sprintf("[error] %v", err) + } + dstAbs, err := resolvePath(args[1]) + if err != nil { + return fmt.Sprintf("[error] %v", err) + } + + data, err := os.ReadFile(srcAbs) + if err != nil { + return fmt.Sprintf("[error] cp read: %v", err) + } + + if err := os.MkdirAll(filepath.Dir(dstAbs), 0o755); err != nil { + return fmt.Sprintf("[error] cp mkdir: %v", err) + } + + if err := os.WriteFile(dstAbs, data, 0o644); err != nil { + return fmt.Sprintf("[error] cp write: %v", err) + } + return fmt.Sprintf("Copied %s → %s (%s)", args[0], args[1], humanSize(int64(len(data)))) +} + +func FsMv(args []string, stdin string) string { + if len(args) < 2 { + return "[error] usage: mv <src> <dst>" + } + + srcAbs, err := resolvePath(args[0]) + if err != nil { + return fmt.Sprintf("[error] %v", err) + } + dstAbs, err := resolvePath(args[1]) + if err != nil { + return fmt.Sprintf("[error] %v", err) + } + + if err := os.MkdirAll(filepath.Dir(dstAbs), 0o755); err != nil { + return fmt.Sprintf("[error] mv mkdir: %v", err) + } + + if err := os.Rename(srcAbs, dstAbs); err != nil { + return fmt.Sprintf("[error] mv: %v", err) + } + return fmt.Sprintf("Moved %s → %s", args[0], args[1]) +} + +func FsMkdir(args []string, stdin string) string { + if len(args) == 0 { + return "[error] usage: mkdir <dir>" + } + + abs, err := resolvePath(args[0]) + if err != nil { + return fmt.Sprintf("[error] %v", err) + } + + if err := os.MkdirAll(abs, 0o755); err != nil { + return fmt.Sprintf("[error] mkdir: %v", err) + } + return fmt.Sprintf("Created %s", args[0]) +} + +// Text processing commands + +func FsEcho(args []string, stdin string) string { + if stdin != "" { + return stdin + } + return strings.Join(args, " ") +} + +func FsTime(args []string, stdin string) string { + return time.Now().Format("2006-01-02 15:04:05 MST") +} + +func FsGrep(args []string, stdin string) string { + if len(args) == 0 { + return "[error] usage: grep [-i] [-v] [-c] <pattern>" + } + ignoreCase := false + invert := false + countOnly := false + var pattern string + for _, a := range args { + switch a { + case "-i": + ignoreCase = true + case "-v": + invert = true + case "-c": + countOnly = true + default: + pattern = a + } + } + if pattern == "" { + return "[error] pattern required" + } + if ignoreCase { + pattern = strings.ToLower(pattern) + } + + lines := strings.Split(stdin, "\n") + var matched []string + for _, line := range lines { + haystack := line + if ignoreCase { + haystack = strings.ToLower(line) + } + match := strings.Contains(haystack, pattern) + if invert { + match = !match + } + if match { + matched = append(matched, line) + } + } + if countOnly { + return fmt.Sprintf("%d", len(matched)) + } + return strings.Join(matched, "\n") +} + +func FsHead(args []string, stdin string) string { + n := 10 + for i, a := range args { + if a == "-n" && i+1 < len(args) { + if parsed, err := strconv.Atoi(args[i+1]); err == nil { + n = parsed + } + } else if strings.HasPrefix(a, "-") { + continue + } else if parsed, err := strconv.Atoi(a); err == nil { + n = parsed + } + } + lines := strings.Split(stdin, "\n") + if n > 0 && len(lines) > n { + lines = lines[:n] + } + return strings.Join(lines, "\n") +} + +func FsTail(args []string, stdin string) string { + n := 10 + for i, a := range args { + if a == "-n" && i+1 < len(args) { + if parsed, err := strconv.Atoi(args[i+1]); err == nil { + n = parsed + } + } else if strings.HasPrefix(a, "-") { + continue + } else if parsed, err := strconv.Atoi(a); err == nil { + n = parsed + } + } + lines := strings.Split(stdin, "\n") + if n > 0 && len(lines) > n { + lines = lines[len(lines)-n:] + } + return strings.Join(lines, "\n") +} + +func FsWc(args []string, stdin string) string { + lines := len(strings.Split(stdin, "\n")) + words := len(strings.Fields(stdin)) + chars := len(stdin) + if len(args) > 0 { + switch args[0] { + case "-l": + return fmt.Sprintf("%d", lines) + case "-w": + return fmt.Sprintf("%d", words) + case "-c": + return fmt.Sprintf("%d", chars) + } + } + return fmt.Sprintf("%d lines, %d words, %d chars", lines, words, chars) +} + +func FsSort(args []string, stdin string) string { + lines := strings.Split(stdin, "\n") + reverse := false + numeric := false + for _, a := range args { + if a == "-r" { + reverse = true + } else if a == "-n" { + numeric = true + } + } + + sortFunc := func(i, j int) bool { + if numeric { + ni, _ := strconv.Atoi(lines[i]) + nj, _ := strconv.Atoi(lines[j]) + if reverse { + return ni > nj + } + return ni < nj + } + if reverse { + return lines[i] > lines[j] + } + return lines[i] < lines[j] + } + + sort.Slice(lines, sortFunc) + return strings.Join(lines, "\n") +} + +func FsUniq(args []string, stdin string) string { + lines := strings.Split(stdin, "\n") + showCount := false + for _, a := range args { + if a == "-c" { + showCount = true + } + } + + var result []string + var prev string + first := true + count := 0 + for _, line := range lines { + if first || line != prev { + if !first && showCount { + result = append(result, fmt.Sprintf("%d %s", count, prev)) + } else if !first { + result = append(result, prev) + } + count = 1 + prev = line + first = false + } else { + count++ + } + } + if !first { + if showCount { + result = append(result, fmt.Sprintf("%d %s", count, prev)) + } else { + result = append(result, prev) + } + } + return strings.Join(result, "\n") +} + +var allowedGitSubcommands = map[string]bool{ + "status": true, + "log": true, + "diff": true, + "show": true, + "branch": true, + "reflog": true, + "rev-parse": true, + "shortlog": true, + "describe": true, + "rev-list": true, +} + +func FsGit(args []string, stdin string) string { + if len(args) == 0 { + return "[error] usage: git <subcommand> [options]" + } + + subcmd := args[0] + if !allowedGitSubcommands[subcmd] { + return fmt.Sprintf("[error] git: '%s' is not an allowed git command. Allowed: status, log, diff, show, branch, reflog, rev-parse, shortlog, describe, rev-list", subcmd) + } + + abs, err := resolvePath(".") + if err != nil { + return fmt.Sprintf("[error] git: %v", err) + } + + // Pass all args to git (first arg is subcommand, rest are options) + cmd := exec.Command("git", args...) + cmd.Dir = abs + output, err := cmd.CombinedOutput() + if err != nil { + return fmt.Sprintf("[error] git %s: %v\n%s", subcmd, err, string(output)) + } + return string(output) +} + +func FsPwd(args []string, stdin string) string { + return fsRootDir +} + +func FsCd(args []string, stdin string) string { + if len(args) == 0 { + return "[error] usage: cd <dir>" + } + dir := args[0] + abs, err := resolvePath(dir) + if err != nil { + return fmt.Sprintf("[error] cd: %v", err) + } + info, err := os.Stat(abs) + if err != nil { + return fmt.Sprintf("[error] cd: %v", err) + } + if !info.IsDir() { + return fmt.Sprintf("[error] cd: not a directory: %s", dir) + } + fsRootDir = abs + return fmt.Sprintf("Changed directory to: %s", fsRootDir) +} + +func FsMemory(args []string, stdin string) string { + if len(args) == 0 { + return "[error] usage: memory store <topic> <data> | memory get <topic> | memory list | memory forget <topic>" + } + + if memoryStore == nil { + return "[error] memory store not initialized" + } + + switch args[0] { + case "store": + if len(args) < 3 && stdin == "" { + return "[error] usage: memory store <topic> <data>" + } + topic := args[1] + var data string + if len(args) >= 3 { + data = strings.Join(args[2:], " ") + } else { + data = stdin + } + _, err := memoryStore.Memorise(agentRole, topic, data) + if err != nil { + return fmt.Sprintf("[error] failed to store: %v", err) + } + return fmt.Sprintf("Stored under topic: %s", topic) + + case "get": + if len(args) < 2 { + return "[error] usage: memory get <topic>" + } + topic := args[1] + data, err := memoryStore.Recall(agentRole, topic) + if err != nil { + return fmt.Sprintf("[error] failed to recall: %v", err) + } + return fmt.Sprintf("Topic: %s\n%s", topic, data) + + case "list", "topics": + topics, err := memoryStore.RecallTopics(agentRole) + if err != nil { + return fmt.Sprintf("[error] failed to list topics: %v", err) + } + if len(topics) == 0 { + return "No topics stored." + } + return "Topics: " + strings.Join(topics, ", ") + + case "forget", "delete": + if len(args) < 2 { + return "[error] usage: memory forget <topic>" + } + topic := args[1] + err := memoryStore.Forget(agentRole, topic) + if err != nil { + return fmt.Sprintf("[error] failed to forget: %v", err) + } + return fmt.Sprintf("Deleted topic: %s", topic) + + default: + return fmt.Sprintf("[error] unknown subcommand: %s. Use: store, get, list, topics, forget, delete", args[0]) + } +} |
