Преглед на файлове

Add AGI Forge: natural-language-to-AGI-script copilot webapp (#252)

A terminal-style web app (Development group) that turns a plain-language
request into an AGI script and runs it on the server, Claude-Code style.

Flow: prompt -> backend/generate.agi asks the configured aimodel, grounded
with the live AGI API reference (Terminal/docs/api.json via appdata) and the
installed ao_module list, to emit a script -> the script is shown with syntax
highlighting -> backend/run.agi evaluates it in a captured VM (same model as
the Terminal REPL), returning stdout, console logs, errors and any file the
script registers via provideOutputFile(); generated files land in
user:/Desktop/AGI Forge/ and are offered as Download / Open-in-Files.

UI is a responsive VS Code-dark terminal: model picker, auto-run toggle,
editable code blocks, per-turn re-run, "Ask AI to fix it" on errors, and
multi-turn context for follow-ups. No Go changes: backend is pure AGI served
through the existing authenticated /system/ajgi/interface endpoint.

https://claude.ai/code/session_0193Pm4yd6mdPDqvdYfeZG3a

Co-authored-by: Claude <noreply@anthropic.com>
Alan Yeung преди 5 дни
родител
ревизия
bbd4f39258
променени са 6 файла, в които са добавени 1161 реда и са изтрити 0 реда
  1. 175 0
      src/web/AGIForge/backend/generate.agi
  2. 38 0
      src/web/AGIForge/backend/models.agi
  3. 144 0
      src/web/AGIForge/backend/run.agi
  4. 32 0
      src/web/AGIForge/img/icon.svg
  5. 739 0
      src/web/AGIForge/index.html
  6. 33 0
      src/web/AGIForge/init.agi

+ 175 - 0
src/web/AGIForge/backend/generate.agi

@@ -0,0 +1,175 @@
+/*
+    AGI Forge — script generation backend
+    =====================================
+    Turns a plain-language request into a ready-to-run AGI script using the
+    configured AI model. The model is grounded with:
+        - the authoritative AGI API reference (Terminal/docs/api.json)
+        - the list of ao_modules (apps) installed on this server
+        - the AGI Forge output / runtime conventions
+
+    POST parameters:
+        prompt   : the user's natural language request (string)
+        history  : JSON array of prior turns [{role:"user"|"assistant", content}]
+        options  : JSON object { model, temperature }
+
+    Response (JSON):
+        { ok:true,  explanation:"...", script:"...", model:"...", usage:{...} }
+        { ok:false, error:"..." }
+*/
+
+requirelib("aimodel");
+requirelib("appdata");
+
+/* ── helpers ─────────────────────────────────────────────────────────────── */
+
+function trim(s) { return ("" + s).replace(/^\s+/, "").replace(/\s+$/, ""); }
+
+//Build a compact, token-friendly API reference from the canonical api.json.
+function buildApiRef() {
+	var raw = "";
+	try { raw = appdata.readFile("Terminal/docs/api.json"); } catch (e) { return ""; }
+	var doc; try { doc = JSON.parse(raw); } catch (e) { return ""; }
+	if (!doc || !doc.sections) return "";
+
+	var lines = [];
+	for (var i = 0; i < doc.sections.length; i++) {
+		var s = doc.sections[i];
+		var head = "## " + s.name;
+		if (s.load) {
+			head += "  (load: " + s.load + ")";
+		} else if (s.id && s.id != "core" && s.id != "db" && s.id != "user") {
+			head += "  (requirelib(\"" + s.id + "\"))";
+		}
+		lines.push(head);
+		var fns = s.functions || [];
+		for (var j = 0; j < fns.length; j++) {
+			var f = fns[j];
+			var l = "  " + (f.sig || f.name);
+			if (f.ret && f.ret != "void") l += " -> " + f.ret;
+			if (f.desc) l += "   // " + f.desc;
+			lines.push(l);
+		}
+	}
+	return lines.join("\n");
+}
+
+//List the installed modules so the model knows what apps exist on this server.
+function buildModuleList() {
+	var mods = [];
+	try { mods = appdata.getModuleList() || []; } catch (e) { return ""; }
+	var out = [];
+	for (var i = 0; i < mods.length; i++) {
+		var m = mods[i];
+		if (!m) continue;
+		var name = m.Name || m.name || "";
+		if (name == "") continue;
+		var grp = m.Group || m.group || "";
+		var desc = m.Desc || m.desc || "";
+		out.push("  - " + name + (grp ? " [" + grp + "]" : "") + (desc ? ": " + desc : ""));
+	}
+	return out.join("\n");
+}
+
+//Pull <explanation> and <script> out of the model reply (robust to stray fences).
+function parseReply(text) {
+	var explanation = "", script = "";
+	var em = /<explanation>([\s\S]*?)<\/explanation>/i.exec(text);
+	var sm = /<script>([\s\S]*?)<\/script>/i.exec(text);
+	if (em) explanation = trim(em[1]);
+	if (sm) script = trim(sm[1]);
+
+	if (script == "") {
+		var fm = /```(?:javascript|js|agi)?\s*([\s\S]*?)```/i.exec(text);
+		if (fm) script = trim(fm[1]);
+	}
+	if (explanation == "" && script == "") explanation = trim(text);
+
+	//Strip any leftover markdown fences that slipped into the script body
+	script = script.replace(/^```(?:javascript|js|agi)?\s*/i, "").replace(/```\s*$/, "");
+	return { explanation: explanation, script: trim(script) };
+}
+
+/* ── read inputs ─────────────────────────────────────────────────────────── */
+
+var userPrompt = (typeof prompt !== "undefined") ? "" + prompt : "";
+var rawHistory = (typeof history !== "undefined") ? history : "[]";
+var rawOptions = (typeof options !== "undefined") ? options : "{}";
+
+var hist = []; try { hist = JSON.parse(rawHistory) || []; } catch (e) { hist = []; }
+var opts = {}; try { opts = JSON.parse(rawOptions) || {}; } catch (e) { opts = {}; }
+
+if (trim(userPrompt) == "") {
+	sendJSONResp(JSON.stringify({ ok: false, error: "Empty request" }));
+	exit();
+}
+
+/* ── build the grounding system prompt ───────────────────────────────────── */
+
+var apiRef = buildApiRef();
+var modList = buildModuleList();
+
+var systemPrompt = [
+	"You are AGI Forge, an expert assistant that writes ArozOS AGI scripts to accomplish a user's task.",
+	"An AGI script is server-side JavaScript executed inside a sandboxed Otto VM on the user's self-hosted ArozOS server, running with that user's own file and system permissions.",
+	"",
+	"=== HARD RULES ===",
+	"1. Otto supports ECMAScript 5 ONLY. Do NOT use let, const, arrow functions (=>), template literals (backticks), async/await, Promise, class, destructuring, spread/rest, for...of, Map, or Set. Use var, classic function expressions, and string concatenation with +.",
+	"2. Load any non-core library before using it, e.g. requirelib(\"filelib\"); . Core and DB functions need no requirelib.",
+	"3. Return a result the user can read with sendResp(text), echo(text) or sendJSONResp(obj). Use console.log(...) for progress/diagnostic lines. Always finish with a short, human-readable summary via sendResp or echo.",
+	"4. Use ArozOS VIRTUAL paths only, never real OS paths: user:/ is the user's home, tmp:/ is temporary storage. Example: \"user:/Desktop/notes.txt\". Never write a literal path like /home/... or C:/...",
+	"5. To give the user a downloadable file: write it (e.g. filelib.writeFile(path, content), or have a library produce it), then call provideOutputFile(path) so AGI Forge shows a download link. A pre-created destination folder is available in the global variable OUTPUT_DIR. Example: requirelib(\"filelib\"); filelib.writeFile(OUTPUT_DIR + \"report.csv\", csv); provideOutputFile(OUTPUT_DIR + \"report.csv\");",
+	"6. The script must be SELF-CONTAINED and run start to finish with NO user interaction. Do NOT call websocket.upgrade(). Avoid infinite loops and long delay() calls.",
+	"7. Validate inputs and wrap risky operations in try/catch; on failure report a clear message via sendResp instead of throwing.",
+	"8. Only use functions documented in the API reference below. Do not invent functions or libraries.",
+	"",
+	"=== AGI API REFERENCE (authoritative) ===",
+	(apiRef != "" ? apiRef : "(reference unavailable — rely on the core functions sendResp, sendJSONResp, echo, console.log, requirelib and the filelib/http/imagelib libraries)"),
+	"",
+	"=== INSTALLED ao_modules (apps on this server, for context) ===",
+	(modList != "" ? modList : "  (module list unavailable)"),
+	"",
+	"=== RESPONSE FORMAT (MANDATORY) ===",
+	"Reply with EXACTLY these two blocks and nothing else — no markdown, no prose outside the tags:",
+	"<explanation>",
+	"One or two short plain sentences describing what the script does (no code here).",
+	"</explanation>",
+	"<script>",
+	"// the complete, ready-to-run AGI script",
+	"</script>",
+	"Do NOT wrap the script in markdown code fences. If the request is impossible or unsafe, explain why inside <explanation> and leave <script> empty."
+].join("\n");
+
+/* ── assemble the conversation ───────────────────────────────────────────── */
+
+var messages = [{ role: "system", content: systemPrompt }];
+for (var i = 0; i < hist.length; i++) {
+	var h = hist[i];
+	if (h && h.role && typeof h.content != "undefined" && (h.role == "user" || h.role == "assistant")) {
+		messages.push({ role: h.role, content: "" + h.content });
+	}
+}
+messages.push({ role: "user", content: userPrompt });
+
+var callOpts = { temperature: (typeof opts.temperature == "number") ? opts.temperature : 0.2 };
+if (opts.model && trim(opts.model) != "") callOpts.model = opts.model;
+
+/* ── call the model ──────────────────────────────────────────────────────── */
+
+try {
+	var res = aimodel.request(messages, callOpts);
+	var text = "";
+	if (res && res.choices && res.choices.length > 0 && res.choices[0].message) {
+		text = res.choices[0].message.content || "";
+	}
+
+	var parsed = parseReply(text);
+	sendJSONResp(JSON.stringify({
+		ok: true,
+		explanation: parsed.explanation,
+		script: parsed.script,
+		model: (res && res.model) ? res.model : (callOpts.model || ""),
+		usage: (res && res.usage) ? res.usage : null
+	}));
+} catch (e) {
+	sendJSONResp(JSON.stringify({ ok: false, error: "" + e }));
+}

+ 38 - 0
src/web/AGIForge/backend/models.agi

@@ -0,0 +1,38 @@
+/*
+    AGI Forge — models backend
+    Returns the default model plus the union of configured (priced) models and
+    the models advertised by the live endpoint, for the model picker.
+    Shape: { "default": "gpt-4o-mini", "models": ["gpt-4o-mini", ...] }
+*/
+
+requirelib("aimodel");
+
+var out = { "default": "", "models": [] };
+
+try {
+	var cfg = aimodel.models(); // { default, models:[pricing keys] }
+	out["default"] = cfg["default"] || "";
+
+	var set = {};
+	var list = cfg.models || [];
+	for (var i = 0; i < list.length; i++) { set[list[i]] = true; }
+
+	//Merge in the live models advertised by the endpoint (best effort)
+	try {
+		var live = aimodel.listModels(); // { models:[...] } or { error }
+		var lm = live.models || [];
+		for (var j = 0; j < lm.length; j++) { set[lm[j]] = true; }
+	} catch (e) { /* endpoint unreachable - keep configured list */ }
+
+	//Always include the default model in the list
+	if (out["default"]) { set[out["default"]] = true; }
+
+	var merged = [];
+	for (var k in set) { if (set.hasOwnProperty(k)) { merged.push(k); } }
+	merged.sort();
+	out.models = merged;
+} catch (e) {
+	out.error = "" + e;
+}
+
+sendJSONResp(JSON.stringify(out));

+ 144 - 0
src/web/AGIForge/backend/run.agi

@@ -0,0 +1,144 @@
+/*
+    AGI Forge — script execution backend
+    ====================================
+    Executes a generated AGI script inside this request's VM with full output
+    capture and output-file detection, then returns a structured result.
+
+    The evaluated script runs with all standard AGI globals and requirelib()
+    libraries available (same model as the Terminal REPL). Two extras are
+    injected for it:
+        OUTPUT_DIR              a pre-created folder ("user:/Desktop/AGI Forge/")
+        provideOutputFile(path) registers a virtual path as a download for the user
+
+    POST parameters:
+        script : the AGI script source to execute (string)
+
+    Response (JSON):
+        {
+            ok       : bool,             // false if the script threw
+            output   : "...",            // captured sendResp/echo/sendJSONResp/return
+            logs     : ["..."],          // captured console.log lines
+            error    : bool,
+            errorMsg : "...",
+            files    : [ { path, name, size, exists } ]
+        }
+*/
+
+var src = (typeof script !== "undefined") ? "" + script : "";
+if (src.replace(/^\s+/, "").replace(/\s+$/, "") == "") {
+	sendJSONResp(JSON.stringify({
+		ok: false, error: true, errorMsg: "No script to run",
+		output: "", logs: [], files: []
+	}));
+	exit();
+}
+
+/* ── capture console.log ─────────────────────────────────────────────────── */
+var _logs = [];
+var _origLog = console.log;
+console.log = function () {
+	var parts = [];
+	for (var i = 0; i < arguments.length; i++) {
+		var a = arguments[i];
+		parts.push(typeof a === "object" ? JSON.stringify(a) : String(a));
+	}
+	var ln = parts.join(" ");
+	_logs.push(ln);
+	try { _origLog("[AGIForge:" + USERNAME + "] " + ln); } catch (e) { }
+};
+
+/* ── capture the textual response channel ────────────────────────────────── */
+var _captured = "";
+var _origSendResp = sendResp;
+var _origEcho = echo;
+var _origSendJSONResp = sendJSONResp;
+sendResp = function (v) { _captured = String(v); };
+echo = function (v) { _captured = _captured + String(v); };
+sendJSONResp = function (v) { _captured = (typeof v === "string") ? v : JSON.stringify(v); };
+
+/* ── output-file registry (exposed to the evaluated script) ──────────────── */
+var _outFiles = [];
+function provideOutputFile(vpath) {
+	if (vpath === undefined || vpath === null) return;
+	_outFiles.push("" + vpath);
+}
+
+/* ── pre-create a destination folder for generated files ─────────────────── */
+var OUTPUT_DIR = "user:/Desktop/AGI Forge/";
+var _filelibReady = false;
+try {
+	requirelib("filelib");
+	_filelibReady = true;
+	if (!filelib.fileExists(OUTPUT_DIR) && !filelib.isDir(OUTPUT_DIR)) {
+		filelib.mkdir(OUTPUT_DIR);
+	}
+} catch (e) { /* filelib unavailable — script may still run without file output */ }
+
+/* ── make exit() a clean stop rather than a hard panic ───────────────────── */
+//A generated script may call exit() to finish early. Throwing unwinds eval();
+//the _exited flag lets us tell that apart from a genuine error regardless of
+//how Otto surfaces the thrown value in the catch clause.
+var _origExit = exit;
+var _exited = false;
+exit = function () { _exited = true; throw "__AGIFORGE_EXIT__"; };
+
+/* ── run the script ──────────────────────────────────────────────────────── */
+HTTP_RESP = "";
+var _error = false;
+var _errorMsg = "";
+var _result;
+try {
+	_result = eval(src);
+} catch (e) {
+	if (_exited || e === "__AGIFORGE_EXIT__") {
+		// script called exit() — treat as a normal finish
+	} else {
+		_error = true;
+		_errorMsg = (e && e.toString) ? e.toString() : String(e);
+	}
+}
+
+/* ── resolve the textual output (priority: explicit > HTTP_RESP > return) ── */
+var _output = "";
+if (_captured !== "") {
+	_output = _captured;
+} else if (typeof HTTP_RESP !== "undefined" && HTTP_RESP !== "") {
+	_output = String(HTTP_RESP);
+} else if (_result !== undefined && _result !== null) {
+	try {
+		_output = (typeof _result === "object") ? JSON.stringify(_result, null, 2) : String(_result);
+	} catch (e) { _output = String(_result); }
+}
+
+/* ── restore the overridden globals (hygiene; VM is per-request anyway) ──── */
+console.log = _origLog;
+sendResp = _origSendResp;
+echo = _origEcho;
+sendJSONResp = _origSendJSONResp;
+exit = _origExit;
+
+/* ── collect metadata for any registered output files ────────────────────── */
+var files = [];
+var seen = {};
+for (var i = 0; i < _outFiles.length; i++) {
+	var p = _outFiles[i];
+	if (!p || seen[p]) continue;
+	seen[p] = true;
+	var entry = { path: p, name: p.split("/").pop(), size: -1, exists: false };
+	try {
+		if (_filelibReady && filelib.fileExists(p)) {
+			entry.exists = true;
+			try { entry.size = filelib.filesize(p); } catch (e) { }
+		}
+	} catch (e) { }
+	files.push(entry);
+}
+
+_origSendJSONResp(JSON.stringify({
+	ok: !_error,
+	output: _output,
+	logs: _logs,
+	error: _error,
+	errorMsg: _errorMsg,
+	files: files
+}));

+ 32 - 0
src/web/AGIForge/img/icon.svg

@@ -0,0 +1,32 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 96" width="96" height="96" role="img" aria-label="AGI Forge">
+  <defs>
+    <linearGradient id="bg" x1="0" y1="0" x2="0" y2="1">
+      <stop offset="0" stop-color="#2d2d2d"/>
+      <stop offset="1" stop-color="#1e1e1e"/>
+    </linearGradient>
+    <linearGradient id="spark" x1="0" y1="0" x2="1" y2="1">
+      <stop offset="0" stop-color="#4ec94e"/>
+      <stop offset="1" stop-color="#27e0a0"/>
+    </linearGradient>
+  </defs>
+
+  <!-- terminal body -->
+  <rect x="6" y="6" width="84" height="84" rx="18" fill="url(#bg)" stroke="#3c3c3c" stroke-width="2"/>
+  <rect x="6" y="6" width="84" height="22" rx="18" fill="#252526"/>
+  <rect x="6" y="18" width="84" height="10" fill="#252526"/>
+
+  <!-- title-bar dots -->
+  <circle cx="20" cy="17" r="3.2" fill="#f44747"/>
+  <circle cx="31" cy="17" r="3.2" fill="#dcdcaa"/>
+  <circle cx="42" cy="17" r="3.2" fill="#4ec94e"/>
+
+  <!-- prompt chevron + cursor -->
+  <path d="M22 46 L34 56 L22 66" fill="none" stroke="url(#spark)" stroke-width="6"
+        stroke-linecap="round" stroke-linejoin="round"/>
+  <rect x="40" y="60" width="22" height="6" rx="3" fill="#d4d4d4"/>
+
+  <!-- AI spark -->
+  <path d="M70 36 L73.4 45.6 L83 49 L73.4 52.4 L70 62 L66.6 52.4 L57 49 L66.6 45.6 Z"
+        fill="url(#spark)"/>
+  <circle cx="80" cy="34" r="3" fill="#27e0a0"/>
+</svg>

+ 739 - 0
src/web/AGIForge/index.html

@@ -0,0 +1,739 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
+    <title>AGI Forge</title>
+    <script src="../script/jquery.min.js"></script>
+    <script src="../script/ao_module.js"></script>
+    <style>
+        *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
+
+        :root {
+            --bg:      #1e1e1e;
+            --bg2:     #252526;
+            --bg3:     #2d2d2d;
+            --bg4:     #333334;
+            --border:  #3c3c3c;
+            --fg:      #d4d4d4;
+            --dim:     #858585;
+            --green:   #4ec94e;
+            --mint:    #27e0a0;
+            --red:     #f44747;
+            --yellow:  #dcdcaa;
+            --blue:    #569cd6;
+            --orange:  #ce9178;
+            --purple:  #c586c0;
+            --numlit:  #b5cea8;
+            --comment: #6a9955;
+        }
+
+        html, body { height: 100%; background: var(--bg); color: var(--fg);
+            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
+            font-size: 14px; overflow: hidden; }
+
+        .mono { font-family: 'Cascadia Code','SFMono-Regular','Consolas','Liberation Mono','Courier New',monospace; }
+
+        .app { display: flex; flex-direction: column; height: 100vh; }
+
+        /* ── Topbar ──────────────────────────────────────────────────── */
+        .topbar { flex-shrink: 0; height: 46px; background: var(--bg3);
+            border-bottom: 1px solid var(--border); display: flex; align-items: center;
+            gap: 10px; padding: 0 12px; }
+        .brand { display: flex; align-items: center; gap: 9px; min-width: 0; }
+        .logo { width: 26px; height: 26px; flex-shrink: 0; border-radius: 6px;
+            background: linear-gradient(135deg, var(--green), var(--mint));
+            color: #0a0a0a; display: flex; align-items: center; justify-content: center;
+            font-weight: 800; font-size: 15px; }
+        .brand .bt { display: flex; flex-direction: column; line-height: 1.15; min-width: 0; }
+        .brand .bt b { font-size: 14px; color: var(--fg); }
+        .brand .bt i { font-size: 11px; color: var(--dim); font-style: normal;
+            white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
+
+        .tb-controls { margin-left: auto; display: flex; align-items: center; gap: 10px; }
+        .model-wrap { font-size: 11px; color: var(--dim); display: flex; align-items: center; gap: 5px; }
+        select#modelSel { background: var(--bg); color: var(--fg); border: 1px solid var(--border);
+            border-radius: 4px; padding: 4px 6px; font-size: 12px; max-width: 190px; outline: none; }
+        select#modelSel:focus { border-color: var(--blue); }
+        .switch { font-size: 11px; color: var(--dim); display: flex; align-items: center;
+            gap: 5px; cursor: pointer; user-select: none; white-space: nowrap; }
+        .switch input { accent-color: var(--green); cursor: pointer; }
+        .tb-btn { padding: 5px 11px; background: transparent; border: 1px solid var(--border);
+            color: var(--dim); cursor: pointer; border-radius: 4px; font-size: 12px;
+            transition: background .1s, color .1s; white-space: nowrap; }
+        .tb-btn:hover { background: var(--bg2); color: var(--fg); }
+
+        /* ── Conversation ────────────────────────────────────────────── */
+        .convo { flex: 1; overflow-y: auto; scroll-behavior: smooth;
+            scrollbar-width: thin; scrollbar-color: var(--border) transparent; }
+        .convo::-webkit-scrollbar { width: 8px; }
+        .convo::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
+        .inner { max-width: 880px; margin: 0 auto; padding: 18px 18px 30px; }
+
+        .turn { margin-bottom: 18px; }
+        .role { display: flex; align-items: center; gap: 7px; font-size: 11px;
+            font-weight: 700; letter-spacing: .04em; text-transform: uppercase;
+            color: var(--dim); margin-bottom: 7px; }
+        .role-badge { width: 18px; height: 18px; border-radius: 5px; display: flex;
+            align-items: center; justify-content: center; font-size: 11px; }
+        .role-badge.you { background: var(--bg4); color: var(--fg); }
+        .role-badge.ai  { background: linear-gradient(135deg, var(--green), var(--mint)); color: #0a0a0a; }
+
+        .turn-user .bubble { background: var(--bg2); border: 1px solid var(--border);
+            border-left: 3px solid var(--green); border-radius: 6px; padding: 10px 13px;
+            white-space: pre-wrap; word-break: break-word; line-height: 1.5; }
+
+        .a-body { line-height: 1.55; }
+        .explanation { white-space: pre-wrap; word-break: break-word; margin-bottom: 11px; }
+        .explanation:empty { display: none; }
+
+        /* thinking dots */
+        .thinking { display: flex; align-items: center; gap: 6px; color: var(--dim);
+            font-size: 13px; padding: 4px 0; }
+        .thinking .dot { width: 6px; height: 6px; border-radius: 50%; background: var(--green);
+            animation: bounce 1.2s infinite ease-in-out; }
+        .thinking .dot:nth-child(2) { animation-delay: .15s; }
+        .thinking .dot:nth-child(3) { animation-delay: .3s; }
+        @keyframes bounce { 0%,80%,100%{ transform: translateY(0); opacity:.4 } 40%{ transform: translateY(-5px); opacity:1 } }
+
+        /* ── Code card ───────────────────────────────────────────────── */
+        .code-card { border: 1px solid var(--border); border-radius: 8px; overflow: hidden;
+            background: #1b1b1b; margin-bottom: 10px; }
+        .code-head { display: flex; align-items: center; gap: 8px; padding: 6px 10px;
+            background: var(--bg3); border-bottom: 1px solid var(--border); }
+        .code-title { font-size: 11px; color: var(--dim); flex: 1; }
+        .code-title b { color: var(--mint); font-weight: 600; }
+        .code-actions { display: flex; gap: 6px; }
+        .ca-btn { padding: 3px 10px; background: transparent; border: 1px solid var(--border);
+            color: var(--dim); cursor: pointer; border-radius: 4px; font-size: 11px; white-space: nowrap; }
+        .ca-btn:hover { color: var(--fg); background: var(--bg2); }
+        .ca-btn.run { border-color: var(--green); color: var(--green); }
+        .ca-btn.run:hover { background: var(--green); color: #0a0a0a; }
+        .ca-btn:disabled { opacity: .45; cursor: default; }
+
+        pre.code { margin: 0; padding: 11px 13px; overflow-x: auto; font-size: 12.5px;
+            line-height: 1.6; tab-size: 4; scrollbar-width: thin; scrollbar-color: var(--border) transparent; }
+        pre.code::-webkit-scrollbar { height: 8px; }
+        pre.code::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
+        textarea.code-edit { width: 100%; min-height: 160px; border: none; resize: vertical;
+            background: #1b1b1b; color: var(--fg); padding: 11px 13px; font-size: 12.5px;
+            line-height: 1.6; outline: none; tab-size: 4; }
+
+        .tk-kw  { color: var(--blue); }
+        .tk-str { color: var(--orange); }
+        .tk-num { color: var(--numlit); }
+        .tk-com { color: var(--comment); font-style: italic; }
+        .tk-fn  { color: var(--yellow); }
+
+        /* ── Run output ──────────────────────────────────────────────── */
+        .run-area { margin-top: 2px; }
+        .run-head { display: flex; align-items: center; gap: 8px; margin: 9px 0 6px; }
+        .status { font-size: 11px; font-weight: 700; padding: 2px 9px; border-radius: 20px;
+            display: inline-flex; align-items: center; gap: 5px; }
+        .status.running { background: rgba(220,220,170,.12); color: var(--yellow); }
+        .status.ok      { background: rgba(78,201,78,.13);  color: var(--green); }
+        .status.err     { background: rgba(244,71,71,.13);  color: var(--red); }
+        .status .sd { width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
+        .status.running .sd { animation: blink 1s infinite; }
+        @keyframes blink { 0%,100%{opacity:1} 50%{opacity:.25} }
+        .run-meta { font-size: 11px; color: var(--dim); }
+
+        .out { border: 1px solid var(--border); border-radius: 6px; padding: 9px 12px;
+            margin-bottom: 7px; font-size: 12.5px; line-height: 1.55; overflow-x: auto;
+            white-space: pre-wrap; word-break: break-word; scrollbar-width: thin; }
+        .out-label { font-size: 10px; text-transform: uppercase; letter-spacing: .05em;
+            color: var(--dim); margin-bottom: 4px; }
+        .out.stdout { background: #161616; color: var(--fg); }
+        .out.logs   { background: var(--bg2); color: var(--yellow); }
+        .out.err    { background: rgba(244,71,71,.07); border-color: rgba(244,71,71,.4); color: #ff9a9a; }
+
+        .files { display: flex; flex-direction: column; gap: 7px; margin-top: 4px; }
+        .file-chip { display: flex; align-items: center; gap: 10px; background: var(--bg2);
+            border: 1px solid var(--border); border-left: 3px solid var(--mint);
+            border-radius: 6px; padding: 8px 11px; }
+        .file-chip.missing { border-left-color: var(--dim); opacity: .8; }
+        .fc-ico { font-size: 17px; }
+        .fc-info { flex: 1; min-width: 0; }
+        .fc-name { font-size: 12.5px; color: var(--fg); word-break: break-all; }
+        .fc-sub { font-size: 10.5px; color: var(--dim); }
+        .fc-actions { display: flex; gap: 6px; flex-shrink: 0; }
+        .fc-btn { padding: 4px 11px; border-radius: 5px; font-size: 11px; cursor: pointer;
+            border: 1px solid var(--border); text-decoration: none; white-space: nowrap;
+            color: var(--dim); background: transparent; }
+        .fc-btn:hover { color: var(--fg); background: var(--bg3); }
+        .fc-btn.dl { border-color: var(--mint); color: var(--mint); }
+        .fc-btn.dl:hover { background: var(--mint); color: #0a0a0a; }
+
+        .fix-btn { margin-top: 4px; padding: 5px 13px; background: transparent;
+            border: 1px solid var(--red); color: var(--red); cursor: pointer;
+            border-radius: 5px; font-size: 12px; }
+        .fix-btn:hover { background: var(--red); color: #fff; }
+
+        /* ── Empty state ─────────────────────────────────────────────── */
+        .empty { text-align: center; padding: 6vh 14px 20px; }
+        .empty-logo { width: 60px; height: 60px; margin: 0 auto 16px; border-radius: 15px;
+            background: linear-gradient(135deg, var(--green), var(--mint)); color: #0a0a0a;
+            display: flex; align-items: center; justify-content: center; font-size: 32px; font-weight: 800; }
+        .empty h1 { font-size: 24px; font-weight: 700; margin-bottom: 8px; }
+        .empty p { color: var(--dim); max-width: 480px; margin: 0 auto 20px; line-height: 1.55; }
+        .examples { display: grid; grid-template-columns: 1fr 1fr; gap: 9px;
+            max-width: 560px; margin: 0 auto; }
+        .ex { text-align: left; background: var(--bg2); border: 1px solid var(--border);
+            border-radius: 8px; padding: 11px 13px; cursor: pointer; color: var(--fg);
+            font-size: 12.5px; line-height: 1.4; transition: border-color .12s, background .12s; }
+        .ex:hover { border-color: var(--green); background: var(--bg3); }
+        .ex span { display: block; color: var(--dim); font-size: 11px; margin-top: 3px; }
+        .empty-note { margin-top: 22px; font-size: 11.5px; color: var(--dim); }
+        .empty-note a { color: var(--blue); cursor: pointer; text-decoration: none; }
+        .empty-note a:hover { text-decoration: underline; }
+
+        /* ── Composer ────────────────────────────────────────────────── */
+        .composer-wrap { flex-shrink: 0; border-top: 1px solid var(--border); background: var(--bg2); }
+        .composer { max-width: 880px; margin: 0 auto; display: flex; align-items: flex-end;
+            gap: 8px; padding: 10px 18px 4px; }
+        #ta { flex: 1; resize: none; background: var(--bg); color: var(--fg);
+            border: 1px solid var(--border); border-radius: 8px; padding: 10px 12px;
+            font-family: inherit; font-size: 14px; line-height: 1.45; outline: none;
+            max-height: 180px; overflow-y: auto; }
+        #ta:focus { border-color: var(--green); }
+        #ta::placeholder { color: var(--dim); }
+        #send { flex-shrink: 0; padding: 10px 16px; background: linear-gradient(135deg, var(--green), var(--mint));
+            border: none; color: #0a0a0a; font-weight: 700; cursor: pointer; border-radius: 8px;
+            font-size: 13px; }
+        #send:hover { filter: brightness(1.08); }
+        #send:disabled { opacity: .5; cursor: default; filter: none; }
+        .composer-hint { max-width: 880px; margin: 0 auto; padding: 2px 18px 9px;
+            font-size: 10.5px; color: var(--dim); text-align: center; }
+
+        /* ── Responsive ──────────────────────────────────────────────── */
+        @media (max-width: 720px) {
+            .brand .bt i { display: none; }
+            .inner { padding: 14px 12px 26px; }
+            .composer { padding: 9px 12px 4px; }
+            .composer-hint { padding: 2px 12px 8px; }
+            .examples { grid-template-columns: 1fr; }
+        }
+        @media (max-width: 540px) {
+            body { font-size: 13.5px; }
+            .topbar { gap: 6px; padding: 0 9px; }
+            .model-wrap { display: none; }
+            .tb-controls { gap: 7px; }
+            .switch { font-size: 11px; }
+            .brand .bt b { font-size: 13px; }
+            .code-actions .ca-btn { padding: 3px 8px; }
+            .empty h1 { font-size: 21px; }
+            #send { padding: 10px 13px; }
+            .fc-actions { flex-direction: column; }
+        }
+    </style>
+</head>
+<body>
+    <div class="app">
+
+        <!-- Topbar -->
+        <div class="topbar">
+            <div class="brand">
+                <div class="logo">&#10095;</div>
+                <div class="bt">
+                    <b>AGI Forge</b>
+                    <i>natural language &rarr; AGI script &rarr; result</i>
+                </div>
+            </div>
+            <div class="tb-controls">
+                <label class="model-wrap">model
+                    <select id="modelSel"><option value="">default</option></select>
+                </label>
+                <label class="switch"><input type="checkbox" id="autorun" checked> auto-run</label>
+                <button class="tb-btn" id="clearBtn" onclick="resetConvo()">New</button>
+            </div>
+        </div>
+
+        <!-- Conversation -->
+        <div class="convo" id="convo">
+            <div class="inner" id="inner">
+                <div class="empty" id="empty">
+                    <div class="empty-logo">&#10095;</div>
+                    <h1>What should we build?</h1>
+                    <p>Describe a task in plain language. AGI Forge writes an AGI script
+                       grounded in the live API reference, runs it on your ArozOS server,
+                       and hands back the output &mdash; including any file it generates.</p>
+                    <div class="examples" id="examples"></div>
+                    <div class="empty-note">
+                        Uses your configured AI model &middot;
+                        <a onclick="openAISettings()">Configure endpoint &amp; key</a>
+                    </div>
+                </div>
+            </div>
+        </div>
+
+        <!-- Composer -->
+        <div class="composer-wrap">
+            <div class="composer">
+                <textarea id="ta" rows="1" placeholder="Describe a task&hellip;  e.g. &ldquo;list the 10 largest files on my Desktop&rdquo;"
+                          autocomplete="off" spellcheck="false"></textarea>
+                <button id="send" onclick="onSend()">Send &#9654;</button>
+            </div>
+            <div class="composer-hint">
+                Enter to send &middot; Shift+Enter for a new line &middot;
+                generated scripts run on the server with your permissions
+            </div>
+        </div>
+
+    </div>
+
+    <script>
+    /* ════════════════════════════════════════════════════════════════════
+       AGI Forge — front-end controller
+       Pipeline:  prompt → generate.agi (LLM writes script) → run.agi (exec)
+       ════════════════════════════════════════════════════════════════════ */
+
+    var convo = [];          // [{role, content}] conversation history for grounding
+    var models = [];
+    var currentModel = "";
+    var busy = false;
+
+    var EXAMPLES = [
+        { t: "List the 10 largest files on my Desktop", s: "filelib · walk + sort" },
+        { t: "Write a text file to my Desktop containing today's date and a hello message", s: "filelib · writeFile + download" },
+        { t: "Show this server's CPU, RAM and disk usage", s: "sysinfo" },
+        { t: "Make a CSV of every image under user:/Photo with its file size", s: "filelib · CSV output file" }
+    ];
+
+    /* ── boot ──────────────────────────────────────────────────────────── */
+    $(document).ready(function () {
+        renderExamples();
+        loadModels();
+
+        var ta = document.getElementById("ta");
+        ta.addEventListener("input", autoGrow);
+        ta.addEventListener("keydown", function (e) {
+            if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); onSend(); }
+        });
+        document.getElementById("autorun").checked = true;
+        ta.focus();
+    });
+
+    function renderExamples() {
+        var box = document.getElementById("examples");
+        box.innerHTML = "";
+        EXAMPLES.forEach(function (ex) {
+            var b = document.createElement("button");
+            b.className = "ex";
+            b.innerHTML = escHtml(ex.t) + "<span>" + escHtml(ex.s) + "</span>";
+            b.onclick = function () {
+                var ta = document.getElementById("ta");
+                ta.value = ex.t; autoGrow(); onSend();
+            };
+            box.appendChild(b);
+        });
+    }
+
+    function loadModels() {
+        ao_module_agirun("AGIForge/backend/models.agi", {}, function (data) {
+            var resp = parseJSON(data);
+            var sel = document.getElementById("modelSel");
+            if (!resp || !resp.models || !resp.models.length) return;
+            models = resp.models;
+            currentModel = resp["default"] || "";
+            sel.innerHTML = "";
+            models.forEach(function (m) {
+                var o = document.createElement("option");
+                o.value = m; o.textContent = m;
+                if (m === currentModel) o.selected = true;
+                sel.appendChild(o);
+            });
+            sel.onchange = function () { currentModel = sel.value; };
+        }, function () { /* picker stays on "default" */ });
+    }
+
+    /* ── send a prompt ─────────────────────────────────────────────────── */
+    function onSend() {
+        var ta = document.getElementById("ta");
+        var text = ta.value.trim();
+        if (!text || busy) return;
+        ta.value = ""; autoGrow();
+        send(text);
+    }
+
+    function send(text) {
+        busy = true;
+        setComposer(false);
+        hideEmpty();
+        addUserTurn(text);
+
+        var a = addAssistantTurn();
+        var historySnapshot = convo.slice();
+
+        ao_module_agirun("AGIForge/backend/generate.agi",
+            {
+                prompt: text,
+                history: JSON.stringify(historySnapshot),
+                options: JSON.stringify({ model: currentModel })
+            },
+            function (data) {
+                var resp = parseJSON(data);
+                convo.push({ role: "user", content: text });
+                if (!resp || !resp.ok) {
+                    a.fail((resp && resp.error) ? resp.error : "The model did not return a script.", true);
+                    finishBusy();
+                    return;
+                }
+                // record assistant turn for follow-up grounding
+                // NOTE: the closing tags are written as "<\/...>" so this literal
+                // string does not prematurely terminate the inline <script> block.
+                convo.push({
+                    role: "assistant",
+                    content: "<explanation>\n" + (resp.explanation || "") +
+                             "\n<\/explanation>\n<script>\n" + (resp.script || "") + "\n<\/script>"
+                });
+                a.fill(resp.explanation || "", resp.script || "", resp.model || currentModel);
+                finishBusy();
+                if (resp.script && resp.script.trim() !== "" && document.getElementById("autorun").checked) {
+                    a.run();
+                }
+            },
+            function () {
+                convo.push({ role: "user", content: text });
+                a.fail("Could not reach the generator backend.", false);
+                finishBusy();
+            },
+            120000);
+    }
+
+    function finishBusy() { busy = false; setComposer(true); }
+
+    /* ── conversation rendering ────────────────────────────────────────── */
+    function addUserTurn(text) {
+        var t = el("div", "turn turn-user");
+        var role = el("div", "role");
+        role.innerHTML = '<span class="role-badge you">&#128100;</span> You';
+        var bubble = el("div", "bubble");
+        bubble.textContent = text;
+        t.appendChild(role); t.appendChild(bubble);
+        innerEl().appendChild(t);
+        scrollDown();
+    }
+
+    // Builds an assistant turn and returns handles to populate it.
+    function addAssistantTurn() {
+        var t = el("div", "turn turn-assistant");
+        var role = el("div", "role");
+        role.innerHTML = '<span class="role-badge ai">&#10095;</span> AGI Forge';
+        var body = el("div", "a-body");
+        body.innerHTML = '<div class="thinking"><span class="dot"></span><span class="dot"></span><span class="dot"></span> writing AGI script&hellip;</div>';
+        t.appendChild(role); t.appendChild(body);
+        innerEl().appendChild(t);
+        scrollDown();
+
+        var state = { script: "", editing: false, ran: false, codeEl: null, editEl: null, runArea: null };
+
+        function fail(msg, configHint) {
+            var h = '<div class="out err"><div class="out-label">Error</div>' + escHtml(msg) + '</div>';
+            if (configHint) {
+                h += '<div class="empty-note" style="text-align:left;margin-top:2px">' +
+                     'Make sure an AI endpoint and API key are set in ' +
+                     '<a onclick="openAISettings()">System Settings &rsaquo; Developer Options &rsaquo; AI Model</a>.</div>';
+            }
+            body.innerHTML = h;
+            scrollDown();
+        }
+
+        function fill(explanation, script, model) {
+            state.script = script;
+            body.innerHTML = "";
+
+            if (explanation) {
+                var ex = el("div", "explanation");
+                ex.innerHTML = nl2br(escHtml(explanation));
+                body.appendChild(ex);
+            }
+
+            if (!script || script.trim() === "") {
+                var none = el("div", "out logs");
+                none.textContent = "The model did not produce a runnable script.";
+                body.appendChild(none);
+                scrollDown();
+                return;
+            }
+
+            // Code card
+            var card = el("div", "code-card");
+            var head = el("div", "code-head");
+            var title = el("span", "code-title");
+            title.innerHTML = '&#9217; <b>generated.agi</b>' + (model ? '  &middot; ' + escHtml(model) : '');
+            var actions = el("div", "code-actions");
+
+            var copyBtn = mkBtn("ca-btn", "Copy", function () { copyText(state.script, copyBtn); });
+            var editBtn = mkBtn("ca-btn", "Edit", function () { toggleEdit(editBtn); });
+            var runBtn  = mkBtn("ca-btn run", "&#9654; Run", function () { run(); });
+            state.runBtn = runBtn;
+            actions.appendChild(copyBtn); actions.appendChild(editBtn); actions.appendChild(runBtn);
+            head.appendChild(title); head.appendChild(actions);
+
+            var pre = el("pre", "code mono");
+            var codeEl = document.createElement("code");
+            codeEl.innerHTML = highlightJS(script);
+            pre.appendChild(codeEl);
+            state.codeEl = codeEl;
+
+            var edit = document.createElement("textarea");
+            edit.className = "code-edit mono";
+            edit.style.display = "none";
+            edit.value = script;
+            edit.addEventListener("input", function () { state.script = edit.value; });
+            state.editEl = edit;
+
+            card.appendChild(head); card.appendChild(pre); card.appendChild(edit);
+            body.appendChild(card);
+
+            var runArea = el("div", "run-area");
+            body.appendChild(runArea);
+            state.runArea = runArea;
+
+            scrollDown();
+        }
+
+        function toggleEdit(btn) {
+            state.editing = !state.editing;
+            if (state.editing) {
+                state.editEl.value = state.script;
+                state.editEl.style.display = "block";
+                state.codeEl.parentNode.style.display = "none";
+                btn.textContent = "Done";
+                state.editEl.focus();
+            } else {
+                state.script = state.editEl.value;
+                state.codeEl.innerHTML = highlightJS(state.script);
+                state.editEl.style.display = "none";
+                state.codeEl.parentNode.style.display = "block";
+                btn.textContent = "Edit";
+            }
+        }
+
+        function run() {
+            if (state.editing) { state.script = state.editEl.value; }
+            if (!state.script || state.script.trim() === "") return;
+            if (state.runBtn) { state.runBtn.disabled = true; }
+
+            var ra = state.runArea;
+            ra.innerHTML = '<div class="run-head"><span class="status running"><span class="sd"></span>Running</span></div>';
+            scrollDown();
+            var started = Date.now();
+
+            ao_module_agirun("AGIForge/backend/run.agi", { script: state.script },
+                function (data) {
+                    var r = parseJSON(data);
+                    if (!r) { r = { error: true, errorMsg: "Unreadable response from runner", output: "", logs: [], files: [] }; }
+                    renderRun(ra, r, Date.now() - started);
+                    if (state.runBtn) { state.runBtn.disabled = false; state.runBtn.innerHTML = "&#10227; Re-run"; }
+                },
+                function () {
+                    renderRun(ra, { error: true, errorMsg: "Runner backend unreachable", output: "", logs: [], files: [] }, Date.now() - started);
+                    if (state.runBtn) { state.runBtn.disabled = false; }
+                },
+                120000);
+        }
+
+        return { fail: fail, fill: fill, run: run };
+    }
+
+    function renderRun(ra, r, ms) {
+        ra.innerHTML = "";
+        var isErr = !!r.error;
+
+        var head = el("div", "run-head");
+        var st = el("span", "status " + (isErr ? "err" : "ok"));
+        st.innerHTML = '<span class="sd"></span>' + (isErr ? "Failed" : "Done");
+        head.appendChild(st);
+        var meta = el("span", "run-meta");
+        meta.textContent = (ms >= 0 ? (ms < 1000 ? ms + " ms" : (ms / 1000).toFixed(1) + " s") : "");
+        head.appendChild(meta);
+        ra.appendChild(head);
+
+        // stdout / response
+        if (r.output && ("" + r.output).trim() !== "") {
+            var o = el("div", "out stdout");
+            o.innerHTML = '<div class="out-label">Output</div>';
+            var pre = document.createElement("span");
+            pre.className = "mono";
+            pre.textContent = r.output;
+            o.appendChild(pre);
+            ra.appendChild(o);
+        }
+
+        // console logs
+        if (r.logs && r.logs.length) {
+            var lg = el("div", "out logs mono");
+            lg.innerHTML = '<div class="out-label" style="font-family:-apple-system,sans-serif">Console</div>';
+            r.logs.forEach(function (l) {
+                var line = document.createElement("div");
+                line.textContent = "· " + l;
+                lg.appendChild(line);
+            });
+            ra.appendChild(lg);
+        }
+
+        // error
+        if (isErr) {
+            var e = el("div", "out err");
+            e.innerHTML = '<div class="out-label">Error</div>';
+            var em = document.createElement("span");
+            em.className = "mono";
+            em.textContent = r.errorMsg || "Script failed";
+            e.appendChild(em);
+            ra.appendChild(e);
+
+            var fix = mkBtn("fix-btn", "&#10024; Ask AI to fix it", function () {
+                if (busy) return;
+                send("The AGI script you just gave me failed when I ran it. The error was:\n\n" +
+                     (r.errorMsg || "(unknown error)") +
+                     "\n\nPlease return a corrected, complete script.");
+            });
+            ra.appendChild(fix);
+        }
+
+        // generated files
+        if (r.files && r.files.length) {
+            var fwrap = el("div", "files");
+            r.files.forEach(function (f) { fwrap.appendChild(buildFileChip(f)); });
+            ra.appendChild(fwrap);
+        }
+
+        if (!isErr && (!r.output || ("" + r.output).trim() === "") &&
+            (!r.logs || !r.logs.length) && (!r.files || !r.files.length)) {
+            var none = el("div", "out logs");
+            none.textContent = "Script finished with no output.";
+            ra.appendChild(none);
+        }
+        scrollDown();
+    }
+
+    function buildFileChip(f) {
+        var chip = el("div", "file-chip" + (f.exists ? "" : " missing"));
+        var ico = el("div", "fc-ico"); ico.textContent = f.exists ? "📄" : "⚠";
+        var info = el("div", "fc-info");
+        var name = el("div", "fc-name"); name.textContent = f.name || f.path;
+        var sub = el("div", "fc-sub");
+        sub.textContent = f.exists
+            ? (f.path + (f.size >= 0 ? "  ·  " + formatBytes(f.size) : ""))
+            : (f.path + "  ·  not found on disk");
+        info.appendChild(name); info.appendChild(sub);
+        chip.appendChild(ico); chip.appendChild(info);
+
+        if (f.exists) {
+            var actions = el("div", "fc-actions");
+            var dl = document.createElement("a");
+            dl.className = "fc-btn dl";
+            dl.innerHTML = "&#11015; Download";
+            dl.href = (ao_root || "/") + "media/download/?file=" + encodeURIComponent(f.path);
+            dl.setAttribute("download", f.name || "");
+            dl.target = "_blank";
+            actions.appendChild(dl);
+
+            var open = mkBtn("fc-btn", "Open in Files", function () {
+                var slash = f.path.lastIndexOf("/");
+                var dir = slash >= 0 ? f.path.substring(0, slash) : f.path;
+                try { ao_module_openPath(dir, f.name); } catch (e) { }
+            });
+            actions.appendChild(open);
+            chip.appendChild(actions);
+        }
+        return chip;
+    }
+
+    /* ── controls ──────────────────────────────────────────────────────── */
+    function resetConvo() {
+        if (busy) return;
+        convo = [];
+        innerEl().innerHTML = "";
+        var empty = el("div", "empty");
+        empty.id = "empty";
+        empty.innerHTML =
+            '<div class="empty-logo">&#10095;</div>' +
+            '<h1>What should we build?</h1>' +
+            '<p>Describe a task in plain language. AGI Forge writes an AGI script grounded in the live ' +
+            'API reference, runs it on your ArozOS server, and hands back the output &mdash; including any file it generates.</p>' +
+            '<div class="examples" id="examples"></div>' +
+            '<div class="empty-note">Uses your configured AI model &middot; ' +
+            '<a onclick="openAISettings()">Configure endpoint &amp; key</a></div>';
+        innerEl().appendChild(empty);
+        renderExamples();
+        document.getElementById("ta").focus();
+    }
+
+    function openAISettings() {
+        try {
+            ao_module_openSetting("Developer Options", "AI Model");
+        } catch (e) {
+            alert("Open System Settings › Developer Options › AI Model to set the AI endpoint, API key and default model.");
+        }
+    }
+
+    /* ── small helpers ─────────────────────────────────────────────────── */
+    function el(tag, cls) { var e = document.createElement(tag); if (cls) e.className = cls; return e; }
+    function mkBtn(cls, html, fn) { var b = el("button", cls); b.innerHTML = html; b.onclick = fn; return b; }
+    function innerEl() { return document.getElementById("inner"); }
+    function hideEmpty() { var e = document.getElementById("empty"); if (e) e.parentNode.removeChild(e); }
+    function scrollDown() { var c = document.getElementById("convo"); c.scrollTop = c.scrollHeight; }
+    function setComposer(on) {
+        document.getElementById("send").disabled = !on;
+        document.getElementById("ta").disabled = !on;
+        if (on) document.getElementById("ta").focus();
+    }
+    function autoGrow() {
+        var ta = document.getElementById("ta");
+        ta.style.height = "auto";
+        ta.style.height = Math.min(ta.scrollHeight, 180) + "px";
+    }
+    function parseJSON(d) { try { return (typeof d === "string") ? JSON.parse(d) : d; } catch (e) { return null; } }
+    function escHtml(s) {
+        return String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;")
+            .replace(/>/g, "&gt;").replace(/"/g, "&quot;");
+    }
+    function nl2br(s) { return s.replace(/\n/g, "<br>"); }
+    function copyText(text, btn) {
+        var done = function () { var o = btn.textContent; btn.textContent = "Copied"; setTimeout(function () { btn.textContent = o; }, 1200); };
+        if (navigator.clipboard && navigator.clipboard.writeText) {
+            navigator.clipboard.writeText(text).then(done, function () { fallbackCopy(text); done(); });
+        } else { fallbackCopy(text); done(); }
+    }
+    function fallbackCopy(text) {
+        var ta = document.createElement("textarea"); ta.value = text;
+        document.body.appendChild(ta); ta.select();
+        try { document.execCommand("copy"); } catch (e) { }
+        document.body.removeChild(ta);
+    }
+    function formatBytes(b) {
+        if (b < 0) return "";
+        if (b < 1024) return b + " B";
+        var u = ["KB", "MB", "GB", "TB"], i = -1;
+        do { b /= 1024; i++; } while (b >= 1024 && i < u.length - 1);
+        return b.toFixed(1) + " " + u[i];
+    }
+
+    /* ── minimal, safe JS syntax highlighter ───────────────────────────── */
+    function highlightJS(code) {
+        try {
+            var kw = /^(?:var|function|return|if|else|for|while|do|switch|case|default|break|continue|new|typeof|instanceof|in|this|null|true|false|undefined|try|catch|finally|throw|delete|void)$/;
+            var re = /(\/\/[^\n]*|\/\*[\s\S]*?\*\/)|('(?:\\.|[^'\\])*'|"(?:\\.|[^"\\])*")|(\b\d+(?:\.\d+)?\b)|([A-Za-z_$][A-Za-z0-9_$]*)/g;
+            var out = "", last = 0, m;
+            while ((m = re.exec(code)) !== null) {
+                out += escHtml(code.slice(last, m.index));
+                var tok = m[0];
+                if (m[1]) { out += '<span class="tk-com">' + escHtml(tok) + '</span>'; }
+                else if (m[2]) { out += '<span class="tk-str">' + escHtml(tok) + '</span>'; }
+                else if (m[3]) { out += '<span class="tk-num">' + escHtml(tok) + '</span>'; }
+                else if (m[4]) {
+                    if (kw.test(tok)) { out += '<span class="tk-kw">' + escHtml(tok) + '</span>'; }
+                    else if (/^\s*\(/.test(code.slice(re.lastIndex))) { out += '<span class="tk-fn">' + escHtml(tok) + '</span>'; }
+                    else { out += escHtml(tok); }
+                } else { out += escHtml(tok); }
+                last = re.lastIndex;
+            }
+            out += escHtml(code.slice(last));
+            return out;
+        } catch (e) { return escHtml(code); }
+    }
+    </script>
+</body>
+</html>

+ 33 - 0
src/web/AGIForge/init.agi

@@ -0,0 +1,33 @@
+/*
+    AGI Forge — Module Registration
+    ===============================
+    A terminal-style copilot for ArozOS. Describe a task in plain language and
+    AGI Forge asks the configured AI model (see System Settings > Developer
+    Options > AI Model) to write an AGI script for it — using the live AGI API
+    reference and the list of installed modules as context — then runs the
+    script in a sandboxed VM and hands back the output (and any generated file).
+
+    Backend scripts (next to this init.agi):
+        backend/generate.agi   natural language -> AGI script via aimodel
+        backend/run.agi        executes a generated script, captures output
+        backend/models.agi     lists the available models for the picker
+*/
+
+newDBTableIfNotExists("AGIForge");
+
+var moduleLaunchInfo = {
+    Name: "AGI Forge",
+    Desc: "Describe a task in plain words — AI writes an AGI script, runs it, and hands you the result.",
+    Group: "Development",
+    IconPath: "AGIForge/img/icon.svg",
+    Version: "1.0",
+    StartDir: "AGIForge/index.html",
+    SupportFW: true,
+    LaunchFWDir: "AGIForge/index.html",
+    InitFWSize: [960, 700],
+    SupportEmb: false,
+    SupportedExt: []
+};
+
+console.log("Registering AGI Forge module");
+registerModule(JSON.stringify(moduleLaunchInfo));