tahamajs/IE / Projects /cli /commands.go
tahamajs's picture
download
raw
52.2 kB
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.