--[[ AxomAI: Roblox Studio plugin Links Studio to AxomAI, then runs the commands the AI sends (read/write scripts, create instances, set properties, read output). Install: 1. Download AxomAI.server.luau from the AxomAI app (your site address is already filled in). 2. In Studio, insert a Script anywhere and paste this file in. 3. Right-click the Script > "Save as Local Plugin...", then delete the Script from the place. 4. Open "AxomAI" in the Plugins tab. Allow the HTTP permission prompt the first time. ]] local HttpService = game:GetService("HttpService") local LogService = game:GetService("LogService") local ChangeHistoryService = game:GetService("ChangeHistoryService") local ScriptEditorService = game:GetService("ScriptEditorService") local StudioService = game:GetService("StudioService") local TweenService = game:GetService("TweenService") local Players = game:GetService("Players") -- The AxomAI server. The copy you download from the app already has the right address. local BASE_URL = "https://axomai-production.up.railway.app" local VERSION = "2.0" local TOKEN_SETTING = "axomai_plugin_token" local MAX_TREE_NODES = 300 local MAX_FEED = 30 local REQUEST_TIMEOUT = 30 -- seconds to wait for the server before giving up on one request local COMMAND_TIMEOUT = 20 -- seconds one command may run before it is abandoned -- Upload the white AxomAI logo as an Image in Creator Hub and paste "rbxassetid://123456" here. local LOGO_ASSET_ID = "" local token: string? = plugin:GetSetting(TOKEN_SETTING) local loopId = 0 -- bumped whenever a new poll loop starts or the plugin is unlinked; old loops exit local me: any = nil -- account info from /api/plugin/me local busyRemote = false ------------------------------------------------------------------------ -- Palette (same as the website) ------------------------------------------------------------------------ local C = { night = Color3.fromHex("#06051A"), night2 = Color3.fromHex("#0D0B2B"), ink = Color3.fromHex("#F4F2FF"), soft = Color3.fromHex("#C9C4EE"), mute = Color3.fromHex("#8F89BD"), sky = Color3.fromHex("#7CC8FF"), lav = Color3.fromHex("#A993FF"), pink = Color3.fromHex("#F3A8EC"), mint = Color3.fromHex("#7FF0C8"), warn = Color3.fromHex("#FFB4A8"), dark = Color3.fromHex("#0B0A24"), white = Color3.new(1, 1, 1), } local BRAND = ColorSequence.new({ ColorSequenceKeypoint.new(0, C.sky), ColorSequenceKeypoint.new(0.5, C.lav), ColorSequenceKeypoint.new(1, C.pink), }) ------------------------------------------------------------------------ -- UI helpers ------------------------------------------------------------------------ local function new(className: string, props: { [string]: any }, children: { Instance }?): any local inst = Instance.new(className) for k, v in props do if k ~= "Parent" then (inst :: any)[k] = v end end for _, child in (children or {}) :: { Instance } do child.Parent = inst end if props.Parent then inst.Parent = props.Parent end return inst end local function corner(r: number) return new("UICorner", { CornerRadius = UDim.new(0, r) }) end local function pad(t: number, r: number?, b: number?, l: number?) return new("UIPadding", { PaddingTop = UDim.new(0, t), PaddingRight = UDim.new(0, r or t), PaddingBottom = UDim.new(0, b or t), PaddingLeft = UDim.new(0, l or r or t), }) end local function list(gap: number, horizontal: boolean?) return new("UIListLayout", { Padding = UDim.new(0, gap), SortOrder = Enum.SortOrder.LayoutOrder, FillDirection = if horizontal then Enum.FillDirection.Horizontal else Enum.FillDirection.Vertical, VerticalAlignment = if horizontal then Enum.VerticalAlignment.Center else Enum.VerticalAlignment.Top, }) end local function brandGradient(rotation: number?) return new("UIGradient", { Color = BRAND, Rotation = rotation or 0 }) end -- Liquid glass: a faint white fill, a rim that is brighter on the top-left edge, and rounded corners. local function glass(props: { [string]: any }, radius: number?) local f = new("Frame", props) f.BackgroundColor3 = C.white f.BackgroundTransparency = props.BackgroundTransparency or 0.94 f.BorderSizePixel = 0 corner(radius or 16).Parent = f local stroke = new("UIStroke", { Color = C.white, Thickness = 1, Transparency = 0, ApplyStrokeMode = Enum.ApplyStrokeMode.Border, Parent = f }) new("UIGradient", { Rotation = 135, Transparency = NumberSequence.new({ NumberSequenceKeypoint.new(0, 0.35), NumberSequenceKeypoint.new(0.35, 0.88), NumberSequenceKeypoint.new(0.75, 0.9), NumberSequenceKeypoint.new(1, 0.7), }), Parent = stroke, }) return f end local function text(props: { [string]: any }) local l = new("TextLabel", { BackgroundTransparency = 1, Font = Enum.Font.BuilderSans, TextSize = 14, TextColor3 = C.ink, TextXAlignment = Enum.TextXAlignment.Left, TextYAlignment = Enum.TextYAlignment.Center, TextWrapped = true, Size = UDim2.new(1, 0, 0, 18), }) for k, v in props do if k ~= "Parent" then l[k] = v end end l.Parent = props.Parent return l end local function tween(inst: Instance, seconds: number, goal: { [string]: any }, style: Enum.EasingStyle?, repeatCount: number?, reverses: boolean?) local t = TweenService:Create(inst, TweenInfo.new(seconds, style or Enum.EasingStyle.Quint, Enum.EasingDirection.Out, repeatCount or 0, reverses or false), goal) t:Play() return t end -- A gradient button that gently grows on hover. local function button(parent: Instance, label: string, primary: boolean?) local b = new("TextButton", { Text = label, Font = Enum.Font.BuilderSansBold, TextSize = 15, TextColor3 = if primary then C.dark else C.ink, BackgroundColor3 = C.white, BackgroundTransparency = if primary then 0 else 0.9, AutoButtonColor = false, BorderSizePixel = 0, Size = UDim2.new(1, 0, 0, 38), Parent = parent, }, { corner(19) }) if primary then brandGradient(0).Parent = b else new("UIStroke", { Color = C.white, Transparency = 0.82, ApplyStrokeMode = Enum.ApplyStrokeMode.Border, Parent = b }) end local scale = new("UIScale", { Scale = 1, Parent = b }) b.MouseEnter:Connect(function() tween(scale, 0.25, { Scale = 1.02 }) end) b.MouseLeave:Connect(function() tween(scale, 0.25, { Scale = 1 }) end) return b end ------------------------------------------------------------------------ -- Toolbar + widget ------------------------------------------------------------------------ local toolbar = plugin:CreateToolbar("AxomAI") local toggleButton = toolbar:CreateButton("AxomAIToggle", "Open AxomAI", LOGO_ASSET_ID, "AxomAI") toggleButton.ClickableWhenViewportHidden = true local widgetInfo = DockWidgetPluginGuiInfo.new(Enum.InitialDockState.Right, false, false, 340, 600, 300, 420) local widget = plugin:CreateDockWidgetPluginGui("AxomAIWidget", widgetInfo) widget.Title = "AxomAI" widget.ZIndexBehavior = Enum.ZIndexBehavior.Sibling local root = new("Frame", { Size = UDim2.fromScale(1, 1), BackgroundColor3 = C.night, BorderSizePixel = 0, ClipsDescendants = true, Parent = widget }, { new("UIGradient", { Rotation = 90, Color = ColorSequence.new(C.night2, C.night) }), }) -- Aurora: soft brand colours drifting behind the header. local aurora = new("Frame", { Size = UDim2.new(1.6, 0, 0, 320), Position = UDim2.new(-0.3, 0, 0, -150), BackgroundColor3 = C.white, BackgroundTransparency = 0, BorderSizePixel = 0, Parent = root, }, { corner(400) }) local auroraGradient = new("UIGradient", { Color = BRAND, Rotation = 20, Transparency = NumberSequence.new({ NumberSequenceKeypoint.new(0, 0.55), NumberSequenceKeypoint.new(0.5, 0.7), NumberSequenceKeypoint.new(1, 0.6), }), Parent = aurora, }) local auroraFade = new("Frame", { Size = UDim2.new(1, 0, 0, 260), BackgroundColor3 = C.night2, BorderSizePixel = 0, Parent = root, }, { new("UIGradient", { Rotation = 90, Transparency = NumberSequence.new({ NumberSequenceKeypoint.new(0, 0.55), NumberSequenceKeypoint.new(0.45, 0.25), NumberSequenceKeypoint.new(1, 0) }), }), }) tween(auroraGradient, 9, { Rotation = 60, Offset = Vector2.new(0.15, 0) }, Enum.EasingStyle.Sine, -1, true) tween(aurora, 12, { Position = UDim2.new(-0.45, 0, 0, -130) }, Enum.EasingStyle.Sine, -1, true) auroraFade.ZIndex = 1 aurora.ZIndex = 0 local scroll = new("ScrollingFrame", { Size = UDim2.fromScale(1, 1), BackgroundTransparency = 1, BorderSizePixel = 0, ScrollBarThickness = 3, ScrollBarImageColor3 = C.lav, CanvasSize = UDim2.new(), AutomaticCanvasSize = Enum.AutomaticSize.Y, ZIndex = 2, Parent = root, }, { pad(16, 16, 20, 16), list(12) }) ------------------------------------------------------------------------ -- Header: logo, name, plan badge ------------------------------------------------------------------------ local header = new("Frame", { Size = UDim2.new(1, 0, 0, 40), BackgroundTransparency = 1, LayoutOrder = 1, Parent = scroll }) local logoX = 0 if LOGO_ASSET_ID ~= "" then new("ImageLabel", { Image = LOGO_ASSET_ID, BackgroundTransparency = 1, Size = UDim2.fromOffset(30, 30), Position = UDim2.new(0, 0, 0.5, -15), Parent = header }) logoX = 38 end text({ Parent = header, Text = "AxomAI", Font = Enum.Font.BuilderSansExtraBold, TextSize = 22, Position = UDim2.fromOffset(logoX, 0), Size = UDim2.new(1, -logoX - 90, 1, 0) }) local planBadge = new("TextLabel", { AnchorPoint = Vector2.new(1, 0.5), Position = UDim2.new(1, 0, 0.5, 0), Size = UDim2.fromOffset(0, 24), AutomaticSize = Enum.AutomaticSize.X, Text = "", Font = Enum.Font.BuilderSansExtraBold, TextSize = 12, TextColor3 = C.dark, BackgroundColor3 = C.white, Visible = false, Parent = header, }, { corner(12), pad(0, 10, 0, 10), brandGradient(0) }) ------------------------------------------------------------------------ -- Account card: avatar, name, plan, credits, daily credit ------------------------------------------------------------------------ local account = glass({ Size = UDim2.new(1, 0, 0, 0), AutomaticSize = Enum.AutomaticSize.Y, LayoutOrder = 2, Visible = false, Parent = scroll }, 20) pad(14).Parent = account list(12).Parent = account local who = new("Frame", { Size = UDim2.new(1, 0, 0, 48), BackgroundTransparency = 1, LayoutOrder = 1, Parent = account }) local avatar = new("ImageLabel", { Size = UDim2.fromOffset(48, 48), BackgroundColor3 = C.lav, BackgroundTransparency = 0.6, Image = "", Parent = who }, { corner(24) }) local avatarRing = new("UIStroke", { Thickness = 2, Color = C.white, Parent = avatar }) brandGradient(45).Parent = avatarRing local nameLabel = text({ Parent = who, Text = "", Font = Enum.Font.BuilderSansBold, TextSize = 17, Position = UDim2.fromOffset(60, 4), Size = UDim2.new(1, -60, 0, 22), TextTruncate = Enum.TextTruncate.AtEnd }) local planLine = text({ Parent = who, Text = "", TextSize = 13, TextColor3 = C.soft, Position = UDim2.fromOffset(60, 26), Size = UDim2.new(1, -60, 0, 18), TextTruncate = Enum.TextTruncate.AtEnd }) local creditsRow = new("Frame", { Size = UDim2.new(1, 0, 0, 44), BackgroundTransparency = 1, LayoutOrder = 2, Parent = account }) local creditsValue = text({ Parent = creditsRow, Text = "0.00", Font = Enum.Font.BuilderSansExtraBold, TextSize = 34, Size = UDim2.new(0, 0, 1, 0), AutomaticSize = Enum.AutomaticSize.X, TextWrapped = false }) brandGradient(0).Parent = creditsValue local creditsWord = text({ Parent = creditsRow, Text = "credits", TextSize = 14, TextColor3 = C.mute, Size = UDim2.new(0, 60, 0, 18), Position = UDim2.new(0, 0, 1, -22) }) local dailyChip = glass({ Size = UDim2.new(1, 0, 0, 34), LayoutOrder = 3, BackgroundTransparency = 0.92, Parent = account }, 17) local dailyDot = new("Frame", { Size = UDim2.fromOffset(8, 8), Position = UDim2.new(0, 14, 0.5, -4), BackgroundColor3 = C.mint, BorderSizePixel = 0, Parent = dailyChip }, { corner(4) }) local dailyText = text({ Parent = dailyChip, Text = "", TextSize = 13, TextColor3 = C.soft, Position = UDim2.fromOffset(30, 0), Size = UDim2.new(1, -40, 1, 0), TextTruncate = Enum.TextTruncate.AtEnd }) ------------------------------------------------------------------------ -- Status card: connection, what the AI is doing, model ------------------------------------------------------------------------ local status = glass({ Size = UDim2.new(1, 0, 0, 0), AutomaticSize = Enum.AutomaticSize.Y, LayoutOrder = 3, Parent = scroll }, 20) pad(14).Parent = status list(8).Parent = status local statusTop = new("Frame", { Size = UDim2.new(1, 0, 0, 22), BackgroundTransparency = 1, LayoutOrder = 1, Parent = status }) local statusDot = new("Frame", { Size = UDim2.fromOffset(10, 10), Position = UDim2.new(0, 2, 0.5, -5), BackgroundColor3 = C.mute, BorderSizePixel = 0, Parent = statusTop }, { corner(5) }) local statusHalo = new("Frame", { Size = UDim2.fromOffset(10, 10), AnchorPoint = Vector2.new(0.5, 0.5), Position = UDim2.new(0, 7, 0.5, 0), BackgroundColor3 = C.mint, BackgroundTransparency = 0.4, BorderSizePixel = 0, Visible = false, Parent = statusTop }, { corner(12) }) local statusTitle = text({ Parent = statusTop, Text = "Not linked", Font = Enum.Font.BuilderSansBold, TextSize = 16, Position = UDim2.fromOffset(22, 0), Size = UDim2.new(1, -110, 1, 0) }) local modelChip = new("TextLabel", { AnchorPoint = Vector2.new(1, 0.5), Position = UDim2.new(1, 0, 0.5, 0), Size = UDim2.fromOffset(0, 22), AutomaticSize = Enum.AutomaticSize.X, Text = "", Font = Enum.Font.BuilderSansMedium, TextSize = 12, TextColor3 = C.soft, BackgroundColor3 = C.white, BackgroundTransparency = 0.9, Visible = false, Parent = statusTop, }, { corner(11), pad(0, 9, 0, 9) }) local statusSub = text({ Parent = status, Text = "Link this place to start building.", TextSize = 13, TextColor3 = C.soft, LayoutOrder = 2, AutomaticSize = Enum.AutomaticSize.Y }) -- A shimmering bar while the AI is working. local shimmer = new("Frame", { Size = UDim2.new(1, 0, 0, 3), BackgroundColor3 = C.white, BackgroundTransparency = 0.88, BorderSizePixel = 0, LayoutOrder = 3, Visible = false, ClipsDescendants = true, Parent = status }, { corner(2) }) local shimmerBar = new("Frame", { Size = UDim2.new(0.4, 0, 1, 0), Position = UDim2.new(-0.4, 0, 0, 0), BackgroundColor3 = C.white, BorderSizePixel = 0, Parent = shimmer }, { corner(2), brandGradient(0) }) tween(shimmerBar, 1.4, { Position = UDim2.new(1, 0, 0, 0) }, Enum.EasingStyle.Sine, -1, false) -- Gentle pulse on the status dot tween(statusHalo, 1.6, { Size = UDim2.fromOffset(22, 22), BackgroundTransparency = 1 }, Enum.EasingStyle.Sine, -1, false) ------------------------------------------------------------------------ -- Session stats ------------------------------------------------------------------------ local stats = { scripts = 0, objects = 0, edits = 0 } local statsRow = new("Frame", { Size = UDim2.new(1, 0, 0, 62), BackgroundTransparency = 1, LayoutOrder = 4, Visible = false, Parent = scroll }, { new("UIGridLayout", { CellSize = UDim2.new(1 / 3, -6, 1, 0), CellPadding = UDim2.fromOffset(8, 0), SortOrder = Enum.SortOrder.LayoutOrder }), }) local statLabels = {} for i, item in { { "scripts", "Scripts" }, { "objects", "Objects" }, { "edits", "Changes" } } do local tile = glass({ LayoutOrder = i, Parent = statsRow }, 16) statLabels[item[1]] = text({ Parent = tile, Text = "0", Font = Enum.Font.BuilderSansExtraBold, TextSize = 22, Position = UDim2.fromOffset(12, 8), Size = UDim2.new(1, -24, 0, 26) }) text({ Parent = tile, Text = item[2], TextSize = 12, TextColor3 = C.mute, Position = UDim2.fromOffset(12, 34), Size = UDim2.new(1, -24, 0, 16) }) end local function bumpStat(key: string) stats[key] += 1 local l = statLabels[key] l.Text = tostring(stats[key]) l.TextSize = 26 tween(l, 0.4, { TextSize = 22 }) end ------------------------------------------------------------------------ -- Link card (shown until Studio is linked) ------------------------------------------------------------------------ local linkCard = glass({ Size = UDim2.new(1, 0, 0, 0), AutomaticSize = Enum.AutomaticSize.Y, LayoutOrder = 5, Parent = scroll }, 20) pad(16).Parent = linkCard list(10).Parent = linkCard text({ Parent = linkCard, Text = "Link Studio", Font = Enum.Font.BuilderSansExtraBold, TextSize = 20, LayoutOrder = 1, Size = UDim2.new(1, 0, 0, 24) }) text({ Parent = linkCard, Text = "1. Open the AxomAI app and press Link Studio\n2. Type the 6-character code below\n3. Describe what to build", TextSize = 13, TextColor3 = C.soft, LayoutOrder = 2, AutomaticSize = Enum.AutomaticSize.Y, LineHeight = 1.25, }) local codeBox = new("TextBox", { Size = UDim2.new(1, 0, 0, 52), PlaceholderText = "ABC123", PlaceholderColor3 = C.mute, Text = "", ClearTextOnFocus = false, Font = Enum.Font.RobotoMono, TextSize = 26, TextColor3 = C.ink, BackgroundColor3 = C.night, BackgroundTransparency = 0.3, BorderSizePixel = 0, LayoutOrder = 3, Parent = linkCard, }, { corner(14) }) local codeStroke = new("UIStroke", { Color = C.white, Transparency = 0.8, ApplyStrokeMode = Enum.ApplyStrokeMode.Border, Parent = codeBox }) codeBox.Focused:Connect(function() tween(codeStroke, 0.2, { Color = C.lav, Transparency = 0.2 }) end) codeBox.FocusLost:Connect(function() tween(codeStroke, 0.2, { Color = C.white, Transparency = 0.8 }) end) codeBox:GetPropertyChangedSignal("Text"):Connect(function() local clean = string.sub(string.upper((string.gsub(codeBox.Text, "[^%w]", ""))), 1, 6) if clean ~= codeBox.Text then codeBox.Text = clean end end) local linkButton = button(linkCard, "Link Studio", true) linkButton.LayoutOrder = 4 local linkError = text({ Parent = linkCard, Text = "", TextSize = 13, TextColor3 = C.warn, LayoutOrder = 5, AutomaticSize = Enum.AutomaticSize.Y, Visible = false }) ------------------------------------------------------------------------ -- Activity feed ------------------------------------------------------------------------ local feedHeader = text({ Parent = scroll, Text = "ACTIVITY", Font = Enum.Font.BuilderSansBold, TextSize = 11, TextColor3 = C.lav, LayoutOrder = 6, Size = UDim2.new(1, 0, 0, 14), Visible = false }) local feed = new("Frame", { Size = UDim2.new(1, 0, 0, 0), AutomaticSize = Enum.AutomaticSize.Y, BackgroundTransparency = 1, LayoutOrder = 7, Visible = false, Parent = scroll }, { list(6) }) local feedEmpty = text({ Parent = feed, Text = "Changes the AI makes in this place show up here.", TextSize = 13, TextColor3 = C.mute, LayoutOrder = 0, AutomaticSize = Enum.AutomaticSize.Y }) local VERB = { list_tree = "Looking around", read_script = "Reading", write_script = "Writing", create_instance = "Creating", set_properties = "Updating", delete_instance = "Removing", get_output = "Checking for errors", } local function targetOf(command: any): string local a = command.args or {} if command.type == "write_script" then return `{a.parent or ""}/{a.name or ""}` elseif command.type == "create_instance" then return `{a.class_name or ""}{if a.name then " · " .. a.name else ""}` elseif command.type == "get_output" then return "Studio output" end return a.path or "game" end local feedCount = 0 local function logActivity(command: any, ok: boolean, err: string?) feedEmpty.Visible = false feedCount += 1 local row = glass({ Size = UDim2.new(1, 0, 0, 0), AutomaticSize = Enum.AutomaticSize.Y, LayoutOrder = -feedCount, BackgroundTransparency = 0.95, Parent = feed }, 12) pad(8, 10, 8, 10).Parent = row new("Frame", { Size = UDim2.fromOffset(8, 8), Position = UDim2.fromOffset(0, 5), BackgroundColor3 = if ok then C.mint else C.warn, BorderSizePixel = 0, Parent = row }, { corner(4) }) text({ Parent = row, Text = VERB[command.type] or command.type, Font = Enum.Font.BuilderSansBold, TextSize = 13, Position = UDim2.fromOffset(16, 0), Size = UDim2.new(1, -60, 0, 18) }) text({ Parent = row, Text = DateTime.now():FormatLocalTime("HH:mm", "en-us"), TextSize = 11, TextColor3 = C.mute, TextXAlignment = Enum.TextXAlignment.Right, Position = UDim2.new(1, -44, 0, 0), Size = UDim2.fromOffset(44, 18) }) local detail = targetOf(command) .. (if ok then "" else `\n{err or "failed"}`) local d = text({ Parent = row, Text = detail, Font = Enum.Font.RobotoMono, TextSize = 11, TextColor3 = if ok then C.mute else C.warn, Position = UDim2.fromOffset(16, 20), Size = UDim2.new(1, -16, 0, 0), AutomaticSize = Enum.AutomaticSize.Y }) d.TextTruncate = if ok then Enum.TextTruncate.AtEnd else Enum.TextTruncate.None -- slide in row.BackgroundTransparency = 1 tween(row, 0.4, { BackgroundTransparency = 0.95 }) if feedCount > MAX_FEED then for _, child in feed:GetChildren() do if child:IsA("Frame") and child.LayoutOrder <= -(feedCount - MAX_FEED) then child:Destroy() end end end end ------------------------------------------------------------------------ -- Footer ------------------------------------------------------------------------ local footer = new("Frame", { Size = UDim2.new(1, 0, 0, 30), BackgroundTransparency = 1, LayoutOrder = 9, Parent = scroll }) local siteLabel = text({ Parent = footer, Text = `v{VERSION}`, TextSize = 12, TextColor3 = C.mute, Size = UDim2.new(1, -80, 1, 0), TextTruncate = Enum.TextTruncate.AtEnd }) local unlinkButton = new("TextButton", { AnchorPoint = Vector2.new(1, 0.5), Position = UDim2.new(1, 0, 0.5, 0), Size = UDim2.fromOffset(70, 26), Text = "Unlink", Font = Enum.Font.BuilderSansMedium, TextSize = 13, TextColor3 = C.soft, BackgroundColor3 = C.white, BackgroundTransparency = 0.92, AutoButtonColor = true, Visible = false, Parent = footer, }, { corner(13) }) ------------------------------------------------------------------------ -- State -> UI ------------------------------------------------------------------------ local function setStatus(kind: string, title: string, sub: string?) local color = if kind == "live" then C.mint elseif kind == "busy" then C.lav elseif kind == "warn" then C.warn else C.mute tween(statusDot, 0.3, { BackgroundColor3 = color }) statusHalo.BackgroundColor3 = color statusHalo.Visible = kind ~= "off" statusTitle.Text = title if sub then statusSub.Text = sub end shimmer.Visible = kind == "busy" end local function formatDate(ms: number?): string if not ms or ms <= 0 then return "" end return DateTime.fromUnixTimestampMillis(math.floor(ms)):FormatLocalTime("MMM D", "en-us") end local function untilText(ms: number): string local mins = math.max(1, math.ceil((ms / 1000 - os.time()) / 60)) local h = math.floor(mins / 60) return if h > 0 then `{h}h {mins % 60}m` else `{mins}m` end local shownBalance = 0 local function animateBalance(target: number) local from = shownBalance shownBalance = target task.spawn(function() local steps = 18 for i = 1, steps do local v = from + (target - from) * (1 - (1 - i / steps) ^ 3) creditsValue.Text = string.format("%.2f", v) task.wait(0.025) end creditsValue.Text = string.format("%.2f", target) end) end local function renderAccount() local linked = token ~= nil account.Visible = linked and me ~= nil statsRow.Visible = linked feedHeader.Visible = linked feed.Visible = linked linkCard.Visible = not linked unlinkButton.Visible = linked planBadge.Visible = linked and me ~= nil if not me then return end nameLabel.Text = me.name or "Creator" planBadge.Text = string.upper(me.planName or "Free") local planText = `{me.planName or "Free"} plan` if me.endsAt then planText ..= ` · ends {formatDate(me.endsAt)}` elseif me.renews then planText ..= ` · renews {formatDate(me.renews)}` elseif me.plan == "free" then planText ..= " · upgrade on the site" end planLine.Text = planText if math.abs((me.balance or 0) - shownBalance) > 0.0005 then animateBalance(me.balance or 0) end creditsWord.Position = UDim2.new(0, creditsValue.AbsoluteSize.X + 8, 1, -22) local d = me.daily or {} if not d.linked then dailyText.Text = "Link Discord on the site for a free daily credit" dailyDot.BackgroundColor3 = C.mute elseif (d.ready or 0) > 0 then dailyText.Text = `{d.ready} free credit{if d.ready == 1 then "" else "s"} ready. Claim on the site` dailyDot.BackgroundColor3 = C.mint elseif d.next and d.next > 0 then dailyText.Text = `Next free credit in {untilText(d.next)}` dailyDot.BackgroundColor3 = C.lav else dailyText.Text = "Daily credit claimed" dailyDot.BackgroundColor3 = C.lav end modelChip.Visible = me.model ~= nil modelChip.Text = me.model or "" siteLabel.Text = if me.site and me.site ~= "" then `{me.site} · v{VERSION}` else `v{VERSION}` if me.busy and me.current then setStatus("busy", "Building…", `"{me.current}"`) end end creditsValue:GetPropertyChangedSignal("AbsoluteSize"):Connect(function() creditsWord.Position = UDim2.new(0, creditsValue.AbsoluteSize.X + 8, 1, -22) end) toggleButton.Click:Connect(function() widget.Enabled = not widget.Enabled end) toggleButton:SetActive(widget.Enabled) widget:GetPropertyChangedSignal("Enabled"):Connect(function() toggleButton:SetActive(widget.Enabled) end) ------------------------------------------------------------------------ -- Timeout helper: runs fn in its own thread and gives up after `seconds`, so one -- stuck request or command can never freeze the whole plugin. ------------------------------------------------------------------------ local function runWithTimeout(seconds: number, fn: () -> any) local done = false local packed = nil task.spawn(function() packed = table.pack(pcall(fn)) done = true end) local deadline = os.clock() + seconds while not done and os.clock() < deadline do task.wait(0.05) end if not done then return false, `timed out after {seconds}s` end return packed[1], packed[2] end ------------------------------------------------------------------------ -- HTTP helper. Returns: ok, decodedBodyOrErrorMessage, rawResponse ------------------------------------------------------------------------ local function request(method: string, path: string, body: any?) local ok, res = runWithTimeout(REQUEST_TIMEOUT, function() return HttpService:RequestAsync({ Url = BASE_URL .. path, Method = method, Headers = { ["Content-Type"] = "application/json", ["x-plugin-token"] = token or "", }, Body = if body ~= nil then HttpService:JSONEncode(body) else nil, }) end) if not ok then local message = tostring(res) if string.find(message, "Http requests are not enabled") or string.find(message, "not allowed") then message = "Studio blocked the connection. Open Plugins > Manage Plugins, find AxomAI, and allow HTTP requests to " .. BASE_URL elseif string.find(message, "ConnectFail") or string.find(message, "DnsResolve") or string.find(message, "timed out") then message = "Can't reach AxomAI at " .. BASE_URL .. ". Check your internet, or download the plugin again from the AxomAI app." end return false, message, nil end local decoded = nil if res.Body ~= "" then pcall(function() decoded = HttpService:JSONDecode(res.Body) end) end if not res.Success then local message = if decoded and decoded.error then decoded.error else `{res.StatusCode} {res.StatusMessage}` return false, message, res end return true, decoded, res end local function loadAvatar() task.spawn(function() local okId, userId = pcall(function() return StudioService:GetUserId() end) if not okId or not userId or userId == 0 then return end local okImg, image = pcall(function() return Players:GetUserThumbnailAsync(userId, Enum.ThumbnailType.HeadShot, Enum.ThumbnailSize.Size150x150) end) if okImg then avatar.Image = image end end) end local function refreshMe() if not token then return end local ok, data = request("GET", "/api/plugin/me") if ok and data then me = data busyRemote = data.busy == true renderAccount() end end ------------------------------------------------------------------------ -- Output capture (edit-mode output only) ------------------------------------------------------------------------ local outputLog = {} LogService.MessageOut:Connect(function(message, messageType) table.insert(outputLog, { type = messageType.Name, message = message }) if #outputLog > 200 then table.remove(outputLog, 1) end end) ------------------------------------------------------------------------ -- Helpers for handlers ------------------------------------------------------------------------ local SCRIPT_CLASSES = { Script = true, LocalScript = true, ModuleScript = true } local function resolve(path: string?): Instance if path == nil or path == "" or path == "game" then return game end local current: Instance = game for segment in string.gmatch(path, "[^/]+") do local child = current:FindFirstChild(segment) if not child then error(`Path not found: "{path}" (no child named "{segment}" under "{current:GetFullName()}")`) end current = child end return current end -- Convert JSON shapes like {"$type":"Vector3",...} into Roblox values. local function decode(value: any): any if type(value) == "table" and value["$type"] then local kind = value["$type"] if kind == "Vector3" then return Vector3.new(value.x or 0, value.y or 0, value.z or 0) elseif kind == "Vector2" then return Vector2.new(value.x or 0, value.y or 0) elseif kind == "Color3" then return Color3.fromRGB(value.r or 0, value.g or 0, value.b or 0) elseif kind == "UDim2" then return UDim2.new(value.xs or 0, value.xo or 0, value.ys or 0, value.yo or 0) elseif kind == "CFrame" then return CFrame.new(value.x or 0, value.y or 0, value.z or 0) elseif kind == "Enum" then return (Enum :: any)[value.enum][value.name] end error(`Unknown value type "{kind}"`) end return value end local function applyProperties(instance: Instance, properties: { [string]: any }?): { string } local failures = {} for name, value in (properties or {}) :: { [string]: any } do local ok, err = pcall(function() local target: any = instance target[name] = decode(value) end) if not ok then table.insert(failures, `{name}: {tostring(err)}`) end end return failures end local function describe(instance: Instance, depth: number, counter: { n: number }) local node: { [string]: any } = { name = instance.Name, class = instance.ClassName } if instance:IsA("LuaSourceContainer") then local _, newlines = string.gsub((instance :: any).Source, "\n", "\n") node.lines = newlines + 1 end local children = instance:GetChildren() if #children > 0 then if depth > 0 then node.children = {} for _, child in children do if counter.n >= MAX_TREE_NODES then node.truncated = true break end counter.n += 1 table.insert(node.children, describe(child, depth - 1, counter)) end else node.childCount = #children end end return node end ------------------------------------------------------------------------ -- Command handlers (names match the tools in server/agent.js) ------------------------------------------------------------------------ local handlers: { [string]: (args: any) -> any } = {} local MUTATING = { write_script = true, create_instance = true, set_properties = true, delete_instance = true } function handlers.list_tree(args) local instance = resolve(args.path) local depth = math.clamp(args.depth or 2, 1, 5) return describe(instance, depth, { n = 0 }) end function handlers.read_script(args) local instance = resolve(args.path) if not instance:IsA("LuaSourceContainer") then error(`"{args.path}" is a {instance.ClassName}, not a script`) end return { path = args.path, class = instance.ClassName, source = (instance :: any).Source } end function handlers.write_script(args) local parent = resolve(args.parent) local existing = parent:FindFirstChild(args.name) if existing then if not existing:IsA("LuaSourceContainer") then error(`"{args.name}" already exists under "{args.parent}" and is a {existing.ClassName}`) end ScriptEditorService:UpdateSourceAsync(existing :: LuaSourceContainer, function() return args.source end) return { ok = true, created = false, class = existing.ClassName } end local className = args.class_name or "Script" if not SCRIPT_CLASSES[className] then error(`class_name must be Script, LocalScript or ModuleScript (got "{className}")`) end local newScript: any = Instance.new(className) newScript.Name = args.name newScript.Source = args.source newScript.Parent = parent return { ok = true, created = true, class = className } end function handlers.create_instance(args) local parent = resolve(args.parent) local ok, instance = pcall(Instance.new, args.class_name) if not ok then error(`Cannot create a "{args.class_name}": {tostring(instance)}`) end if args.name then instance.Name = args.name end local failures = applyProperties(instance, args.properties) instance.Parent = parent return { ok = true, path = instance:GetFullName(), failedProperties = failures } end function handlers.set_properties(args) local instance = resolve(args.path) local failures = applyProperties(instance, args.properties) return { ok = #failures == 0, failedProperties = failures } end function handlers.delete_instance(args) local instance = resolve(args.path) if instance == game or instance.Parent == game then error("Refusing to delete the DataModel or a top-level service") end instance:Destroy() return { ok = true } end function handlers.get_output(args) local limit = math.clamp(args.limit or 40, 1, 100) local lines = {} for i = math.max(1, #outputLog - limit + 1), #outputLog do table.insert(lines, outputLog[i]) end return { lines = lines } end local function execute(command: { id: string, type: string, args: any }) local handler = handlers[command.type] if not handler then return false, `Unknown command "{command.type}"` end -- Wrap every change in an undo step so users can Ctrl+Z the AI's work. local recording = nil if MUTATING[command.type] then recording = ChangeHistoryService:TryBeginRecording(`AxomAI: {command.type}`) end local ok, result = runWithTimeout(COMMAND_TIMEOUT, function() return handler(command.args or {}) end) if recording then ChangeHistoryService:FinishRecording( recording, if ok then Enum.FinishRecordingOperation.Commit else Enum.FinishRecordingOperation.Cancel ) end if ok then if command.type == "write_script" then bumpStat("scripts") elseif command.type == "create_instance" then bumpStat("objects") end if MUTATING[command.type] then bumpStat("edits") end end return ok, result end ------------------------------------------------------------------------ -- Loops: long-poll for commands, and refresh the account card ------------------------------------------------------------------------ local function idleStatus() local site = if me and me.site and me.site ~= "" then me.site else "the AxomAI app" setStatus("live", "Connected", `Waiting for your next prompt on {site}.`) end local function pollLoop() loopId += 1 local myId = loopId idleStatus() renderAccount() while loopId == myId and token do local ok, data, res = request("GET", "/api/plugin/poll") if loopId ~= myId then break end if ok then local commands = (data and data.commands) or {} if #commands > 0 then for _, command in commands do setStatus("busy", "Building…", if me and me.current then `"{me.current}"` else `{VERB[command.type] or command.type} {targetOf(command)}`) local success, result = execute(command) logActivity(command, success, if success then nil else tostring(result)) request("POST", "/api/plugin/result", { id = command.id, ok = success, result = if success then result else nil, error = if success then nil else tostring(result), }) end task.spawn(refreshMe) elseif not busyRemote then idleStatus() end elseif res and res.StatusCode == 401 then -- The server doesn't know this token any more. Ask for a new code. token = nil me = nil plugin:SetSetting(TOKEN_SETTING, nil) setStatus("warn", "Link expired", "Get a new code from the AxomAI app and link again.") renderAccount() break else setStatus("warn", "Reconnecting…", `{tostring(data)} Retrying.`) task.wait(3) end end end local function accountLoop() local myId = loopId while token and loopId == myId do refreshMe() if busyRemote then if me and me.current then setStatus("busy", "Building…", `"{me.current}"`) end task.wait(4) else task.wait(15) end end end local function startLoops() task.spawn(pollLoop) task.delay(0.2, function() task.spawn(accountLoop) end) loadAvatar() end ------------------------------------------------------------------------ -- Link / unlink ------------------------------------------------------------------------ local linking = false local function showLinkError(message: string?) linkError.Text = message or "" linkError.Visible = message ~= nil end linkButton.Activated:Connect(function() if linking then return end local code = string.upper((string.gsub(codeBox.Text, "%s", ""))) if #code ~= 6 then showLinkError("Enter the 6-character code from the AxomAI app.") return end linking = true showLinkError(nil) linkButton.Text = "Linking…" -- Tell the server which Roblox account is signed in to Studio, so a code can only be used by its owner. local gotId, studioUserId = pcall(function() return StudioService:GetUserId() end) local ok, data = request("POST", "/api/plugin/pair", { code = code, userId = if gotId then studioUserId else 0 }) linking = false linkButton.Text = "Link Studio" if ok and data and data.token then token = data.token plugin:SetSetting(TOKEN_SETTING, token) codeBox.Text = "" renderAccount() startLoops() else showLinkError(tostring(data)) end end) unlinkButton.Activated:Connect(function() token = nil me = nil loopId += 1 plugin:SetSetting(TOKEN_SETTING, nil) setStatus("off", "Not linked", "Link this place to start building.") renderAccount() end) renderAccount() if token then setStatus("busy", "Connecting…", "Reconnecting to AxomAI.") startLoops() else setStatus("off", "Not linked", "Link this place to start building.") end