| package main | |
| import ( | |
| "bytes" | |
| _ "embed" | |
| "encoding/base64" | |
| "encoding/json" | |
| "fmt" | |
| "io" | |
| "net/http" | |
| "net/url" | |
| "os" | |
| "os/exec" | |
| "strconv" | |
| "strings" | |
| "text/tabwriter" | |
| "time" | |
| "github.com/fatih/color" | |
| ) | |
| func VersionString() string { | |
| return "ie-cli v2.0.0 (Go Sandbox SDK)" | |
| } | |
| func showHelp() { | |
| banner := ` | |
| __ ____ ____ __ | |
| / |/ (_)___ / __ )____ ____ / /__ | |
| / /|_/ / / __ \/ __ / __ \/ __ \/ //_/ | |
| / / / / / /_/ / /_/ / /_/ / /_/ / ,< | |
| /_/ /_/_/\____/_____/\____/\____/_/|_| | |
| Sandboxed Extension SDK | |
| ` | |
| fmt.Printf("%s%s%s", Purple, banner, Reset) | |
| fmt.Printf("%sVersion:%s %s\n\n", Bold, Reset, VersionString()) | |
| fmt.Printf("%sUSAGE%s\n", Bold, Reset) | |
| fmt.Println(" ie-cli <command> [arguments] [flags]") | |
| fmt.Println(" ie-cli help <command> — detailed help for a specific command") | |
| fmt.Printf("\n%s⚡ CORE COMMANDS%s\n", Bold+Cyan, Reset) | |
| fmt.Println(" init Initialize a new sandboxed tool project") | |
| fmt.Println(" dev Start local preview server (http://localhost:3000)") | |
| fmt.Println(" generate AI-powered tool generator from a text prompt") | |
| fmt.Println(" validate Lint tool.html + validate tool.json schema") | |
| fmt.Println(" build (alias: validate) — full pre-deploy check") | |
| fmt.Printf("\n%s☁ DEPLOYMENT COMMANDS%s\n", Bold+Blue, Reset) | |
| fmt.Println(" deploy Upload and publish your tool to the platform") | |
| fmt.Println(" update Update an already-deployed tool (--id <n>)") | |
| fmt.Println(" delete Permanently delete a deployed tool (--id <n>)") | |
| fmt.Println(" list List all tools you own with status & stats") | |
| fmt.Println(" status Get detailed status for a specific tool (--id <n>)") | |
| fmt.Println(" logs Show deployment & sandbox execution logs") | |
| fmt.Printf("\n%s🔍 DISCOVERY COMMANDS%s\n", Bold+Green, Reset) | |
| fmt.Println(" search Search the tool store by keyword (--query <text>)") | |
| fmt.Println(" trending Show top trending tools on the platform") | |
| fmt.Println(" download Download a public tool's source (--id <n>)") | |
| fmt.Println(" review Submit a rating & review for a tool (--id <n>)") | |
| fmt.Println(" view Inspect a tool's full metadata (--id <n>)") | |
| fmt.Println(" web-search Search the web from your terminal (DuckDuckGo)") | |
| fmt.Printf("\n%s🤖 AI AGENT%s\n", Bold+Purple, Reset) | |
| fmt.Println(" agent Start an autonomous AI agent session (requires OpenAI key)") | |
| fmt.Println(" builder AI-assisted interactive tool builder wizard") | |
| fmt.Printf("\n%s🔐 AUTH & CONFIG%s\n", Bold+Yellow, Reset) | |
| fmt.Println(" login Authenticate with the platform server") | |
| fmt.Println(" logout Clear saved session credentials") | |
| fmt.Println(" whoami Show current logged-in user profile") | |
| fmt.Println(" key set Save your OpenAI API key (stored in ~/.ie-research-cli.json)") | |
| fmt.Println(" key show Show the saved OpenAI API key (masked)") | |
| fmt.Println(" key clear Remove the saved OpenAI API key") | |
| fmt.Println(" version Print CLI version information") | |
| fmt.Printf("\n%sGLOBAL FLAGS%s\n", Bold, Reset) | |
| fmt.Println(" --host <url> Override platform host (default: http://localhost:9092)") | |
| fmt.Println(" --json Output results as JSON (machine-readable)") | |
| fmt.Println(" --help, -h Show this help message") | |
| fmt.Println() | |
| fmt.Printf("%sTip:%s Run 'ie-cli help <command>' for detailed usage of any command.\n\n", Bold+Cyan, Reset) | |
| } | |
| // handleKey manages the OpenAI API key subcommands: set / show / clear | |
| func handleKey() { | |
| sub := "" | |
| if len(parsedArgs.Positional) > 1 { | |
| sub = parsedArgs.Positional[1] | |
| } | |
| switch sub { | |
| case "set": | |
| var key string | |
| // Accept from flag or prompt | |
| key = parsedArgs.Get("--key", "-k") | |
| if key == "" { | |
| key = ask("Enter your OpenAI API key (sk-...): ") | |
| } | |
| key = strings.TrimSpace(key) | |
| if !strings.HasPrefix(key, "sk-") { | |
| fmt.Println(err("Invalid key format — OpenAI keys start with 'sk-'")) | |
| return | |
| } | |
| cfg, loadErr := loadConfig() | |
| if loadErr != nil { | |
| cfg = &Config{Host: DefaultHost} | |
| } | |
| cfg.OpenAIKey = key | |
| if saveErr := saveConfig(cfg); saveErr != nil { | |
| fmt.Println(err("Failed to save key: " + saveErr.Error())) | |
| return | |
| } | |
| fmt.Println(ok("OpenAI API key saved to " + getConfigPath())) | |
| fmt.Println(inf("Key stored with file permission 0600 (owner read-only)")) | |
| case "show": | |
| cfg, loadErr := loadConfig() | |
| if loadErr != nil || cfg.OpenAIKey == "" { | |
| fmt.Println(wrn("No OpenAI API key saved. Run: ie-cli key set")) | |
| return | |
| } | |
| k := cfg.OpenAIKey | |
| masked := k[:7] + strings.Repeat("*", len(k)-11) + k[len(k)-4:] | |
| fmt.Printf("%sOpenAI API Key:%s %s\n", Bold, Reset, masked) | |
| fmt.Println(inf("Config file: " + getConfigPath())) | |
| case "clear": | |
| cfg, loadErr := loadConfig() | |
| if loadErr != nil || cfg.OpenAIKey == "" { | |
| fmt.Println(inf("No key to clear.")) | |
| return | |
| } | |
| cfg.OpenAIKey = "" | |
| saveConfig(cfg) | |
| fmt.Println(ok("OpenAI API key cleared.")) | |
| default: | |
| fmt.Printf("%sUsage:%s ie-cli key <set|show|clear>\n", Bold, Reset) | |
| fmt.Println() | |
| fmt.Println(" ie-cli key set Save your OpenAI API key") | |
| fmt.Println(" ie-cli key show Display the saved key (masked)") | |
| fmt.Println(" ie-cli key clear Remove the saved key") | |
| } | |
| } | |
| // handleVersion prints detailed version info | |
| func handleVersion() { | |
| fmt.Printf("%s%s%s\n", Bold+Purple, VersionString(), Reset) | |
| fmt.Printf(" Platform: IE Research Platform\n") | |
| fmt.Printf(" SDK: Sandboxed Extension SDK v2.0\n") | |
| fmt.Printf(" Go: %s\n", "1.22+") | |
| fmt.Printf(" Config: %s\n", getConfigPath()) | |
| } | |
| func handleInit() { | |
| fmt.Println(hdr("Initializing new custom tool project")) | |
| name := ask("Tool Name (e.g. Citation Counter): ") | |
| if name == "" { | |
| name = "My Custom Tool" | |
| } | |
| description := ask("Short Description: ") | |
| if description == "" { | |
| description = "A sandboxed custom tool." | |
| } | |
| author := ask("Author Name: ") | |
| if author == "" { | |
| author = "Developer" | |
| } | |
| category := ask("Category [ai|statistics|data-viz|productivity|utility]: ") | |
| if category == "" { | |
| category = "utility" | |
| } | |
| version := ask("Version (default 1.0.0): ") | |
| if version == "" { | |
| version = "1.0.0" | |
| } | |
| isPublic := strings.ToLower(ask("Publish to tool store? [Y/n]: ")) != "n" | |
| requiresPro := strings.ToLower(ask("Require Pro subscription? [y/N]: ")) == "y" | |
| meta := ToolMeta{ | |
| Name: name, | |
| Description: description, | |
| Author: author, | |
| Category: category, | |
| Version: version, | |
| Type: "script", | |
| Tags: []string{category, "custom"}, | |
| IsPublic: &isPublic, | |
| RequiresPro: &requiresPro, | |
| } | |
| metaData, _ := json.MarshalIndent(meta, "", " ") | |
| os.WriteFile("tool.json", metaData, 0644) | |
| fmt.Println(ok("Created tool.json")) | |
| sampleHtml := fmt.Sprintf(`<!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>%s</title> | |
| <style> | |
| *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } | |
| body { | |
| font-family: system-ui, -apple-system, sans-serif; | |
| background: linear-gradient(135deg, #0f172a, #1e293b); | |
| color: #f1f5f9; | |
| min-height: 100vh; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| padding: 24px; | |
| } | |
| .card { | |
| background: rgba(255,255,255,0.07); | |
| backdrop-filter: blur(20px); | |
| border: 1px solid rgba(255,255,255,0.15); | |
| border-radius: 20px; | |
| padding: 36px 40px; | |
| max-width: 460px; | |
| width: 100%%%%; | |
| text-align: center; | |
| box-shadow: 0 25px 50px -12px rgba(0,0,0,0.5); | |
| } | |
| .icon { font-size: 48px; margin-bottom: 16px; } | |
| h1 { font-size: 22px; font-weight: 700; margin-bottom: 8px; } | |
| p { font-size: 14px; color: #94a3b8; margin-bottom: 24px; line-height: 1.6; } | |
| button { | |
| padding: 10px 28px; | |
| background: linear-gradient(135deg, #14b8a6, #0ea5e9); | |
| color: white; | |
| border: none; | |
| border-radius: 50px; | |
| font-size: 14px; | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: transform 0.15s, box-shadow 0.15s; | |
| } | |
| button:hover { transform: translateY(-2px); box-shadow: 0 8px 20px rgba(20,184,166,0.35); } | |
| button:active { transform: translateY(0); } | |
| #output { margin-top: 20px; font-size: 13px; color: #4ade80; font-weight: 500; min-height: 20px; } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="card"> | |
| <div class="icon">🔧</div> | |
| <h1>%s</h1> | |
| <p>%s</p> | |
| <button id="btn">Run Action</button> | |
| <div id="output"></div> | |
| </div> | |
| <script> | |
| document.getElementById('btn').addEventListener('click', () => { | |
| document.getElementById('output').textContent = '✓ Action executed successfully inside the sandbox!'; | |
| // Dispatch notification to parent portal frame via secure postMessage broker | |
| if (typeof window.sendNotification === 'function') { | |
| window.sendNotification('Tool Action', '✓ Action executed successfully inside the sandbox!', 'medium'); | |
| } else { | |
| window.parent.postMessage({ | |
| type: 'SEND_NOTIFICATION', | |
| payload: { | |
| title: 'Tool Action', | |
| body: '✓ Action executed successfully inside the sandbox!', | |
| priority: 'medium' | |
| } | |
| }, '*'); | |
| } | |
| }); | |
| </script> | |
| </body> | |
| </html> | |
| `, name, name, description) | |
| os.WriteFile("tool.html", []byte(sampleHtml), 0644) | |
| fmt.Println(ok("Created tool.html")) | |
| fmt.Printf("\n%sProject ready!%s Next steps:\n\n", Bold+Green, Reset) | |
| fmt.Printf(" 1. Edit %stool.html%s with your custom UI and JavaScript logic\n", Cyan, Reset) | |
| fmt.Printf(" 2. Edit %stool.json%s to update metadata (name, description, category…)\n", Cyan, Reset) | |
| fmt.Printf(" 3. Validate before deploying:\n %sie-cli validate%s\n", Dim, Reset) | |
| fmt.Printf(" 4. Log in to the platform:\n %sie-cli login --username <you> --password <pass>%s\n", Dim, Reset) | |
| fmt.Printf(" 5. Deploy:\n %sie-cli deploy --file ./tool.html%s\n\n", Dim, Reset) | |
| } | |
| func handleLogin() { | |
| host := parsedArgs.Get("--host", "") | |
| if host == "" { | |
| host = DefaultHost | |
| } | |
| username := parsedArgs.Get("--username", "-u") | |
| password := parsedArgs.Get("--password", "-p") | |
| if username == "" { | |
| username = ask("Username: ") | |
| } | |
| if password == "" { | |
| password = ask("Password: ") | |
| } | |
| if username == "" || password == "" { | |
| fmt.Println(err("Credentials are required")) | |
| return | |
| } | |
| spin := startSpinner("Logging in to " + host + "…") | |
| payload := map[string]string{ | |
| "username": username, | |
| "password": password, | |
| } | |
| resp, errVal := fetchAPI("POST", host+"/api/auth/login", payload, "") | |
| if errVal != nil { | |
| spin.Fail("Login failed: " + errVal.Error()) | |
| return | |
| } | |
| token, _ := resp["token"].(string) | |
| if token == "" { | |
| spin.Fail("No token returned by the server") | |
| return | |
| } | |
| cfg := &Config{ | |
| Token: token, | |
| Host: host, | |
| Username: username, | |
| LoggedInAt: time.Now().Format(time.RFC3339), | |
| } | |
| if errSave := saveConfig(cfg); errSave != nil { | |
| spin.Fail("Failed to save credentials: " + errSave.Error()) | |
| return | |
| } | |
| spin.Stop("Logged in successfully as " + username) | |
| } | |
| func handleLogout() { | |
| path := getConfigPath() | |
| if _, errStat := os.Stat(path); os.IsNotExist(errStat) { | |
| fmt.Println(wrn("No active session found")) | |
| return | |
| } | |
| spin := startSpinner("Clearing credentials…") | |
| if errDel := os.Remove(path); errDel != nil { | |
| spin.Fail("Failed to delete config file: " + errDel.Error()) | |
| return | |
| } | |
| spin.Stop("Logged out successfully.") | |
| } | |
| func handleWhoami() { | |
| cfg, errVal := loadConfig() | |
| if errVal != nil { | |
| fmt.Println(err("Not logged in. Use 'ie-cli login' first.")) | |
| return | |
| } | |
| fmt.Printf("%sSession Status:%s\n", Bold, Reset) | |
| fmt.Printf(" Host: %s\n", cfg.Host) | |
| fmt.Printf(" Username: %s\n", cfg.Username) | |
| fmt.Printf(" Token: %s…\n", cfg.Token[:8]) | |
| if cfg.LoggedInAt != "" { | |
| fmt.Printf(" Session: Since %s\n", formatDate(cfg.LoggedInAt)) | |
| } | |
| spin := startSpinner("Fetching remote user profile…") | |
| profile, errAPI := fetchAPI("GET", cfg.Host+"/api/users/me", nil, cfg.Token) | |
| if errAPI != nil { | |
| spin.Fail("Failed to fetch profile: " + errAPI.Error()) | |
| return | |
| } | |
| spin.Stop("Profile loaded") | |
| email, _ := profile["email"].(string) | |
| role, _ := profile["role"].(string) | |
| plan, _ := profile["plan"].(string) | |
| fmt.Printf("\n%sMioBook Account Details:%s\n", Bold, Reset) | |
| fmt.Printf(" Email: %s\n", email) | |
| fmt.Printf(" Role: %s\n", role) | |
| fmt.Printf(" Plan: %s\n\n", plan) | |
| } | |
| func handleValidate() { | |
| fmt.Println(hdr("Validating tool sandbox guidelines")) | |
| // 1. Check metadata | |
| spinMeta := startSpinner("Parsing tool.json…") | |
| metaBytes, errRead := os.ReadFile("tool.json") | |
| if errRead != nil { | |
| spinMeta.Fail("Missing tool.json. Run 'ie-cli init' first.") | |
| return | |
| } | |
| var meta ToolMeta | |
| if errJson := json.Unmarshal(metaBytes, &meta); errJson != nil { | |
| spinMeta.Fail("Invalid tool.json format: " + errJson.Error()) | |
| return | |
| } | |
| if meta.Name == "" || meta.Version == "" || meta.Category == "" { | |
| spinMeta.Fail("tool.json is missing required fields (name, version, or category)") | |
| return | |
| } | |
| spinMeta.Stop("tool.json is valid") | |
| // 2. Check HTML code layout | |
| spinHtml := startSpinner("Verifying tool.html sandbox restrictions…") | |
| htmlBytes, errHtml := os.ReadFile("tool.html") | |
| if errHtml != nil { | |
| // Try custom file flag | |
| filePath := parsedArgs.Get("--file", "-f") | |
| if filePath == "" { | |
| spinHtml.Fail("tool.html not found. Run 'ie-cli init' or use '--file <path>'") | |
| return | |
| } | |
| htmlBytes, errHtml = os.ReadFile(filePath) | |
| if errHtml != nil { | |
| spinHtml.Fail("Could not read file: " + filePath) | |
| return | |
| } | |
| } | |
| content := string(htmlBytes) | |
| // Sandbox rules check | |
| var violations []string | |
| if strings.Contains(content, "localStorage") || strings.Contains(content, "sessionStorage") { | |
| violations = append(violations, "Direct Storage access ('localStorage' / 'sessionStorage') is forbidden. Use sandboxed APIs.") | |
| } | |
| if strings.Contains(content, "cookie") { | |
| violations = append(violations, "Cookies access is restricted.") | |
| } | |
| if strings.Contains(content, "XMLHttpRequest") || strings.Contains(content, "fetch(") { | |
| violations = append(violations, "Raw network requests are disabled to enforce data security. Use broker messaging.") | |
| } | |
| if strings.Contains(content, "top.location") || strings.Contains(content, "parent.location") { | |
| violations = append(violations, "Window location redirections are blocked.") | |
| } | |
| if len(violations) > 0 { | |
| spinHtml.Fail("Sandbox validation failed with " + strconv.Itoa(len(violations)) + " warnings:") | |
| for _, v := range violations { | |
| fmt.Printf(" - %s%s%s\n", Yellow, v, Reset) | |
| } | |
| fmt.Printf("\n%sPlease resolve these warnings before deploying.%s\n\n", Yellow, Reset) | |
| return | |
| } | |
| spinHtml.Stop("tool.html is valid! Ready for secure iframe sandboxing.") | |
| } | |
| func handleDeploy() { | |
| cfg, errVal := loadConfig() | |
| if errVal != nil { | |
| fmt.Println(err("Not logged in. Use 'ie-cli login' first.")) | |
| return | |
| } | |
| // 1. Read metadata | |
| metaBytes, errRead := os.ReadFile("tool.json") | |
| if errRead != nil { | |
| fmt.Println(err("tool.json not found. Run 'ie-cli init' first.")) | |
| return | |
| } | |
| var meta ToolMeta | |
| if errJson := json.Unmarshal(metaBytes, &meta); errJson != nil { | |
| fmt.Println(err("Invalid tool.json: " + errJson.Error())) | |
| return | |
| } | |
| // 2. Read code file | |
| filePath := parsedArgs.Get("--file", "-f") | |
| if filePath == "" { | |
| filePath = "tool.html" | |
| } | |
| htmlBytes, errHtml := os.ReadFile(filePath) | |
| if errHtml != nil { | |
| fmt.Println(err("Could not read tool HTML file: " + filePath)) | |
| return | |
| } | |
| meta.Code = string(htmlBytes) | |
| if parsedArgs.Has("--dry-run", "") { | |
| fmt.Printf("%sℹ Dry Run Mode%s\n", Cyan, Reset) | |
| fmt.Printf("Validation passed. Payload size: %d bytes.\n", len(meta.Code)) | |
| fmt.Println("Skipping actual deployment.") | |
| return | |
| } | |
| spin := startSpinner("Deploying extension '" + meta.Name + "' to store…") | |
| resp, errAPI := fetchAPI("POST", cfg.Host+"/api/tools/deploy", meta, cfg.Token) | |
| if errAPI != nil { | |
| spin.Fail("Deployment failed: " + errAPI.Error()) | |
| return | |
| } | |
| toolIdVal := getMapVal(resp, "toolId") | |
| toolId := 0.0 | |
| if toolIdVal != nil { | |
| toolId, _ = toolIdVal.(float64) | |
| } | |
| // Save last deployed tool ID to config | |
| cfg.LastDeployedID = strconv.FormatFloat(toolId, 'f', 0, 64) | |
| saveConfig(cfg) | |
| spin.Stop("Deployed successfully! Tool ID is " + cfg.LastDeployedID) | |
| fmt.Printf("\n%sPublic URL:%s %s/tools/%s\n\n", Bold+Green, Reset, cfg.Host, cfg.LastDeployedID) | |
| } | |
| func handleUpdate() { | |
| cfg, errVal := loadConfig() | |
| if errVal != nil { | |
| fmt.Println(err("Not logged in. Use 'ie-cli login' first.")) | |
| return | |
| } | |
| toolId := parsedArgs.Get("--id", "") | |
| if toolId == "" { | |
| toolId = cfg.LastDeployedID | |
| } | |
| if toolId == "" { | |
| fmt.Println(err("Tool ID is required. Use '--id <number>'")) | |
| return | |
| } | |
| metaBytes, errRead := os.ReadFile("tool.json") | |
| if errRead != nil { | |
| fmt.Println(err("tool.json not found. Run 'ie-cli init' first.")) | |
| return | |
| } | |
| var meta ToolMeta | |
| if errJson := json.Unmarshal(metaBytes, &meta); errJson != nil { | |
| fmt.Println(err("Invalid tool.json: " + errJson.Error())) | |
| return | |
| } | |
| filePath := parsedArgs.Get("--file", "-f") | |
| if filePath == "" { | |
| filePath = "tool.html" | |
| } | |
| htmlBytes, errHtml := os.ReadFile(filePath) | |
| if errHtml != nil { | |
| fmt.Println(err("Could not read tool HTML file: " + filePath)) | |
| return | |
| } | |
| meta.Code = string(htmlBytes) | |
| if parsedArgs.Has("--dry-run", "") { | |
| fmt.Printf("%sℹ Dry Run Mode%s\n", Cyan, Reset) | |
| fmt.Printf("Validation passed. Update payload size: %d bytes.\n", len(meta.Code)) | |
| fmt.Println("Skipping actual update.") | |
| return | |
| } | |
| spin := startSpinner("Updating extension v" + meta.Version + " (ID: " + toolId + ")…") | |
| _, errAPI := fetchAPI("PUT", cfg.Host+"/api/tools/"+toolId, meta, cfg.Token) | |
| if errAPI != nil { | |
| spin.Fail("Update failed: " + errAPI.Error()) | |
| return | |
| } | |
| spin.Stop("Updated successfully!") | |
| } | |
| func handleDelete() { | |
| cfg, errVal := loadConfig() | |
| if errVal != nil { | |
| fmt.Println(err("Not logged in. Use 'ie-cli login' first.")) | |
| return | |
| } | |
| toolId := parsedArgs.Get("--id", "") | |
| if toolId == "" { | |
| toolId = cfg.LastDeployedID | |
| } | |
| if toolId == "" { | |
| fmt.Println(err("Tool ID is required. Use '--id <number>'")) | |
| return | |
| } | |
| confirm := ask("Are you sure you want to delete tool with ID " + toolId + "? [y/N]: ") | |
| if strings.ToLower(confirm) != "y" { | |
| fmt.Println("Aborted.") | |
| return | |
| } | |
| spin := startSpinner("Deleting tool " + toolId + "…") | |
| _, errAPI := fetchAPI("DELETE", cfg.Host+"/api/tools/"+toolId, nil, cfg.Token) | |
| if errAPI != nil { | |
| spin.Fail("Failed to delete tool: " + errAPI.Error()) | |
| return | |
| } | |
| if cfg.LastDeployedID == toolId { | |
| cfg.LastDeployedID = "" | |
| saveConfig(cfg) | |
| } | |
| spin.Stop("Tool deleted successfully.") | |
| } | |
| func handleList() { | |
| cfg, errVal := loadConfig() | |
| if errVal != nil { | |
| fmt.Println(err("Not logged in. Use 'ie-cli login' first.")) | |
| return | |
| } | |
| spin := startSpinner("Fetching tools…") | |
| resp, errAPI := fetchAPI("GET", cfg.Host+"/api/tools/mine", nil, cfg.Token) | |
| if errAPI != nil { | |
| spin.Fail("Failed to fetch tools: " + errAPI.Error()) | |
| return | |
| } | |
| spin.Stop("Loaded owned tools") | |
| var list []interface{} | |
| // Check standard pagination format vs raw list | |
| if items, ok := resp["tools"].([]interface{}); ok { | |
| list = items | |
| } else if items, ok := resp["content"].([]interface{}); ok { | |
| list = items | |
| } else { | |
| // Fallback to iterating map | |
| for _, v := range resp { | |
| if items, ok := v.([]interface{}); ok { | |
| list = items | |
| break | |
| } | |
| } | |
| } | |
| if len(list) == 0 { | |
| fmt.Println("No deployed tools found. Use 'ie-cli deploy' to publish one.") | |
| return | |
| } | |
| fmt.Printf("\n%-6s %-25s %-10s %-8s %-12s %-18s\n", "ID", "Name", "Version", "Status", "Installs", "Created At") | |
| fmt.Println(strings.Repeat("-", 85)) | |
| for _, item := range list { | |
| m, ok := item.(map[string]interface{}) | |
| if !ok { | |
| continue | |
| } | |
| idVal := getMapVal(m, "id") | |
| idStr := "" | |
| if idVal != nil { | |
| idStr = strconv.FormatFloat(idVal.(float64), 'f', 0, 64) | |
| } | |
| name, _ := getMapVal(m, "name").(string) | |
| version, _ := getMapVal(m, "version").(string) | |
| isPubVal := getMapVal(m, "public") | |
| if isPubVal == nil { | |
| isPubVal = getMapVal(m, "isPublic") | |
| } | |
| statusStr := "Private" | |
| if isPub, ok := isPubVal.(bool); ok && isPub { | |
| statusStr = "Public" | |
| } | |
| downloadsVal := getMapVal(m, "downloads") | |
| downloads := 0.0 | |
| if downloadsVal != nil { | |
| downloads, _ = downloadsVal.(float64) | |
| } | |
| createdAt := formatDate(getMapVal(m, "createdAt")) | |
| fmt.Printf("%-6s %-25s %-10s %-8s %-12.0f %-18s\n", | |
| idStr, | |
| truncateOrPad(name, 24), | |
| truncateOrPad(version, 9), | |
| statusStr, | |
| downloads, | |
| createdAt, | |
| ) | |
| } | |
| fmt.Println() | |
| } | |
| func handleStatus() { | |
| cfg, errVal := loadConfig() | |
| if errVal != nil { | |
| fmt.Println(err("Not logged in. Use 'ie-cli login' first.")) | |
| return | |
| } | |
| toolId := parsedArgs.Get("--id", "") | |
| if toolId == "" { | |
| toolId = cfg.LastDeployedID | |
| } | |
| if toolId == "" { | |
| fmt.Println(err("Tool ID is required. Use '--id <number>'")) | |
| return | |
| } | |
| spin := startSpinner("Fetching tool status (ID: " + toolId + ")…") | |
| resp, errAPI := fetchAPI("GET", cfg.Host+"/api/tools/"+toolId+"/state", nil, cfg.Token) | |
| if errAPI != nil { | |
| spin.Fail("Failed to load status: " + errAPI.Error()) | |
| return | |
| } | |
| spin.Stop("Loaded status details") | |
| fmt.Printf("\n%sTool Configuration State:%s\n", Bold, Reset) | |
| installed, _ := resp["installed"].(bool) | |
| liked, _ := resp["liked"].(bool) | |
| fmt.Printf(" Tool ID: %s\n", toolId) | |
| fmt.Printf(" Installed: %t\n", installed) | |
| fmt.Printf(" Liked: %t\n\n", liked) | |
| } | |
| func handleLogs() { | |
| cfg, errVal := loadConfig() | |
| if errVal != nil { | |
| fmt.Println(err("Not logged in. Use 'ie-cli login' first.")) | |
| return | |
| } | |
| toolId := parsedArgs.Get("--id", "") | |
| limitStr := parsedArgs.Get("--limit", "") | |
| if limitStr == "" { | |
| limitStr = "15" | |
| } | |
| url := cfg.Host + "/api/tools/logs?limit=" + limitStr | |
| label := "Fetching system logs…" | |
| if toolId != "" { | |
| url = cfg.Host + "/api/tools/" + toolId + "/logs?limit=" + limitStr | |
| label = "Fetching logs for tool " + toolId + "…" | |
| } | |
| spin := startSpinner(label) | |
| resp, errAPI := fetchAPI("GET", url, nil, cfg.Token) | |
| if errAPI != nil { | |
| spin.Fail("Failed to fetch logs: " + errAPI.Error()) | |
| return | |
| } | |
| spin.Stop("Logs loaded") | |
| logsList, _ := resp["logs"].([]interface{}) | |
| if len(logsList) == 0 { | |
| fmt.Println("No log traces found.") | |
| return | |
| } | |
| fmt.Println(hdr("Event Logs")) | |
| for _, item := range logsList { | |
| m, ok := item.(map[string]interface{}) | |
| if !ok { | |
| continue | |
| } | |
| timestamp, _ := m["timestamp"].(string) | |
| level, _ := m["level"].(string) | |
| message, _ := m["message"].(string) | |
| // Color level | |
| lvlColor := Reset | |
| switch strings.ToUpper(level) { | |
| case "INFO": | |
| lvlColor = Green | |
| case "WARN", "WARNING": | |
| lvlColor = Yellow | |
| case "ERROR", "FATAL": | |
| lvlColor = Red | |
| } | |
| fmt.Printf("[%s] %s%s%s - %s\n", | |
| formatDate(timestamp), | |
| lvlColor, level, Reset, | |
| message, | |
| ) | |
| } | |
| fmt.Println() | |
| } | |
| func showDetailedHelp(cmd string) { | |
| fmt.Printf("\n%s%sMioBook Sandbox CLI Helper – %s%s\n", Bold, Purple, cmd, Reset) | |
| fmt.Println(strings.Repeat("─", 45)) | |
| switch cmd { | |
| case "init": | |
| fmt.Println("Usage:\n ie-cli init") | |
| fmt.Println("\nDescription:") | |
| fmt.Println(" Interactively bootstraps a new custom tool project in the current directory.") | |
| fmt.Println(" Creates 'tool.json' (metadata config) and 'tool.html' (custom iframe template).") | |
| case "login": | |
| fmt.Println("Usage:\n ie-cli login [flags]") | |
| fmt.Println("\nFlags:") | |
| fmt.Println(" -u, --username <string> Specify platform portal username") | |
| fmt.Println(" -p, --password <string> Specify platform portal password") | |
| fmt.Println(" --host <url> Specify custom server host (default: http://localhost:8080)") | |
| case "logout": | |
| fmt.Println("Usage:\n ie-cli logout") | |
| fmt.Println("\nDescription:") | |
| fmt.Println(" Clears persistent session credentials from ~/.ie-research-cli.json file.") | |
| case "whoami": | |
| fmt.Println("Usage:\n ie-cli whoami") | |
| fmt.Println("\nDescription:") | |
| fmt.Println(" Queries and displays your profile email, active role, and subscription tier.") | |
| case "validate": | |
| fmt.Println("Usage:\n ie-cli validate [flags]") | |
| fmt.Println("\nFlags:") | |
| fmt.Println(" -f, --file <path> Path to sandbox HTML script file (default: ./tool.html)") | |
| case "deploy": | |
| fmt.Println("Usage:\n ie-cli deploy [flags]") | |
| fmt.Println("\nFlags:") | |
| fmt.Println(" -f, --file <path> Path to sandbox HTML script file (default: ./tool.html)") | |
| case "update": | |
| fmt.Println("Usage:\n ie-cli update [flags]") | |
| fmt.Println("\nFlags:") | |
| fmt.Println(" --id <number> Specify deployed Tool ID to update (defaults to last deployed ID)") | |
| fmt.Println(" -f, --file <path> Path to the updated HTML script file (default: ./tool.html)") | |
| case "delete": | |
| fmt.Println("Usage:\n ie-cli delete [flags]") | |
| fmt.Println("\nFlags:") | |
| fmt.Println(" --id <number> Specify Tool ID to permanently delete (defaults to last deployed ID)") | |
| case "list": | |
| fmt.Println("Usage:\n ie-cli list") | |
| fmt.Println("\nDescription:") | |
| fmt.Println(" Lists all custom extensions you have deployed, showing their IDs, names, download count, and status.") | |
| case "status": | |
| fmt.Println("Usage:\n ie-cli status [flags]") | |
| fmt.Println("\nFlags:") | |
| fmt.Println(" --id <number> Specify Tool ID (defaults to last deployed ID)") | |
| case "logs": | |
| fmt.Println("Usage:\n ie-cli logs [flags]") | |
| fmt.Println("\nFlags:") | |
| fmt.Println(" --id <number> Specify Tool ID to fetch logs for (if omitted, queries general system logs)") | |
| fmt.Println(" --limit <number> Number of log entries to retrieve (default: 15)") | |
| case "search": | |
| fmt.Println("Usage:\n ie-cli search [flags]") | |
| fmt.Println("\nFlags:") | |
| fmt.Println(" --query, -q <string> Search query") | |
| case "trending": | |
| fmt.Println("Usage:\n ie-cli trending") | |
| fmt.Println("\nDescription:") | |
| fmt.Println(" Lists trending tools on the platform.") | |
| case "download": | |
| fmt.Println("Usage:\n ie-cli download [flags]") | |
| fmt.Println("\nFlags:") | |
| fmt.Println(" --id <number> Specify Tool ID to download") | |
| case "review": | |
| fmt.Println("Usage:\n ie-cli review [flags]") | |
| fmt.Println("\nFlags:") | |
| fmt.Println(" --id <number> Specify Tool ID to review") | |
| fmt.Println(" --rating <1-5> Rating out of 5") | |
| fmt.Println(" --comment <string> Review comment") | |
| case "key": | |
| fmt.Println("Usage:\n ie-cli key <set|show|clear>") | |
| fmt.Println("\nSubcommands:") | |
| fmt.Println(" set Securely save your OpenAI API key to ~/.ie-research-cli.json (chmod 0600)") | |
| fmt.Println(" show Display the stored key with all but last 4 characters masked") | |
| fmt.Println(" clear Remove the stored key") | |
| fmt.Println("\nFlags (for 'set'):") | |
| fmt.Println(" --key, -k <string> Pass key directly (non-interactive)") | |
| fmt.Println("\nExample:") | |
| fmt.Println(" ie-cli key set --key sk-abc...") | |
| fmt.Println(" ie-cli key show") | |
| case "version": | |
| fmt.Println("Usage:\n ie-cli version") | |
| fmt.Println("\nDescription:") | |
| fmt.Println(" Prints CLI version, SDK version, Go runtime version, and config file path.") | |
| case "build": | |
| fmt.Println("Usage:\n ie-cli build") | |
| fmt.Println("\nDescription:") | |
| fmt.Println(" Alias for 'validate'. Runs full pre-deploy check: lints tool.html and validates tool.json schema.") | |
| case "agent": | |
| fmt.Println("Usage:\n ie-cli agent") | |
| fmt.Println("\nDescription:") | |
| fmt.Println(" Starts an interactive autonomous AI agent powered by OpenAI GPT-4o.") | |
| fmt.Println(" The agent has access to tools: run_command, execute_python, read_file,") | |
| fmt.Println(" write_file, search_web, list_directory, deploy_tool, and more.") | |
| fmt.Println("\nRequirements:") | |
| fmt.Println(" OpenAI API key — set with: ie-cli key set") | |
| fmt.Println(" Or set env var: export OPENAI_API_KEY=sk-...") | |
| fmt.Println("\nExample:") | |
| fmt.Println(" ie-cli agent") | |
| fmt.Println(" > Build me a BMI calculator tool") | |
| case "builder": | |
| fmt.Println("Usage:\n ie-cli builder") | |
| fmt.Println("\nDescription:") | |
| fmt.Println(" Interactive AI-assisted tool builder wizard.") | |
| fmt.Println(" Guides you step-by-step through building a sandboxed tool.") | |
| case "dev": | |
| fmt.Println("Usage:\n ie-cli dev [flags]") | |
| fmt.Println("\nFlags:") | |
| fmt.Println(" --port, -p <number> Port to run the local server on (default: 3000)") | |
| fmt.Println("\nDescription:") | |
| fmt.Println(" Starts a local development HTTP server to preview your tool before deploying.") | |
| case "generate": | |
| fmt.Println("Usage:\n ie-cli generate [flags]") | |
| fmt.Println("\nFlags:") | |
| fmt.Println(" --prompt, -p <string> Describe the tool you want to build.") | |
| fmt.Println(" --api-key, -k <string> Your OpenAI API Key (or set OPENAI_API_KEY env var).") | |
| fmt.Println("\nDescription:") | |
| fmt.Println(" An AI Agent that automatically writes 'tool.json' and 'tool.html' based on your prompt.") | |
| default: | |
| fmt.Printf("Unknown command '%s'. Run 'ie-cli help' to see all commands.\n", cmd) | |
| } | |
| fmt.Println() | |
| } | |
| func handleSearch() { | |
| fmt.Println(hdr("Search Tools")) | |
| query := parsedArgs.Flags["--query"] | |
| if query == "" { | |
| query = parsedArgs.Flags["-q"] | |
| } | |
| if query == "" { | |
| fmt.Println(err("Missing --query. Example: ie-cli search --query 'calculator'")) | |
| return | |
| } | |
| cfg, _ := loadConfig() | |
| url := fmt.Sprintf("%s/api/tools?search=%s", cfg.Host, query) | |
| spin := startSpinner("Searching...") | |
| resp, errAPI := fetchAPI("GET", url, nil, "") | |
| if errAPI != nil { | |
| spin.Fail("Failed to search tools: " + errAPI.Error()) | |
| return | |
| } | |
| spin.Stop("Search complete") | |
| printToolsList(resp) | |
| } | |
| func handleTrending() { | |
| fmt.Println(hdr("Trending Tools")) | |
| cfg, _ := loadConfig() | |
| url := fmt.Sprintf("%s/api/tools/trending", cfg.Host) | |
| spin := startSpinner("Fetching trending tools...") | |
| resp, errAPI := fetchAPI("GET", url, nil, "") | |
| if errAPI != nil { | |
| spin.Fail("Failed to fetch trending tools: " + errAPI.Error()) | |
| return | |
| } | |
| spin.Stop("Trending tools loaded") | |
| items, ok := resp["items"].([]interface{}) | |
| if !ok { | |
| fmt.Println("No items found.") | |
| return | |
| } | |
| for _, item := range items { | |
| m, ok := item.(map[string]interface{}) | |
| if !ok { | |
| continue | |
| } | |
| id, _ := m["id"].(float64) | |
| name, _ := m["name"].(string) | |
| author, _ := m["authorUsername"].(string) | |
| fmt.Printf(" %sID %v%s | %s | %s@%s%s\n", Dim, int(id), Reset, name, Cyan, author, Reset) | |
| } | |
| fmt.Println() | |
| } | |
| func handleDownload() { | |
| fmt.Println(hdr("Download Tool")) | |
| id := parsedArgs.Flags["--id"] | |
| if id == "" { | |
| fmt.Println(err("Missing --id. Example: ie-cli download --id 42")) | |
| return | |
| } | |
| cfg, _ := loadConfig() | |
| url := fmt.Sprintf("%s/api/tools/%s/download", cfg.Host, id) | |
| spin := startSpinner("Downloading...") | |
| resp, errAPI := fetchAPI("GET", url, nil, "") | |
| if errAPI != nil { | |
| spin.Fail("Failed to download tool: " + errAPI.Error()) | |
| return | |
| } | |
| spin.Stop("Download complete") | |
| html, _ := resp["code"].(string) | |
| if html == "" { | |
| html = fmt.Sprintf("<!-- Downloaded Tool %s -->\n", id) | |
| } | |
| filename := fmt.Sprintf("tool_%s.html", id) | |
| os.WriteFile(filename, []byte(html), 0644) | |
| fmt.Println(ok(fmt.Sprintf("Saved tool to %s", filename))) | |
| } | |
| func handleReview() { | |
| fmt.Println(hdr("Submit Review")) | |
| id := parsedArgs.Flags["--id"] | |
| if id == "" { | |
| fmt.Println(err("Missing --id. Example: ie-cli review --id 42 --rating 5 --comment 'Great!'")) | |
| return | |
| } | |
| rating := parsedArgs.Flags["--rating"] | |
| comment := parsedArgs.Flags["--comment"] | |
| if rating == "" || comment == "" { | |
| fmt.Println(err("Missing --rating or --comment.")) | |
| return | |
| } | |
| cfg, _ := loadConfig() | |
| url := fmt.Sprintf("%s/api/tools/%s/reviews", cfg.Host, id) | |
| payload := map[string]interface{}{ | |
| "rating": rating, | |
| "comment": comment, | |
| } | |
| spin := startSpinner("Submitting review...") | |
| _, errAPI := fetchAPI("POST", url, payload, cfg.Token) | |
| if errAPI != nil { | |
| spin.Fail("Failed to submit review: " + errAPI.Error()) | |
| return | |
| } | |
| spin.Stop("Review submitted successfully") | |
| } | |
| func handleWebSearch() { | |
| fmt.Println(hdr("Web Search")) | |
| query := parsedArgs.Flags["--query"] | |
| if query == "" { | |
| query = parsedArgs.Flags["-q"] | |
| } | |
| if query == "" { | |
| fmt.Println(err("Missing --query. Example: ie-cli web-search --query 'machine learning'")) | |
| return | |
| } | |
| spin := startSpinner("Searching the web...") | |
| // Query DuckDuckGo HTML lite | |
| searchUrl := fmt.Sprintf("https://html.duckduckgo.com/html/?q=%s", strings.ReplaceAll(query, " ", "+")) | |
| req, _ := http.NewRequest("GET", searchUrl, nil) | |
| req.Header.Set("User-Agent", "Mozilla/5.0 (ie-cli search bot)") | |
| client := &http.Client{Timeout: 10 * time.Second} | |
| resp, errReq := client.Do(req) | |
| if errReq != nil { | |
| spin.Fail("Failed to search web: " + errReq.Error()) | |
| return | |
| } | |
| defer resp.Body.Close() | |
| bodyBytes, _ := io.ReadAll(resp.Body) | |
| bodyStr := string(bodyBytes) | |
| spin.Stop("Search complete") | |
| // A very rudimentary extraction of DuckDuckGo Lite results | |
| fmt.Printf("%sResults for: %s%s\n\n", Cyan, query, Reset) | |
| count := 0 | |
| parts := strings.Split(bodyStr, "result__url\" href=\"") | |
| for i := 1; i < len(parts) && count < 5; i++ { | |
| urlEnd := strings.Index(parts[i], "\"") | |
| if urlEnd == -1 { | |
| continue | |
| } | |
| link := parts[i][:urlEnd] | |
| // Unescape duckduckgo redirect if present | |
| if strings.HasPrefix(link, "//duckduckgo.com/l/?uddg=") { | |
| link = strings.TrimPrefix(link, "//duckduckgo.com/l/?uddg=") | |
| link = strings.Split(link, "&")[0] | |
| } | |
| fmt.Printf(" %d. %s%s%s\n", count+1, Green, link, Reset) | |
| count++ | |
| } | |
| if count == 0 { | |
| fmt.Println("No results found or could not parse response.") | |
| } | |
| fmt.Println() | |
| } | |
| func handleBuilder() { | |
| fmt.Println(hdr("Visual Tool Builder")) | |
| cfg, _ := loadConfig() | |
| // Default frontend URL based on the API host, usually 5173 | |
| frontHost := strings.Replace(cfg.Host, "8080", "5173", 1) | |
| url := frontHost + "/tools/builder" | |
| fmt.Printf("%sOpening Visual Builder in your browser...%s\n", Cyan, Reset) | |
| fmt.Printf("URL: %s\n", url) | |
| errBrowser := openBrowser(url) | |
| if errBrowser != nil { | |
| fmt.Println(err("Failed to open browser: " + errBrowser.Error())) | |
| } | |
| } | |
| func handleView() { | |
| fmt.Println(hdr("View Tool")) | |
| cfg, errVal := loadConfig() | |
| if errVal != nil { | |
| fmt.Println(err("Not logged in. Use 'ie-cli login' first.")) | |
| return | |
| } | |
| toolId := parsedArgs.Get("--id", "") | |
| if toolId == "" { | |
| toolId = cfg.LastDeployedID | |
| } | |
| if toolId == "" { | |
| fmt.Println(err("Tool ID is required. Use '--id <number>'")) | |
| return | |
| } | |
| if parsedArgs.Has("--web", "-w") { | |
| frontHost := strings.Replace(cfg.Host, "8080", "5173", 1) | |
| url := frontHost + "/tools/" + toolId | |
| fmt.Printf("%sOpening tool in your browser...%s\n", Cyan, Reset) | |
| fmt.Printf("URL: %s\n", url) | |
| errBrowser := openBrowser(url) | |
| if errBrowser != nil { | |
| fmt.Println(err("Failed to open browser: " + errBrowser.Error())) | |
| } | |
| } else { | |
| // Just fetch and print status | |
| url := fmt.Sprintf("%s/api/tools/%s", cfg.Host, toolId) | |
| resp, errAPI := fetchAPI("GET", url, nil, "") | |
| if errAPI != nil { | |
| fmt.Println(err("Failed to fetch tool: " + errAPI.Error())) | |
| return | |
| } | |
| name, _ := resp["name"].(string) | |
| desc, _ := resp["description"].(string) | |
| author, _ := resp["authorUsername"].(string) | |
| fmt.Printf("Name: %s%s%s\n", Bold, name, Reset) | |
| fmt.Printf("Author: %s@%s%s\n", Cyan, author, Reset) | |
| fmt.Printf("Desc: %s\n\n", desc) | |
| fmt.Printf("Use '--web' to open this tool in the browser.\n") | |
| } | |
| } | |
| var ( | |
| devNotifier = make(chan bool, 10) | |
| lastFileMod time.Time | |
| ) | |
| func watchFiles() { | |
| for { | |
| time.Sleep(500 * time.Millisecond) | |
| info, err := os.Stat("tool.html") | |
| if err == nil { | |
| modTime := info.ModTime() | |
| if !lastFileMod.IsZero() && modTime.After(lastFileMod) { | |
| select { | |
| case devNotifier <- true: | |
| default: | |
| } | |
| } | |
| lastFileMod = modTime | |
| } | |
| infoJson, errJson := os.Stat("tool.json") | |
| if errJson == nil { | |
| modTimeJson := infoJson.ModTime() | |
| if !lastFileMod.IsZero() && modTimeJson.After(lastFileMod) { | |
| select { | |
| case devNotifier <- true: | |
| default: | |
| } | |
| } | |
| } | |
| } | |
| } | |
| //go:embed templates/simulator.html | |
| var simulatorHtmlBytes []byte | |
| func handleDev() { | |
| fmt.Println(hdr("Local Development Server")) | |
| port := parsedArgs.Get("--port", "-p") | |
| if port == "" { | |
| port = "3000" | |
| } | |
| fmt.Printf("%sStarting local preview server on port %s...%s\n", Cyan, port, Reset) | |
| fmt.Println("Press Ctrl+C to stop.") | |
| // Set initial mod time | |
| if info, err := os.Stat("tool.html"); err == nil { | |
| lastFileMod = info.ModTime() | |
| } | |
| go watchFiles() | |
| // 1. Live Reload Event Stream | |
| http.HandleFunc("/live-reload", func(w http.ResponseWriter, r *http.Request) { | |
| w.Header().Set("Content-Type", "text/event-stream") | |
| w.Header().Set("Cache-Control", "no-cache") | |
| w.Header().Set("Connection", "keep-alive") | |
| w.Header().Set("Access-Control-Allow-Origin", "*") | |
| flusher, ok := w.(http.Flusher) | |
| if !ok { | |
| http.Error(w, "Streaming unsupported!", http.StatusInternalServerError) | |
| return | |
| } | |
| fmt.Fprintf(w, "data: connected\n\n") | |
| flusher.Flush() | |
| select { | |
| case <-devNotifier: | |
| fmt.Fprintf(w, "data: reload\n\n") | |
| flusher.Flush() | |
| case <-r.Context().Done(): | |
| case <-time.After(30 * time.Second): | |
| fmt.Fprintf(w, "data: ping\n\n") | |
| flusher.Flush() | |
| } | |
| }) | |
| // 2. Intercept tool.html to inject hot reload & console redirect script | |
| http.HandleFunc("/tool.html", func(w http.ResponseWriter, r *http.Request) { | |
| data, err := os.ReadFile("tool.html") | |
| if err != nil { | |
| http.NotFound(w, r) | |
| return | |
| } | |
| script := ` | |
| <script> | |
| (function() { | |
| console.log("[Dev Server] Live reload & console logging redirection active."); | |
| // Redirect console logs to parent simulator window | |
| const _log = console.log; | |
| const _warn = console.warn; | |
| const _error = console.error; | |
| console.log = function(...args) { | |
| _log.apply(console, args); | |
| window.parent.postMessage({ type: 'console', level: 'log', args: args }, '*'); | |
| }; | |
| console.warn = function(...args) { | |
| _warn.apply(console, args); | |
| window.parent.postMessage({ type: 'console', level: 'warn', args: args }, '*'); | |
| }; | |
| console.error = function(...args) { | |
| _error.apply(console, args); | |
| window.parent.postMessage({ type: 'console', level: 'error', args: args }, '*'); | |
| }; | |
| const eventSource = new EventSource('/live-reload'); | |
| eventSource.onmessage = function(e) { | |
| if (e.data === 'reload') { | |
| console.log('[Dev Server] File change detected, reloading...'); | |
| window.location.reload(); | |
| } | |
| }; | |
| eventSource.onerror = function() { | |
| setTimeout(() => { | |
| window.location.reload(); | |
| }, 1000); | |
| }; | |
| })(); | |
| </script> | |
| ` | |
| htmlStr := string(data) | |
| if idx := strings.LastIndex(htmlStr, "</body>"); idx != -1 { | |
| htmlStr = htmlStr[:idx] + script + htmlStr[idx:] | |
| } else { | |
| htmlStr = htmlStr + script | |
| } | |
| w.Header().Set("Content-Type", "text/html; charset=utf-8") | |
| w.Write([]byte(htmlStr)) | |
| }) | |
| // 3. CORS Proxy Endpoint | |
| http.HandleFunc("/proxy", func(w http.ResponseWriter, r *http.Request) { | |
| targetUrl := r.URL.Query().Get("url") | |
| if targetUrl == "" { | |
| http.Error(w, "Missing 'url' query parameter.", http.StatusBadRequest) | |
| return | |
| } | |
| req, err := http.NewRequest(r.Method, targetUrl, r.Body) | |
| if err != nil { | |
| http.Error(w, err.Error(), http.StatusInternalServerError) | |
| return | |
| } | |
| // Copy request headers | |
| for k, vv := range r.Header { | |
| if k != "Host" && k != "Origin" && k != "Referer" { | |
| for _, v := range vv { | |
| req.Header.Add(k, v) | |
| } | |
| } | |
| } | |
| client := &http.Client{Timeout: 15 * time.Second} | |
| resp, err := client.Do(req) | |
| if err != nil { | |
| http.Error(w, err.Error(), http.StatusBadGateway) | |
| return | |
| } | |
| defer resp.Body.Close() | |
| // Copy response headers | |
| for k, vv := range resp.Header { | |
| for _, v := range vv { | |
| w.Header().Add(k, v) | |
| } | |
| } | |
| w.Header().Set("Access-Control-Allow-Origin", "*") | |
| w.WriteHeader(resp.StatusCode) | |
| io.Copy(w, resp.Body) | |
| }) | |
| // 4. API Deploy Trigger Endpoint | |
| http.HandleFunc("/api/deploy", func(w http.ResponseWriter, r *http.Request) { | |
| w.Header().Set("Content-Type", "application/json") | |
| w.Header().Set("Access-Control-Allow-Origin", "*") | |
| if r.Method != "POST" { | |
| http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) | |
| return | |
| } | |
| // Run "ie-cli deploy" programmatically | |
| cmd := exec.Command("go", "run", "cli/main.go", "cli/commands.go", "cli/config.go", "cli/utils.go", "cli/agent.go", "deploy") | |
| out, err := cmd.CombinedOutput() | |
| status := "success" | |
| if err != nil { | |
| status = "error" | |
| } | |
| response := map[string]string{ | |
| "status": status, | |
| "output": string(out), | |
| } | |
| json.NewEncoder(w).Encode(response) | |
| }) | |
| // 4b. API Terminal Command Runner Endpoint | |
| http.HandleFunc("/api/terminal", func(w http.ResponseWriter, r *http.Request) { | |
| w.Header().Set("Content-Type", "application/json") | |
| w.Header().Set("Access-Control-Allow-Origin", "*") | |
| if r.Method != "POST" { | |
| http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) | |
| return | |
| } | |
| var reqBody struct { | |
| Command string `json:"command"` | |
| } | |
| if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { | |
| http.Error(w, err.Error(), http.StatusBadRequest) | |
| return | |
| } | |
| if reqBody.Command == "" { | |
| json.NewEncoder(w).Encode(map[string]string{ | |
| "status": "error", | |
| "output": "Command cannot be empty", | |
| }) | |
| return | |
| } | |
| parts := strings.Fields(reqBody.Command) | |
| if len(parts) == 0 { | |
| json.NewEncoder(w).Encode(map[string]string{ | |
| "status": "error", | |
| "output": "Invalid command", | |
| }) | |
| return | |
| } | |
| cmdName := parts[0] | |
| cmdArgs := parts[1:] | |
| cmd := exec.Command(cmdName, cmdArgs...) | |
| out, err := cmd.CombinedOutput() | |
| status := "success" | |
| if err != nil { | |
| status = "error" | |
| } | |
| response := map[string]string{ | |
| "status": status, | |
| "output": string(out), | |
| } | |
| json.NewEncoder(w).Encode(response) | |
| }) | |
| // 5. Serve the Sandbox Simulator on "/" | |
| http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { | |
| if r.URL.Path == "/" || r.URL.Path == "/index.html" { | |
| w.Header().Set("Content-Type", "text/html; charset=utf-8") | |
| w.Write(simulatorHtmlBytes) | |
| return | |
| } | |
| // Fallback to static serving | |
| http.FileServer(http.Dir(".")).ServeHTTP(w, r) | |
| }) | |
| url := "http://localhost:" + port | |
| // Try to open the browser after a short delay | |
| go func() { | |
| time.Sleep(500 * time.Millisecond) | |
| openBrowser(url) | |
| }() | |
| errServe := http.ListenAndServe(":"+port, nil) | |
| if errServe != nil { | |
| fmt.Println(err("Server failed: " + errServe.Error())) | |
| } | |
| } | |
| func handleGenerate() { | |
| fmt.Println(hdr("AI Tool Generator Agent")) | |
| prompt := parsedArgs.Get("--prompt", "-p") | |
| if prompt == "" { | |
| fmt.Println(err("Missing --prompt. Example: ie-cli generate --prompt 'a calculator tool'")) | |
| return | |
| } | |
| apiKey := parsedArgs.Get("--api-key", "-k") | |
| if apiKey == "" { | |
| apiKey = os.Getenv("OPENAI_API_KEY") | |
| } | |
| if apiKey == "" { | |
| fmt.Println(err("Missing OpenAI API Key. Pass --api-key or set OPENAI_API_KEY env var.")) | |
| return | |
| } | |
| spin := startSpinner("Agent is thinking and writing code...") | |
| systemPrompt := `You are an expert developer building a sandboxed HTML tool. | |
| You must return a raw JSON object with exactly two keys: | |
| 1. "tool_json": an object with keys "name", "description", "version" (e.g. "1.0.0"), "category". | |
| 2. "tool_html": a string containing the complete, single-file HTML code. The HTML must be beautifully styled (you can use Tailwind CDN: <script src="https://cdn.tailwindcss.com"></script>) and should include JS logic to fulfill the user's prompt. | |
| To communicate with the host, the JS can call: window.parent.postMessage({type:"SEND_NOTIFICATION", payload:{title, body, priority:"medium"}}, "*") | |
| IMPORTANT: Return ONLY the raw JSON object. Do not include markdown formatting like json blocks.` | |
| payload := map[string]interface{}{ | |
| "model": "gpt-4o-mini", // or gpt-3.5-turbo | |
| "messages": []map[string]string{ | |
| {"role": "system", "content": systemPrompt}, | |
| {"role": "user", "content": prompt}, | |
| }, | |
| "temperature": 0.7, | |
| } | |
| payloadBytes, _ := json.Marshal(payload) | |
| req, _ := http.NewRequest("POST", "https://api.openai.com/v1/chat/completions", bytes.NewBuffer(payloadBytes)) | |
| req.Header.Set("Authorization", "Bearer "+apiKey) | |
| req.Header.Set("Content-Type", "application/json") | |
| client := &http.Client{Timeout: 60 * time.Second} | |
| resp, errReq := client.Do(req) | |
| if errReq != nil { | |
| spin.Fail("Failed to contact OpenAI: " + errReq.Error()) | |
| return | |
| } | |
| defer resp.Body.Close() | |
| bodyBytes, _ := io.ReadAll(resp.Body) | |
| if resp.StatusCode != 200 { | |
| spin.Fail(fmt.Sprintf("OpenAI API Error (%d): %s", resp.StatusCode, string(bodyBytes))) | |
| return | |
| } | |
| var openAIResp map[string]interface{} | |
| json.Unmarshal(bodyBytes, &openAIResp) | |
| choices, ok := openAIResp["choices"].([]interface{}) | |
| if !ok || len(choices) == 0 { | |
| spin.Fail("No response from OpenAI.") | |
| return | |
| } | |
| choice := choices[0].(map[string]interface{}) | |
| msg := choice["message"].(map[string]interface{}) | |
| content := msg["content"].(string) | |
| // Clean up markdown block if the model ignored instructions | |
| content = strings.TrimPrefix(content, "```json") | |
| content = strings.TrimPrefix(content, "```") | |
| content = strings.TrimSuffix(content, "```") | |
| content = strings.TrimSpace(content) | |
| var generatedData map[string]interface{} | |
| errParse := json.Unmarshal([]byte(content), &generatedData) | |
| if errParse != nil { | |
| spin.Fail("Failed to parse AI output: " + errParse.Error()) | |
| fmt.Println("Raw output:", content) | |
| return | |
| } | |
| toolJsonMap, ok1 := generatedData["tool_json"].(map[string]interface{}) | |
| toolHtml, ok2 := generatedData["tool_html"].(string) | |
| if !ok1 || !ok2 { | |
| spin.Fail("AI response missing tool_json or tool_html fields.") | |
| return | |
| } | |
| toolJsonBytes, _ := json.MarshalIndent(toolJsonMap, "", " ") | |
| os.WriteFile("tool.json", toolJsonBytes, 0644) | |
| os.WriteFile("tool.html", []byte(toolHtml), 0644) | |
| spin.Stop("Agent successfully generated the tool!") | |
| fmt.Printf("\n%sFiles created:%s\n - tool.json\n - tool.html\n", Bold+Green, Reset) | |
| fmt.Println("Run 'ie-cli dev' to preview, or 'ie-cli deploy' to publish!") | |
| } | |
| func printToolsList(resp map[string]interface{}) { | |
| tools, ok := resp["tools"].([]interface{}) | |
| if !ok || len(tools) == 0 { | |
| fmt.Println("No tools found.") | |
| return | |
| } | |
| w := tabwriter.NewWriter(os.Stdout, 0, 0, 4, ' ', tabwriter.TabIndent) | |
| fmt.Fprintf(w, "%s%sID\tNAME\tCATEGORY\tSTATUS%s\n", Cyan, Bold, Reset) | |
| fmt.Fprintf(w, "%s%s--\t----\t--------\t------%s\n", Dim, Dim, Reset) | |
| for _, item := range tools { | |
| m, ok := item.(map[string]interface{}) | |
| if !ok { | |
| continue | |
| } | |
| id, _ := m["id"].(float64) | |
| name, _ := m["name"].(string) | |
| cat, _ := m["category"].(string) | |
| status, _ := m["status"].(string) | |
| statusCell := status | |
| if status == "ACTIVE" { | |
| statusCell = color.GreenString(status) | |
| } else { | |
| statusCell = color.YellowString(status) | |
| } | |
| fmt.Fprintf(w, "%v\t%s\t%s\t%s\n", int(id), name, cat, statusCell) | |
| } | |
| w.Flush() | |
| fmt.Println() | |
| } | |
| func handleGithub() { | |
| if len(parsedArgs.Positional) < 2 { | |
| fmt.Println(err("Usage: ie-cli github search <query> OR ie-cli github view <owner/repo>")) | |
| return | |
| } | |
| sub := parsedArgs.Positional[1] | |
| switch sub { | |
| case "search": | |
| if len(parsedArgs.Positional) < 3 { | |
| fmt.Println(err("Missing search query. Example: ie-cli github search react")) | |
| return | |
| } | |
| query := strings.Join(parsedArgs.Positional[2:], " ") | |
| searchGithubRepos(query) | |
| case "view": | |
| if len(parsedArgs.Positional) < 3 { | |
| fmt.Println(err("Missing owner/repo. Example: ie-cli github view facebook/react")) | |
| return | |
| } | |
| repo := parsedArgs.Positional[2] | |
| viewGithubRepo(repo) | |
| default: | |
| fmt.Printf("%sUnknown github subcommand '%s'.%s Supported: search, view\n", Red, sub, Reset) | |
| } | |
| } | |
| func searchGithubRepos(query string) { | |
| fmt.Println(hdr("GitHub Repository Search")) | |
| urlStr := fmt.Sprintf("https://api.github.com/search/repositories?q=%s&per_page=5", url.QueryEscape(query)) | |
| req, _ := http.NewRequest("GET", urlStr, nil) | |
| req.Header.Set("User-Agent", "ie-cli") | |
| client := &http.Client{Timeout: 10 * time.Second} | |
| resp, errReq := client.Do(req) | |
| if errReq != nil { | |
| fmt.Println(err(fmt.Sprintf("Failed to query GitHub API: %v", errReq))) | |
| return | |
| } | |
| defer resp.Body.Close() | |
| if resp.StatusCode != 200 { | |
| fmt.Println(err(fmt.Sprintf("GitHub API returned status: %s", resp.Status))) | |
| return | |
| } | |
| var result struct { | |
| Items []struct { | |
| FullName string `json:"full_name"` | |
| Description string `json:"description"` | |
| StargazersCount int `json:"stargazers_count"` | |
| HtmlUrl string `json:"html_url"` | |
| Language string `json:"language"` | |
| } `json:"items"` | |
| } | |
| if errDec := json.NewDecoder(resp.Body).Decode(&result); errDec != nil { | |
| fmt.Println(err(fmt.Sprintf("Failed to decode response: %v", errDec))) | |
| return | |
| } | |
| if len(result.Items) == 0 { | |
| fmt.Println("No repositories found.") | |
| return | |
| } | |
| w := tabwriter.NewWriter(os.Stdout, 0, 0, 4, ' ', tabwriter.TabIndent) | |
| fmt.Fprintf(w, "%s%sREPOSITORY\tSTARS\tLANGUAGE\tDESCRIPTION%s\n", Green, Bold, Reset) | |
| fmt.Fprintf(w, "%s%s----------\t-----\t--------\t-----------%s\n", Dim, Dim, Reset) | |
| for _, item := range result.Items { | |
| desc := item.Description | |
| if len(desc) > 80 { | |
| desc = desc[:77] + "..." | |
| } | |
| fmt.Fprintf(w, "%s\t⭐ %d\t%s\t%s\n", | |
| color.CyanString(item.FullName), | |
| item.StargazersCount, | |
| color.MagentaString(item.Language), | |
| desc, | |
| ) | |
| } | |
| w.Flush() | |
| fmt.Println() | |
| } | |
| func viewGithubRepo(repo string) { | |
| fmt.Println(hdr("GitHub Repository View: " + repo)) | |
| repoUrl := fmt.Sprintf("https://api.github.com/repos/%s", repo) | |
| req, _ := http.NewRequest("GET", repoUrl, nil) | |
| req.Header.Set("User-Agent", "ie-cli") | |
| client := &http.Client{Timeout: 10 * time.Second} | |
| resp, errReq := client.Do(req) | |
| if errReq != nil { | |
| fmt.Println(err(fmt.Sprintf("Failed to fetch repository details: %v", errReq))) | |
| return | |
| } | |
| defer resp.Body.Close() | |
| if resp.StatusCode != 200 { | |
| fmt.Println(err(fmt.Sprintf("Repository not found or API error: %s", resp.Status))) | |
| return | |
| } | |
| var details struct { | |
| FullName string `json:"full_name"` | |
| Description string `json:"description"` | |
| StargazersCount int `json:"stargazers_count"` | |
| ForksCount int `json:"forks_count"` | |
| OpenIssuesCount int `json:"open_issues_count"` | |
| Language string `json:"language"` | |
| } | |
| if errDec := json.NewDecoder(resp.Body).Decode(&details); errDec != nil { | |
| fmt.Println(err(fmt.Sprintf("Failed to decode details: %v", errDec))) | |
| return | |
| } | |
| fmt.Printf("%sDescription:%s %s\n", Bold, Reset, details.Description) | |
| fmt.Printf("%sStats:%s ⭐ %d | 🍴 %d | 🐛 %d | 💻 %s\n\n", Bold, Reset, details.StargazersCount, details.ForksCount, details.OpenIssuesCount, details.Language) | |
| readmeUrl := fmt.Sprintf("https://api.github.com/repos/%s/readme", repo) | |
| reqReadme, _ := http.NewRequest("GET", readmeUrl, nil) | |
| reqReadme.Header.Set("User-Agent", "ie-cli") | |
| respReadme, errReadme := client.Do(reqReadme) | |
| if errReadme != nil { | |
| fmt.Println(wrn("Failed to fetch README content.")) | |
| return | |
| } | |
| defer respReadme.Body.Close() | |
| if respReadme.StatusCode != 200 { | |
| fmt.Println(wrn("README not found in repository.")) | |
| return | |
| } | |
| var readme struct { | |
| Content string `json:"content"` | |
| Encoding string `json:"encoding"` | |
| } | |
| if err := json.NewDecoder(respReadme.Body).Decode(&readme); err != nil { | |
| fmt.Println(wrn("Failed to decode README metadata.")) | |
| return | |
| } | |
| if readme.Encoding == "base64" { | |
| cleanB64 := strings.ReplaceAll(readme.Content, "\n", "") | |
| cleanB64 = strings.ReplaceAll(cleanB64, "\r", "") | |
| decoded, errDec := base64.StdEncoding.DecodeString(cleanB64) | |
| if errDec == nil { | |
| fmt.Printf("%s=== README.md ===%s\n", Purple+Bold, Reset) | |
| readmeStr := string(decoded) | |
| if len(readmeStr) > 4000 { | |
| fmt.Println(readmeStr[:4000]) | |
| fmt.Printf("\n%s... [README truncated to 4000 characters. View online for full documentation.] ...%s\n", Dim, Reset) | |
| } else { | |
| fmt.Println(readmeStr) | |
| } | |
| } else { | |
| fmt.Println(wrn("Failed to decode README base64 content: " + errDec.Error())) | |
| } | |
| } else { | |
| fmt.Println(readme.Content) | |
| } | |
| } | |
Xet Storage Details
- Size:
- 52.2 kB
- Xet hash:
- 0a09f8ad9c3c607d1516a285842a47014b3f363c32f90ec9534ba39fcd74b95a
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.