@@ -9,7 +9,9 @@ package main
99import (
1010 "encoding/json"
1111 "fmt"
12+ "io"
1213 "math"
14+ "net/http"
1315 "os"
1416 "strconv"
1517 "strings"
@@ -19,6 +21,7 @@ import (
1921 "github.com/DOS/DOSRouter/proxy"
2022 "github.com/DOS/DOSRouter/router"
2123 "github.com/DOS/DOSRouter/stats"
24+ "github.com/DOS/DOSRouter/wallet"
2225)
2326
2427func main () {
@@ -40,8 +43,18 @@ func main() {
4043 cmdLogs ()
4144 case "partners" :
4245 cmdPartners ()
46+ case "cache" :
47+ cmdCache ()
48+ case "report" :
49+ cmdReport ()
50+ case "wallet" :
51+ cmdWallet ()
52+ case "chain" :
53+ cmdChain ()
54+ case "doctor" :
55+ cmdDoctor ()
4356 case "version" :
44- fmt .Println ("DOSRouter v1.0.0 (ported from ClawRouter)" )
57+ fmt .Printf ("DOSRouter %s (Go port of ClawRouter)\n " , proxy . Version )
4558 default :
4659 printUsage ()
4760 os .Exit (1 )
@@ -56,14 +69,28 @@ Usage:
5669 dosrouter classify "prompt" Classify a prompt's complexity
5770 dosrouter models List available models with pricing
5871 dosrouter stats [--days N] Usage statistics (default: 7 days)
72+ dosrouter stats clear Clear all usage logs
5973 dosrouter logs [--days N] Per-request log (default: 1 day)
74+ dosrouter cache Cache statistics
75+ dosrouter report [period] Usage report (daily, weekly, monthly)
6076 dosrouter partners List available partner APIs
77+ dosrouter wallet Show wallet address and balance
78+ dosrouter wallet recover Recover wallet from mnemonic
79+ dosrouter chain [name] Show or switch payment chain
80+ dosrouter doctor AI-powered diagnostics
6181 dosrouter version Show version
6282
6383Serve flags:
6484 --port PORT Listen port (default: 8080)
6585 --upstream URL Upstream API base URL
66- --api-key KEY API key for upstream` )
86+ --api-key KEY API key for upstream
87+
88+ Environment:
89+ DOSROUTER_UPSTREAM Upstream API base URL
90+ DOSROUTER_API_KEY API key for upstream
91+ DOSROUTER_WALLET_KEY Private key (hex) for wallet
92+ DOSROUTER_CHAIN Payment chain (default: doschain)
93+ DOSROUTER_RPC_URL RPC endpoint for payment chain` )
6794}
6895
6996func cmdServe () {
@@ -181,6 +208,16 @@ func cmdModels() {
181208}
182209
183210func cmdStats () {
211+ // Handle "stats clear" subcommand
212+ if len (os .Args ) > 2 && (os .Args [2 ] == "clear" || os .Args [2 ] == "reset" ) {
213+ if err := stats .ClearStats (); err != nil {
214+ fmt .Fprintf (os .Stderr , "Error clearing stats: %v\n " , err )
215+ os .Exit (1 )
216+ }
217+ fmt .Println ("Usage statistics cleared." )
218+ return
219+ }
220+
184221 days := 7
185222 for i := 2 ; i < len (os .Args ); i ++ {
186223 if os .Args [i ] == "--days" && i + 1 < len (os .Args ) {
@@ -223,3 +260,236 @@ func cmdPartners() {
223260 fmt .Println ()
224261 }
225262}
263+
264+ func cmdCache () {
265+ // Query the running proxy's /cache endpoint
266+ port := os .Getenv ("DOSROUTER_PORT" )
267+ if port == "" {
268+ port = "8080"
269+ }
270+ resp , err := httpGet (fmt .Sprintf ("http://localhost:%s/cache" , port ))
271+ if err != nil {
272+ fmt .Fprintf (os .Stderr , "Error: proxy not running on port %s (%v)\n " , port , err )
273+ os .Exit (1 )
274+ }
275+ var cacheStats struct {
276+ Hits int64 `json:"hits"`
277+ Misses int64 `json:"misses"`
278+ Evictions int64 `json:"evictions"`
279+ HitRate float64 `json:"hitRate"`
280+ }
281+ if json .Unmarshal (resp , & cacheStats ) != nil {
282+ fmt .Println (string (resp ))
283+ return
284+ }
285+ fmt .Println ("+----------------------------------+" )
286+ fmt .Println ("| Response Cache |" )
287+ fmt .Println ("+----------------------------------+" )
288+ fmt .Printf ("| Hits: %-21d|\n " , cacheStats .Hits )
289+ fmt .Printf ("| Misses: %-21d|\n " , cacheStats .Misses )
290+ fmt .Printf ("| Evictions: %-21d|\n " , cacheStats .Evictions )
291+ fmt .Printf ("| Hit Rate: %-20.1f%%|\n " , cacheStats .HitRate * 100 )
292+ fmt .Println ("+----------------------------------+" )
293+ }
294+
295+ func cmdReport () {
296+ period := "daily"
297+ jsonOutput := false
298+ for i := 2 ; i < len (os .Args ); i ++ {
299+ switch os .Args [i ] {
300+ case "daily" , "weekly" , "monthly" :
301+ period = os .Args [i ]
302+ case "--json" :
303+ jsonOutput = true
304+ }
305+ }
306+
307+ days := 1
308+ switch period {
309+ case "weekly" :
310+ days = 7
311+ case "monthly" :
312+ days = 30
313+ }
314+
315+ s := stats .GetStats (days )
316+ if jsonOutput {
317+ enc := json .NewEncoder (os .Stdout )
318+ enc .SetIndent ("" , " " )
319+ enc .Encode (s )
320+ return
321+ }
322+ fmt .Printf ("DOSRouter Usage Report (%s)\n " , period )
323+ fmt .Println (strings .Repeat ("=" , 50 ))
324+ fmt .Println (stats .FormatStatsASCII (s ))
325+ }
326+
327+ func cmdWallet () {
328+ if len (os .Args ) > 2 && os .Args [2 ] == "recover" {
329+ cmdWalletRecover ()
330+ return
331+ }
332+
333+ w , err := wallet .LoadOrCreate ()
334+ if err != nil {
335+ fmt .Fprintf (os .Stderr , "Error loading wallet: %v\n " , err )
336+ os .Exit (1 )
337+ }
338+
339+ fmt .Println ("+--------------------------------------------------+" )
340+ fmt .Println ("| DOSRouter Wallet |" )
341+ fmt .Println ("+--------------------------------------------------+" )
342+ fmt .Printf ("| Address: %-38s|\n " , w .Address ())
343+ fmt .Printf ("| Chain: %-38s|\n " , w .Chain ())
344+
345+ balance , err := w .GetBalance ()
346+ if err != nil {
347+ fmt .Printf ("| Balance: %-38s|\n " , "error: " + err .Error ())
348+ } else {
349+ fmt .Printf ("| Balance: $%-37.6f|\n " , balance )
350+ }
351+ fmt .Println ("+--------------------------------------------------+" )
352+
353+ if w .IsNew () {
354+ fmt .Println ("\n New wallet created. Fund it with USDC on" , w .Chain ())
355+ fmt .Println ("Mnemonic (save this!):" , w .Mnemonic ())
356+ }
357+ }
358+
359+ func cmdWalletRecover () {
360+ fmt .Print ("Enter mnemonic phrase: " )
361+ var mnemonic string
362+ fmt .Scanln (& mnemonic )
363+ // Read full line (mnemonic has spaces)
364+ if mnemonic == "" {
365+ fmt .Fprintln (os .Stderr , "Error: mnemonic required" )
366+ os .Exit (1 )
367+ }
368+
369+ w , err := wallet .Recover (mnemonic )
370+ if err != nil {
371+ fmt .Fprintf (os .Stderr , "Error recovering wallet: %v\n " , err )
372+ os .Exit (1 )
373+ }
374+ fmt .Printf ("Wallet recovered: %s\n " , w .Address ())
375+ }
376+
377+ func cmdChain () {
378+ w , err := wallet .LoadOrCreate ()
379+ if err != nil {
380+ fmt .Fprintf (os .Stderr , "Error loading wallet: %v\n " , err )
381+ os .Exit (1 )
382+ }
383+
384+ if len (os .Args ) > 2 {
385+ chain := os .Args [2 ]
386+ if err := w .SetChain (chain ); err != nil {
387+ fmt .Fprintf (os .Stderr , "Error: %v\n " , err )
388+ os .Exit (1 )
389+ }
390+ fmt .Printf ("Payment chain set to: %s\n " , chain )
391+ return
392+ }
393+
394+ fmt .Printf ("Current chain: %s\n " , w .Chain ())
395+ fmt .Println ("\n Available chains:" )
396+ for _ , c := range wallet .SupportedChains () {
397+ marker := " "
398+ if c == w .Chain () {
399+ marker = "* "
400+ }
401+ fmt .Printf (" %s%s\n " , marker , c )
402+ }
403+ }
404+
405+ func cmdDoctor () {
406+ fmt .Println ("DOSRouter Diagnostics" )
407+ fmt .Println (strings .Repeat ("=" , 50 ))
408+
409+ // 1. Version
410+ fmt .Printf ("\n [Version] %s\n " , proxy .Version )
411+
412+ // 2. Wallet
413+ fmt .Print ("\n [Wallet] " )
414+ w , err := wallet .LoadOrCreate ()
415+ if err != nil {
416+ fmt .Printf ("ERROR: %v\n " , err )
417+ } else {
418+ fmt .Printf ("%s on %s\n " , w .Address (), w .Chain ())
419+ balance , err := w .GetBalance ()
420+ if err != nil {
421+ fmt .Printf (" Balance: error (%v)\n " , err )
422+ } else {
423+ fmt .Printf (" Balance: $%.6f\n " , balance )
424+ if balance < 1.0 {
425+ fmt .Println (" WARNING: Low balance (< $1.00)" )
426+ }
427+ }
428+ }
429+
430+ // 3. Proxy
431+ fmt .Print ("\n [Proxy] " )
432+ port := os .Getenv ("DOSROUTER_PORT" )
433+ if port == "" {
434+ port = "8080"
435+ }
436+ healthResp , err := httpGet (fmt .Sprintf ("http://localhost:%s/health?full=true" , port ))
437+ if err != nil {
438+ fmt .Printf ("NOT RUNNING on port %s\n " , port )
439+ } else {
440+ var health map [string ]interface {}
441+ json .Unmarshal (healthResp , & health )
442+ fmt .Printf ("Running on port %s (status: %v)\n " , port , health ["status" ])
443+ if sessions , ok := health ["sessions" ].(float64 ); ok {
444+ fmt .Printf (" Sessions: %.0f\n " , sessions )
445+ }
446+ }
447+
448+ // 4. Upstream
449+ fmt .Print ("\n [Upstream] " )
450+ upstream := os .Getenv ("DOSROUTER_UPSTREAM" )
451+ if upstream == "" {
452+ fmt .Println ("NOT CONFIGURED (set DOSROUTER_UPSTREAM)" )
453+ } else {
454+ _ , err := httpGet (upstream + "/v1/models" )
455+ if err != nil {
456+ fmt .Printf ("UNREACHABLE (%s)\n " , upstream )
457+ } else {
458+ fmt .Printf ("OK (%s)\n " , upstream )
459+ }
460+ }
461+
462+ // 5. Usage (last 24h)
463+ fmt .Println ("\n [Usage - Last 24h]" )
464+ s := stats .GetStats (1 )
465+ fmt .Printf (" Requests: %d\n " , s .TotalRequests )
466+ fmt .Printf (" Cost: $%.4f\n " , s .TotalCost )
467+ if s .TotalSavings > 0 {
468+ fmt .Printf (" Savings: $%.4f (%.1f%%)\n " , s .TotalSavings , s .SavingsPercentage )
469+ }
470+
471+ // 6. Models
472+ fmt .Printf ("\n [Models] %d available\n " , countActiveModels ())
473+
474+ fmt .Println ("\n " + strings .Repeat ("=" , 50 ))
475+ fmt .Println ("Diagnostics complete." )
476+ }
477+
478+ func countActiveModels () int {
479+ count := 0
480+ for _ , m := range models .Models {
481+ if ! m .Deprecated {
482+ count ++
483+ }
484+ }
485+ return count
486+ }
487+
488+ func httpGet (url string ) ([]byte , error ) {
489+ resp , err := http .Get (url )
490+ if err != nil {
491+ return nil , err
492+ }
493+ defer resp .Body .Close ()
494+ return io .ReadAll (resp .Body )
495+ }
0 commit comments