返回 DeepSeek-Reasonix
cli_flags.go
根目录 / internal / cli / cli_flags.go
1 package cli
2
3 import (
4 "fmt"
5 "os"
6 "path/filepath"
7 "strings"
8 "unicode"
9
10 "reasonix/internal/agent"
11 )
12
13 const resumePickerSentinel = "__reasonix_resume_picker__"
14
15 func splitAllowedToolRules(values []string) ([]string, error) {
16 var rules []string
17 for _, value := range values {
18 start := -1
19 depth := 0
20 flush := func(end int) {
21 if start < 0 {
22 return
23 }
24 if rule := strings.TrimSpace(value[start:end]); rule != "" {
25 rules = append(rules, rule)
26 }
27 start = -1
28 }
29 for i, r := range value {
30 switch r {
31 case '(':
32 if start < 0 {
33 start = i
34 }
35 depth++
36 case ')':
37 if depth == 0 {
38 return nil, fmt.Errorf("invalid --allowed-tools value %q: unexpected ')'", value)
39 }
40 depth--
41 default:
42 if depth == 0 && (r == ',' || unicode.IsSpace(r)) {
43 flush(i)
44 continue
45 }
46 if start < 0 {
47 start = i
48 }
49 }
50 }
51 if depth != 0 {
52 return nil, fmt.Errorf("invalid --allowed-tools value %q: unclosed '('", value)
53 }
54 flush(len(value))
55 }
56 return uniqueStrings(rules), nil
57 }
58
59 func uniqueStrings(values []string) []string {
60 seen := make(map[string]struct{}, len(values))
61 out := make([]string, 0, len(values))
62 for _, value := range values {
63 value = strings.TrimSpace(value)
64 if value == "" {
65 continue
66 }
67 if _, ok := seen[value]; ok {
68 continue
69 }
70 seen[value] = struct{}{}
71 out = append(out, value)
72 }
73 return out
74 }
75
76 // hasLeadingPrintFlag reports whether a standalone -p/--print token appears in
77 // the top-level flag run, i.e. before any "--" terminator. reasonix has no
78 // interactive -p, so its presence means the user wants one-shot print mode even
79 // when it trails other flags (`reasonix --model X -p "task"`).
80 func hasLeadingPrintFlag(args []string) bool {
81 for _, arg := range args {
82 if arg == "--" {
83 return false
84 }
85 if arg == "-p" || arg == "--print" {
86 return true
87 }
88 }
89 return false
90 }
91
92 // stripLeadingPrintFlag drops the first standalone -p/--print token before any
93 // "--" terminator, leaving the rest (including everything after "--") untouched.
94 // Used when re-routing a top-level invocation to `run --print` so the print flag
95 // is not duplicated.
96 func stripLeadingPrintFlag(args []string) []string {
97 out := make([]string, 0, len(args))
98 dropped := false
99 for i, arg := range args {
100 if arg == "--" {
101 out = append(out, args[i:]...)
102 break
103 }
104 if !dropped && (arg == "-p" || arg == "--print") {
105 dropped = true
106 continue
107 }
108 out = append(out, arg)
109 }
110 return out
111 }
112
113 // normalizeOptionalResumeArg gives pflag the optional-value behavior Claude's
114 // --resume [value] exposes. Interactive sessions have no positional arguments,
115 // so a following non-flag token is unambiguously the resume query.
116 func normalizeOptionalResumeArg(args []string) []string {
117 out := make([]string, 0, len(args))
118 for i := 0; i < len(args); i++ {
119 arg := args[i]
120 if (arg == "--resume" || arg == "-r") && i+1 < len(args) && !strings.HasPrefix(args[i+1], "-") {
121 out = append(out, arg+"="+args[i+1])
122 i++
123 continue
124 }
125 out = append(out, arg)
126 }
127 return out
128 }
129
130 func resolveSessionQuery(dir, query string) (string, error) {
131 query = strings.TrimSpace(query)
132 if query == "" || query == resumePickerSentinel {
133 return "", nil
134 }
135 if info, err := os.Stat(query); err == nil && !info.IsDir() {
136 abs, absErr := filepath.Abs(query)
137 if absErr != nil {
138 return "", absErr
139 }
140 return abs, nil
141 }
142 sessions, err := agent.ListSessions(dir)
143 if err != nil {
144 return "", fmt.Errorf("list sessions: %w", err)
145 }
146 // Opaque machine session IDs (session_<hex>) are what --events-jsonl and
147 // `session show --json` expose. Match them before preview/partial search so
148 // one-shot `run --resume` can resume without scanning private paths (#7429).
149 if looksLikeMachineSessionID(query) {
150 key, keyErr := loadMachineIdentityKey()
151 if keyErr != nil {
152 return "", fmt.Errorf("machine identity is unavailable: %w", keyErr)
153 }
154 for _, session := range sessions {
155 if machineSessionIDWithKey(agent.BranchID(session.Path), key) == query {
156 return session.Path, nil
157 }
158 }
159 return "", fmt.Errorf("no session matches %q", query)
160 }
161 lower := strings.ToLower(query)
162 var exact []string
163 var partial []string
164 for _, session := range sessions {
165 id := agent.BranchID(session.Path)
166 base := filepath.Base(session.Path)
167 if query == id || query == base || query == session.Path {
168 exact = append(exact, session.Path)
169 continue
170 }
171 haystack := strings.ToLower(strings.Join([]string{id, base, session.CustomTitle, session.TopicTitle, session.Preview}, "\n"))
172 if strings.Contains(haystack, lower) {
173 partial = append(partial, session.Path)
174 }
175 }
176 matches := exact
177 if len(matches) == 0 {
178 matches = partial
179 }
180 switch len(matches) {
181 case 0:
182 return "", fmt.Errorf("no session matches %q", query)
183 case 1:
184 return matches[0], nil
185 default:
186 return "", fmt.Errorf("session query %q is ambiguous (%d matches)", query, len(matches))
187 }
188 }
189
190 // looksLikeMachineSessionID reports whether query is the opaque HMAC form
191 // emitted by machineSessionIDWithKey (`session_` + 32 lowercase hex chars).
192 func looksLikeMachineSessionID(query string) bool {
193 const prefix = "session_"
194 if !strings.HasPrefix(query, prefix) {
195 return false
196 }
197 hexPart := query[len(prefix):]
198 if len(hexPart) != 32 {
199 return false
200 }
201 for i := 0; i < len(hexPart); i++ {
202 c := hexPart[i]
203 if (c < '0' || c > '9') && (c < 'a' || c > 'f') {
204 return false
205 }
206 }
207 return true
208 }
209
209 lines GO