@@ -6,11 +6,16 @@ import (
66 "os"
77 "os/exec"
88 "os/signal"
9+ "runtime/debug"
910 "strings"
1011
1112 "github.com/BackendStack21/kode"
1213)
1314
15+ // version is set at build time via ldflags: -ldflags "-X main.version=v0.2.1"
16+ // Falls back to VCS tag from debug.ReadBuildInfo, then to "dev".
17+ var version string
18+
1419const defaultSystem = `You are kode, an autonomous AI coding agent. You solve tasks by reasoning step by step, then executing tools.
1520
1621Rules:
@@ -27,148 +32,212 @@ func main() {
2732
2833 switch os .Args [1 ] {
2934 case "run" :
30- runCmd ()
35+ if err := run (os .Args [2 :]); err != nil {
36+ fmt .Fprintf (os .Stderr , "kode: %v\n " , err )
37+ os .Exit (1 )
38+ }
3139 case "version" :
32- fmt .Println ("kode v0.2.0" )
40+ fmt .Println ("kode" , getVersion () )
3341 default :
3442 fmt .Fprintf (os .Stderr , "kode: unknown command %q\n " , os .Args [1 ])
3543 printUsage ()
3644 os .Exit (1 )
3745 }
3846}
3947
40- func printUsage () {
41- fmt .Println (`Usage:
42- kode run [flags] <task>
43- kode version
44-
45- Flags:
46- --model <name> LLM model (default: deepseek-chat)
47- --base-url <url> API endpoint (default: https://api.deepseek.com/v1)
48- --max-iter <n> Max think->act cycles (default: 90)
49- --thinking <level> Reasoning depth: enabled|disabled (Deepseek) or low|medium|high (OpenAI o-series)
50- --sandbox Run in isolated Docker container
51- --system <prompt> System prompt override` )
48+ // runFlags holds the parsed CLI flags for `kode run`.
49+ type runFlags struct {
50+ Model string
51+ BaseURL string
52+ System string
53+ Thinking string
54+ MaxIter int
55+ Sandbox bool
56+ Task string
5257}
5358
54- func runCmd () {
55- args := os . Args [ 2 :]
56- var model , baseURL , system , thinking string
57- maxIter := 90
58- sandbox := false
59+ // parseRunFlags parses `kode run` arguments and returns the parsed flags.
60+ // Exported for testing.
61+ func parseRunFlags ( args [] string ) ( runFlags , error ) {
62+ var f runFlags
63+ f . MaxIter = 90
5964
6065 i := 0
6166 for i < len (args )- 1 {
6267 switch args [i ] {
6368 case "--model" :
64- model = args [i + 1 ]
69+ f . Model = args [i + 1 ]
6570 i += 2
6671 case "--base-url" :
67- baseURL = args [i + 1 ]
72+ f . BaseURL = args [i + 1 ]
6873 i += 2
6974 case "--max-iter" :
70- fmt .Sscanf (args [i + 1 ], "%d" , & maxIter )
75+ fmt .Sscanf (args [i + 1 ], "%d" , & f . MaxIter )
7176 i += 2
7277 case "--system" :
73- system = args [i + 1 ]
78+ f . System = args [i + 1 ]
7479 i += 2
7580 case "--thinking" :
76- thinking = args [i + 1 ]
81+ f . Thinking = args [i + 1 ]
7782 i += 2
7883 case "--sandbox" :
79- sandbox = true
84+ f . Sandbox = true
8085 i ++
8186 default :
8287 // Not a flag — treat remaining as the task
8388 goto done
8489 }
8590 }
8691done:
87- task := strings .Join (args [i :], " " )
88- if task == "" {
89- fmt .Fprintln (os .Stderr , "kode: no task provided" )
90- os .Exit (1 )
92+ f .Task = strings .Join (args [i :], " " )
93+ if f .Task == "" {
94+ return f , fmt .Errorf ("no task provided" )
9195 }
96+ return f , nil
97+ }
9298
93- if system == "" {
94- system = defaultSystem
99+ func printUsage () {
100+ fmt .Println (`Usage:
101+ kode run [flags] <task>
102+ kode version
103+
104+ Flags:
105+ --model <name> LLM model (default: deepseek-chat)
106+ --base-url <url> API endpoint (default: https://api.deepseek.com/v1)
107+ --max-iter <n> Max think->act cycles (default: 90)
108+ --thinking <level> Reasoning depth: enabled|disabled (Deepseek) or low|medium|high (OpenAI o-series)
109+ --sandbox Run in isolated Docker container
110+ --system <prompt> System prompt override` )
111+ }
112+
113+ // run executes the `kode run` command and returns an error on failure.
114+ // The caller is responsible for printing the error and calling os.Exit.
115+ func run (args []string ) error {
116+ f , err := parseRunFlags (args )
117+ if err != nil {
118+ return err
95119 }
96120
97- ctx , cancel := signal .NotifyContext (context .Background (), os .Interrupt )
98- defer cancel ()
121+ if f .System == "" {
122+ f .System = defaultSystem
123+ }
99124
100- // --- sandbox setup ---
125+ // Sandbox setup
101126 var sandboxCleanup func () error
102127 tools := builtinTools ()
103128
104- if sandbox {
105- containerName := fmt .Sprintf ("kode-%d" , os .Getpid ())
106- fmt .Fprintf (os .Stderr , "kode: starting sandbox container %s...\n " , containerName )
107-
108- wd , err := os .Getwd ()
129+ if f .Sandbox {
130+ cleanup , err := setupSandbox (tools )
109131 if err != nil {
110- fmt .Fprintf (os .Stderr , "kode: sandbox: getwd: %v\n " , err )
111- os .Exit (1 )
112- }
113-
114- createCmd := exec .Command ("docker" , "run" ,
115- "--rm" , // destroy on exit
116- "--detach" , // run in background
117- "--name" , containerName ,
118- "--cap-drop" , "ALL" , // no capabilities
119- "--security-opt" , "no-new-privileges" , // no privilege escalation
120- "--network" , "none" , // no network
121- "--tmpfs" , "/tmp:noexec" , // no executable temp files
122- "-v" , wd + ":/workspace" , // working dir (read-write inside sandbox)
123- "alpine:latest" ,
124- "sleep" , "infinity" ,
125- )
126- createCmd .Stderr = os .Stderr
127- if err := createCmd .Run (); err != nil {
128- fmt .Fprintf (os .Stderr , "kode: sandbox: failed to create container: %v\n " , err )
129- os .Exit (1 )
130- }
131-
132- sandboxCleanup = func () error {
133- fmt .Fprintf (os .Stderr , "kode: destroying sandbox container %s...\n " , containerName )
134- return exec .Command ("docker" , "rm" , "-f" , containerName ).Run ()
132+ return fmt .Errorf ("sandbox: %w" , err )
135133 }
136-
137- // Wire the shell tool to execute commands inside the sandbox.
138- tools [0 ].(* shellTool ).containerName = containerName
134+ sandboxCleanup = cleanup
139135 }
140136
141137 agent , err := kode .New (kode.Config {
142- Model : model ,
143- BaseURL : baseURL ,
144- MaxIterations : maxIter ,
145- SystemMessage : system ,
146- Thinking : thinking ,
138+ Model : f . Model ,
139+ BaseURL : f . BaseURL ,
140+ MaxIterations : f . MaxIter ,
141+ SystemMessage : f . System ,
142+ Thinking : f . Thinking ,
147143 Tools : tools ,
148144 SandboxCleanup : sandboxCleanup ,
149145 })
150146 if err != nil {
151- fmt .Fprintf (os .Stderr , "kode: %v\n " , err )
152- os .Exit (1 )
147+ return err
153148 }
154149 defer agent .Close ()
155150
156- modelName := model
151+ modelName := f . Model
157152 if modelName == "" {
158153 modelName = "deepseek-chat"
159154 }
160155 fmt .Fprintf (os .Stderr , "kode: %s thinking...\n " , modelName )
161156
162- result , err := agent .Run (ctx , task )
157+ ctx , cancel := signal .NotifyContext (context .Background (), os .Interrupt )
158+ defer cancel ()
159+
160+ result , err := agent .Run (ctx , f .Task )
163161 if err != nil {
164- fmt .Fprintf (os .Stderr , "kode: %v\n " , err )
165- os .Exit (1 )
162+ return err
166163 }
167164 fmt .Println (result )
165+ return nil
166+ }
167+
168+ // setupSandbox creates a Docker container and wires the shell tool to use it.
169+ // Returns a cleanup function that destroys the container.
170+ func setupSandbox (tools []kode.Tool ) (func () error , error ) {
171+ containerName := fmt .Sprintf ("kode-%d" , os .Getpid ())
172+ fmt .Fprintf (os .Stderr , "kode: starting sandbox container %s...\n " , containerName )
173+
174+ wd , err := os .Getwd ()
175+ if err != nil {
176+ return nil , fmt .Errorf ("getwd: %w" , err )
177+ }
178+
179+ createCmd := exec .Command ("docker" , "run" ,
180+ "--rm" , // destroy on exit
181+ "--detach" , // run in background
182+ "--name" , containerName ,
183+ "--cap-drop" , "ALL" , // no capabilities
184+ "--security-opt" , "no-new-privileges" , // no privilege escalation
185+ "--network" , "none" , // no network
186+ "--tmpfs" , "/tmp:noexec" , // no executable temp files
187+ "-v" , wd + ":/workspace" , // working dir (read-write inside sandbox)
188+ "alpine:latest" ,
189+ "sleep" , "infinity" ,
190+ )
191+ createCmd .Stderr = os .Stderr
192+ if err := createCmd .Run (); err != nil {
193+ return nil , fmt .Errorf ("failed to create container: %w" , err )
194+ }
195+
196+ cleanup := func () error {
197+ fmt .Fprintf (os .Stderr , "kode: destroying sandbox container %s...\n " , containerName )
198+ return exec .Command ("docker" , "rm" , "-f" , containerName ).Run ()
199+ }
200+
201+ // Wire the shell tool to execute commands inside the sandbox.
202+ tools [0 ].(* shellTool ).containerName = containerName
203+ return cleanup , nil
168204}
169205
170206func builtinTools () []kode.Tool {
171207 return []kode.Tool {
172208 & shellTool {},
173209 }
210+ }
211+
212+ // getVersion returns the version string. Resolution order:
213+ // 1. ldflags override (-X main.version=v0.2.1)
214+ // 2. VCS tag from debug.ReadBuildInfo (when built with go install)
215+ // 3. VCS revision (short commit hash)
216+ // 4. "dev" (local go build without VCS info)
217+ func getVersion () string {
218+ if version != "" {
219+ return version
220+ }
221+ info , ok := debug .ReadBuildInfo ()
222+ if ! ok {
223+ return "dev"
224+ }
225+ var revision string
226+ for _ , s := range info .Settings {
227+ switch s .Key {
228+ case "vcs.revision" :
229+ revision = s .Value
230+ if len (revision ) > 7 {
231+ revision = revision [:7 ]
232+ }
233+ case "vcs.tag" :
234+ if s .Value != "" {
235+ return s .Value
236+ }
237+ }
238+ }
239+ if revision != "" {
240+ return revision
241+ }
242+ return "dev"
174243}
0 commit comments