bots-garden/mini-mepublic Fork 0
main
Commits
Clone
git clone https://git.rickub.com/bots-garden/mini-me.git
git clone ssh://git@rickub.com/bots-garden/mini-me.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

mention.go · 114 lines · 3.5 KBGo Blame HistoryRaw
💾 Saved. d722711 k33g 4h ago1// Package mention understands the @path notation of a prompt: "explain
2// @internal/acp/acp.go" names a file the model should look at. There is no
3// picker — the user types the path — and the file's content does not travel
4// in the prompt: as for an editor's @-mention over ACP, the mention becomes an
5// `[attached file: <absolute path>]` line and the model reads the file with
6// its tools. Both front ends use it, so a typed @path means the same thing in
7// the terminal and in an editor whose picker was bypassed.
8package mention
9
10import (
11 "fmt"
12 "os"
13 "path/filepath"
14 "strings"
15)
16
17// Attachment is one @path mention that named an existing file or directory.
18type Attachment struct {
19 Mention string // as typed, without the @ and the trailing punctuation
20 Path string // absolute, cleaned
21 Dir bool // a directory rather than a file
22}
23
24// trailing is the sentence punctuation a mention may be glued to — "look at
25// @main.go, then…" mentions main.go, not "main.go,".
26const trailing = ",.;:!?)]}'\"`"
27
28// Expand finds the @path mentions of input, resolves them against cwd (an
29// absolute path starts with / or ~/, anything else is relative to cwd) and
30// returns the input followed by one attachment line per existing path, in
31// order of first appearance, each path once:
32//
33// [attached file: /work/project/main.go]
34// [attached directory: /work/project/internal]
35//
36// A mention that names nothing on disk is left alone: "@bob" in a sentence is
37// not a file. A @ glued to a word (an e-mail address) is not a mention either.
38func Expand(input, cwd string) (string, []Attachment) {
39 var out []Attachment
40 seen := map[string]bool{}
41 for _, m := range mentions(input) {
42 path := resolve(m, cwd)
43 if seen[path] {
44 continue
45 }
46 info, err := os.Stat(path)
47 if err != nil {
48 continue
49 }
50 seen[path] = true
51 out = append(out, Attachment{Mention: m, Path: path, Dir: info.IsDir()})
52 }
53 var b strings.Builder
54 b.WriteString(input)
55 for _, a := range out {
56 fmt.Fprintf(&b, "\n%s", a.Line())
57 }
58 return b.String(), out
59}
60
61// Line is the attachment as the model sees it — the same shape an ACP
62// resource_link takes, so the model meets one convention, not two.
63func (a Attachment) Line() string {
64 if a.Dir {
65 return fmt.Sprintf("[attached directory: %s]", a.Path)
66 }
67 return fmt.Sprintf("[attached file: %s]", a.Path)
68}
69
70// mentions returns the candidate paths: every "@something" that starts the
71// input or follows a space, an opening bracket or a quote, stripped of the
72// punctuation it may end with. Existence is the caller's business.
73func mentions(input string) []string {
74 var out []string
75 for i := 0; i < len(input); i++ {
76 if input[i] != '@' || !startsMention(input, i) {
77 continue
78 }
79 end := i + 1
80 for end < len(input) && !isSpace(input[end]) {
81 end++
82 }
83 m := strings.TrimRight(input[i+1:end], trailing)
84 if m != "" {
85 out = append(out, m)
86 }
87 i = end
88 }
89 return out
90}
91
92func startsMention(input string, i int) bool {
93 if i == 0 {
94 return true
95 }
96 return isSpace(input[i-1]) || strings.IndexByte("([{'\"`", input[i-1]) >= 0
97}
98
99func isSpace(c byte) bool {
100 return c == ' ' || c == '\t' || c == '\n' || c == '\r'
101}
102
103// resolve turns a mention into an absolute, cleaned path.
104func resolve(m, cwd string) string {
105 switch {
106 case filepath.IsAbs(m):
107 return filepath.Clean(m)
108 case m == "~" || strings.HasPrefix(m, "~/"):
109 if home, err := os.UserHomeDir(); err == nil {
110 return filepath.Clean(filepath.Join(home, m[1:]))
111 }
112 }
113 return filepath.Clean(filepath.Join(cwd, m))
114}