@krmxd/onegpt 0.1.1 → 0.1.5-beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -34,6 +34,13 @@ installed as **`ogpt-node`** (same app, unshadowable name).
34
34
 
35
35
  - 24 built-in tools: read/write/edit files, run Python & Node code,
36
36
  install packages (pip/npm), call HTTP APIs, git, web search & fetch
37
+ - **Self-installing**: missing packages - including **Ollama** itself if the
38
+ device doesn't have it - install quietly behind a single
39
+ `Installing packages...` line; never raw installer logs
40
+ (`OGPT_NO_AUTO_INSTALL=1` disables)
41
+ - **Smart Project Mode**: "build me a portfolio website" → it plans, writes
42
+ complete HTML/CSS/JS (never stubs), verifies the files exist and only then
43
+ reports done - lazy one-file answers get auto-corrected
37
44
  - Permission prompts only for genuinely risky actions (shell, installs,
38
45
  git commit) - everything else just works
39
46
  - Token dashboard with live tok/s charts at `http://127.0.0.1:8756` (`/dash`)
package/bin/ogpt.js CHANGED
@@ -8,14 +8,13 @@ if (maj < 18) {
8
8
  process.exit(1);
9
9
  }
10
10
 
11
- let CLI, getConfig;
11
+ let CLI, getConfig, VERSION_FULL;
12
12
  try {
13
- ({ CLI } = require("../src/cli"));
13
+ ({ CLI, VERSION_FULL } = require("../src/cli"));
14
14
  ({ getConfig } = require("../src/config"));
15
15
  } catch (e) {
16
16
  console.error(`OGPT failed to start: ${e.message}`);
17
- if (e.code === "MODULE_NOT_FOUND") {
18
- console.error(`
17
+ if (e.code === "MODULE_NOT_FOUND") { console.error(`
19
18
  The installation looks broken or out of date. Fix it with:
20
19
 
21
20
  npm cache clean --force
@@ -33,6 +32,10 @@ Or run straight from the repo without installing:
33
32
  process.exit(1);
34
33
  }
35
34
 
35
+ // Silent dependency self-heal + Ollama install - all hidden behind a single
36
+ // 'Installing packages...' line; no-op when everything is already present.
37
+ try { require("../src/bootstrap").ensureBoot(); } catch {}
38
+
36
39
  async function main() {
37
40
  const args = process.argv.slice(2);
38
41
  const cfg = getConfig();
@@ -57,7 +60,7 @@ async function main() {
57
60
  const name = args[++i];
58
61
  cfg.set("active_model", cfg.resolveModel(name));
59
62
  } else if (args[i] === "--version" || args[i] === "-v") {
60
- console.log("ogpt v1.1.0 (by KareemXD)");
63
+ console.log(`ogpt v${VERSION_FULL} (by KareemXD)`);
61
64
  process.exit(0);
62
65
  } else if (args[i] === "--help") {
63
66
  console.log(`Usage: ogpt [options] [prompt]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@krmxd/onegpt",
3
- "version": "0.1.1",
3
+ "version": "0.1.5-beta",
4
4
  "description": "OGPT - AI coding assistant for the terminal with a built-in local engine",
5
5
  "main": "src/index.js",
6
6
  "bin": {
package/src/agent.js CHANGED
@@ -104,7 +104,9 @@ const TOOL_GUIDANCE = (
104
104
  const TASK_RE = new RegExp(
105
105
  "\\b(create|make|build|write|add|generate|scaffold|implement|fix|update)\\b" +
106
106
  "[^\\n]{0,120}?\\b(file|files|folder|folders|directory|dir|script|module|" +
107
- "app|component|config|readme|tests?)\\b", "i");
107
+ "app|component|config|readme|tests?|website|web\\s?site|web\\s?app(?:lication)?|" +
108
+ "landing(?:\\s?page)?|portfolio|blog|dashboard|homepage|page|site|game|" +
109
+ "calculator|quiz|\\bbot\\b|\\bapi\\b|server|tool|cli|program|project)\\b", "i");
108
110
  const QUESTION_RE = new RegExp(
109
111
  "^\\s*(how|what|why|when|where|which|who|whom|can you (?:tell|explain)|" +
110
112
  "explain|describe|list|is|are|does|do)\\b", "i");
@@ -185,13 +187,62 @@ function stalledHalfway(reply, userInput, doneOutputs) {
185
187
  if (!t || t.length > 160 || t.includes("```")) return false;
186
188
  if (!ANNOUNCE_RE.test(t)) return false;
187
189
  const bits = (doneOutputs || []).join(" ").toLowerCase();
188
- return namedFiles(userInput).some((n) => {
190
+ let expected = namedFiles(userInput);
191
+ // A web-build brief demands the standard trio - hold the model to what
192
+ // PROJECT MODE promised ("make me a portfolio" => all three files).
193
+ if (projectBrief(userInput) === PROJECT_WEB_BRIEF) {
194
+ const have = new Set(expected.map((n) => n.toLowerCase()));
195
+ for (const f of ["index.html", "styles.css", "script.js"]) {
196
+ if (!have.has(f)) expected = expected.concat([f]);
197
+ }
198
+ }
199
+ return expected.some((n) => {
189
200
  const base = path.basename(n).toLowerCase();
190
201
  if (base && bits.includes(base)) return false;
191
202
  return !artifactExists(n);
192
203
  });
193
204
  }
194
205
 
206
+ const PROJECT_VERB_RE = /\b(?:build|create|make|write|start|scaffold|develop|design|code)\b/i;
207
+ const PROJECT_WEB_NOUN_RE = /\b(?:web\s?site|web\s?app(?:lication)?|landing(?:\s?page)?|portfolio|blog|dashboard|homepage|front[\s-]?end|game|calculator|to-?do\s?(?:list|app)|quiz)\b/i;
208
+ const PROJECT_CODE_NOUN_RE = /\b(?:project|application|\bapp\b|\bcli\b|\btool\b|script\b|\bbot\b|\bapi\b|server|program|package|library|automation)\b/i;
209
+
210
+ const PROJECT_WEB_BRIEF = `
211
+
212
+ ### PROJECT MODE - WEB BUILD
213
+ The user asked you to build a web project. Deliver a COMPLETE working site:
214
+ 1. PLAN first - list every file you will create (2-4 lines max).
215
+ 2. Create ALL of these in a project folder: index.html, styles.css, script.js.
216
+ 3. Quality bar (mandatory):
217
+ - index.html: semantic HTML5 with viewport meta, linking styles.css and script.js
218
+ - styles.css: modern design - custom properties, flexbox/grid layout, responsive media queries, hover/focus states (aim for 80+ lines)
219
+ - script.js: real interactivity wired to actual element ids from your HTML (aim for 40+ lines)
220
+ 4. Zero placeholders ("TODO", "content here", "..."). Every button and link does something real.
221
+ 5. VERIFY each file exists (read_file/list_files), then summarize how to open it.
222
+ Do NOT say done until every file from your plan exists on disk.`;
223
+
224
+ const PROJECT_CODE_BRIEF = `
225
+
226
+ ### PROJECT MODE - BUILD
227
+ The user asked you to build something. Process:
228
+ 1. PLAN first - list the files you will create.
229
+ 2. Write every file completely with real working code - no TODO placeholders.
230
+ 3. Include a README.md with run instructions.
231
+ 4. TEST your work: run the code (run_python/run_node/terminal) and fix any errors before finishing.
232
+ 5. Summarize what you built and how to run it.
233
+ Do NOT say done until the code runs without errors.`;
234
+
235
+ // Extra system-prompt guidance when the request is 'build me a ...'.
236
+ function projectBrief(userInput) {
237
+ const text = userInput || "";
238
+ if (!PROJECT_VERB_RE.test(text)) return "";
239
+ // Questions ABOUT building are not build orders ("how do I make a game?").
240
+ if (/^\s*(?:how|what|why|when|where|which|who|can\s+you\s+(?:tell|explain)|explain|describe)\b/i.test(text)) return "";
241
+ if (PROJECT_WEB_NOUN_RE.test(text)) return PROJECT_WEB_BRIEF;
242
+ if (PROJECT_CODE_NOUN_RE.test(text)) return PROJECT_CODE_BRIEF;
243
+ return "";
244
+ }
245
+
195
246
  function expandHome(p) {
196
247
  return String(p).replace(/^~(?=$|\/)/, os.homedir());
197
248
  }
@@ -226,15 +277,73 @@ function callSig(calls) {
226
277
 
227
278
  // Catch tool calls the model printed as bare JSON text instead of using the
228
279
  // native tool_calls channel (very common with tiny models).
280
+ // Salvage a malformed tool-call blob. Tiny local models constantly emit
281
+ // invalid JSON - most often unescaped quotes inside string values
282
+ // ("content": "<div class="hero">") - which strict parsing rejects and the
283
+ // call silently becomes chat noise. Field-wise extraction does not care.
284
+ function repairToolJson(raw) {
285
+ const nameM = raw.match(/"name"\s*:\s*"([A-Za-z_][\w]*)"/);
286
+ if (!nameM) return null;
287
+ const argIdx = raw.indexOf('"arguments"');
288
+ if (argIdx === -1) return null;
289
+ let body = raw.slice(argIdx + '"arguments"'.length);
290
+ const braceAt = body.indexOf("{");
291
+ if (braceAt !== -1 && !body.slice(0, braceAt).includes(":")) body = body.slice(braceAt + 1);
292
+
293
+ const keyRe = /"([A-Za-z_]\w*)"\s*:\s*/g;
294
+ const marks = [];
295
+ let m;
296
+ while ((m = keyRe.exec(body)) !== null) marks.push({ key: m[1], vs: m.index + m[0].length });
297
+ const args = {};
298
+ for (let i = 0; i < marks.length; i++) {
299
+ const k = marks[i];
300
+ if (k.key === "arguments") continue; // the wrapper key itself
301
+ const next = marks.find((x, j) => j > i && x.key !== "arguments");
302
+ let v;
303
+ if (next) {
304
+ // Value runs until just before the next key's opening quote.
305
+ const nq = body.lastIndexOf('"', Math.max(0, next.vs - next.key.length - 2));
306
+ v = body.slice(k.vs, nq > k.vs ? nq : undefined);
307
+ v = v.replace(/\s*$/, "").replace(/,\s*$/, "");
308
+ if (v.startsWith('"')) v = v.slice(1);
309
+ if (v.endsWith('"')) v = v.slice(0, -1);
310
+ } else {
311
+ // Last value: everything up to the closing brace(s) of the blob.
312
+ v = body.slice(k.vs).replace(/\s*\}\s*\}\s*$/, "").replace(/\s*\}\s*$/, "");
313
+ if (v.endsWith(",")) v = v.slice(0, -1);
314
+ if (v.startsWith('"')) v = v.slice(1);
315
+ if (v.endsWith('"')) v = v.slice(0, -1);
316
+ }
317
+ if (v.startsWith("[") && v.endsWith("]")) {
318
+ try { args[k.key] = JSON.parse(v); continue; }
319
+ catch {}
320
+ args[k.key] = v.slice(1, -1).split(",").map((s) => s.trim().replace(/^["']|["']$/g, "")).filter(Boolean);
321
+ continue;
322
+ }
323
+ args[k.key] = v;
324
+ }
325
+ if (!Object.keys(args).length) return null;
326
+ return {
327
+ id: `tc_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
328
+ name: nameM[1],
329
+ arguments: args,
330
+ };
331
+ }
332
+
229
333
  function extractJsonTools(text) {
230
334
  const calls = [];
231
335
  const spans = [];
232
336
  if (!text || text.indexOf('"arguments"') === -1) return { clean: text || "", calls };
337
+ // Fenced blobs (```json ... ```) - strip the fence markers so the objects
338
+ // inside become scannable. Python's scanner does this natively.
339
+ const src = text.includes("```")
340
+ ? text.replace(/```[a-zA-Z]*\n?/g, "\n")
341
+ : text;
233
342
  let i = 0;
234
- while ((i = text.indexOf("{", i)) !== -1 && calls.length < 8) {
343
+ while ((i = src.indexOf("{", i)) !== -1 && calls.length < 8) {
235
344
  let depth = 0, inStr = false, esc = false;
236
- for (let j = i; j < text.length && j < i + 20000; j++) {
237
- const c = text[j];
345
+ for (let j = i; j < src.length && j < i + 20000; j++) {
346
+ const c = src[j];
238
347
  if (inStr) {
239
348
  if (esc) esc = false;
240
349
  else if (c === "\\") esc = true;
@@ -247,7 +356,7 @@ function extractJsonTools(text) {
247
356
  depth--;
248
357
  if (depth === 0) {
249
358
  try {
250
- const obj = JSON.parse(text.slice(i, j + 1));
359
+ const obj = JSON.parse(src.slice(i, j + 1));
251
360
  if (obj && typeof obj.name === "string"
252
361
  && obj.arguments && typeof obj.arguments === "object"
253
362
  && !Array.isArray(obj.arguments)) {
@@ -266,13 +375,17 @@ function extractJsonTools(text) {
266
375
  }
267
376
  i++;
268
377
  }
378
+ if (!calls.length && /"name"\s*:/.test(src) && text.includes('"arguments"')) {
379
+ const rep = repairToolJson(src);
380
+ if (rep) return { clean: "", calls: [rep] };
381
+ }
269
382
  let clean = "";
270
383
  let pos = 0;
271
384
  for (const [s, e] of spans) {
272
- clean += text.slice(pos, s);
385
+ clean += src.slice(pos, s);
273
386
  pos = e;
274
387
  }
275
- clean += text.slice(pos);
388
+ clean += src.slice(pos);
276
389
  return { clean, calls };
277
390
  }
278
391
 
@@ -365,11 +478,13 @@ class Agent {
365
478
  this.lastRun = {};
366
479
  }
367
480
 
368
- _systemPrompt() {
481
+ _systemPrompt(userInput) {
369
482
  // The base prompt may contain an {ogpt_model} placeholder that is replaced
370
483
  // with the active model's oGPT display name (e.g. oGPT-1a). The identity
371
484
  // block is always appended so the assistant never exposes the underlying
372
- // engine, even if an older prompt is set in the user's config.
485
+ // engine, even if an older prompt is set in the user's config. When the
486
+ // request looks like a build-a-project task, a PROJECT MODE brief is
487
+ // appended for this turn only.
373
488
  const base =
374
489
  this.cfg.get("agent.system_prompt", "") ||
375
490
  "You are OGPT, an expert AI coding assistant running as {ogpt_model}.";
@@ -382,6 +497,7 @@ class Agent {
382
497
  base.split("{ogpt_model}").join(name)
383
498
  + IDENTITY_BLOCK.split("{name}").join(name)
384
499
  + TOOL_GUIDANCE.split("{cwd}").join(cwd)
500
+ + projectBrief(userInput)
385
501
  );
386
502
  }
387
503
 
@@ -444,7 +560,7 @@ class Agent {
444
560
  const model = this.cfg.activeModel();
445
561
  const resp = await this.provider.chat(this.history, model, {
446
562
  tools: this.tools.definitions(),
447
- system: this._systemPrompt(),
563
+ system: this._systemPrompt(userInput),
448
564
  temperature: this.temperature,
449
565
  maxTokens: this.maxTokens,
450
566
  });
@@ -560,11 +676,12 @@ class Agent {
560
676
  const contentParts = [];
561
677
  const toolCalls = [];
562
678
  let usage = { prompt: 0, completion: 0, total: 0 };
679
+ let jsonHold = false;
563
680
 
564
681
  try {
565
682
  for await (const chunk of this.provider.stream(this.history, model, {
566
683
  tools: this.tools.definitions(),
567
- system: this._systemPrompt(),
684
+ system: this._systemPrompt(userInput),
568
685
  temperature: this.temperature,
569
686
  maxTokens: this.maxTokens,
570
687
  })) {
@@ -573,7 +690,14 @@ class Agent {
573
690
  if (!ttft) ttft = Date.now() - tStart;
574
691
  nTokens++;
575
692
  sawText = true;
576
- yield chunk.text;
693
+ // A tool-call blob typed as plain text must not scroll into the
694
+ // chat as garbage - hold it back; the executor prints chips.
695
+ const acc = contentParts.join("");
696
+ if (!jsonHold && ((/^\s*\{/.test(acc) && acc.includes('"name"'))
697
+ || (/^\s*```/.test(acc) && (acc.includes('"name"') || acc.includes('"arguments"'))))) {
698
+ jsonHold = true;
699
+ }
700
+ if (!jsonHold) yield chunk.text;
577
701
  } else if (chunk.type === "tool") {
578
702
  toolCalls.push(chunk);
579
703
  } else if (chunk.type === "usage") {
@@ -760,4 +884,4 @@ class Agent {
760
884
  }
761
885
  }
762
886
 
763
- module.exports = { Agent };
887
+ module.exports = { Agent, extractJsonTools, repairToolJson };
@@ -0,0 +1,102 @@
1
+ "use strict";
2
+
3
+ // Boot-time dependency self-healing (parity with the Python build).
4
+ //
5
+ // - PACKAGES: runtime npm deps (empty today -> instant no-op). List entries
6
+ // as [require-name, npm-name] and first launch installs them quietly.
7
+ // - Ollama: if the local-engine binary is missing, the official installer
8
+ // runs hidden. Never touched when already present.
9
+ //
10
+ // The user only ever sees one line: 'Installing packages...'.
11
+ // Set OGPT_NO_AUTO_INSTALL=1 to disable (CI, tests, offline devices).
12
+
13
+ const { spawnSync } = require("child_process");
14
+ const fs = require("fs");
15
+ const path = require("path");
16
+
17
+ const PACKAGES = [];
18
+
19
+ function missingPackages() {
20
+ return PACKAGES.filter(([req]) => {
21
+ try { require.resolve(req); return false; } catch { return true; }
22
+ });
23
+ }
24
+
25
+ function haveOllama() {
26
+ if (spawnSync("ollama", ["--version"],
27
+ { stdio: "ignore", timeout: 15000 }).status === 0) return true;
28
+ // same-process PATH additions (fresh install) + common locations
29
+ for (const dir of ["/usr/local/bin", "/usr/bin", "/opt/homebrew/bin",
30
+ path.join(process.env.HOME || "", ".local/bin")]) {
31
+ try {
32
+ if (dir && fs.existsSync(path.join(dir, "ollama"))) {
33
+ process.env.PATH = dir + path.delimiter + process.env.PATH;
34
+ return true;
35
+ }
36
+ } catch {}
37
+ }
38
+ if (process.platform === "win32") {
39
+ for (const dir of ["C:\\Program Files\\Ollama",
40
+ path.join(process.env.LOCALAPPDATA || "", "Programs", "Ollama")]) {
41
+ try { if (dir && fs.existsSync(path.join(dir, "ollama.exe"))) return true; }
42
+ catch {}
43
+ }
44
+ }
45
+ return false;
46
+ }
47
+
48
+ function npmInstall(pkgs, cwd, timeout = 600000) {
49
+ try {
50
+ const r = spawnSync("npm", ["install", "--no-fund", "--no-audit",
51
+ "--silent", ...pkgs],
52
+ { cwd: cwd ? path.resolve(cwd) : undefined, stdio: "ignore", timeout });
53
+ return !r.error && r.status === 0;
54
+ } catch { return false; }
55
+ }
56
+
57
+ function installOllama(timeout = 1200000) {
58
+ if (process.platform === "win32") {
59
+ try {
60
+ spawnSync("winget", ["install", "--silent",
61
+ "--accept-package-agreements", "--accept-source-agreements",
62
+ "Ollama.Ollama"], { stdio: "ignore", timeout, shell: true });
63
+ } catch {}
64
+ return haveOllama();
65
+ }
66
+ const has = (c) => {
67
+ try { return spawnSync(c, ["--version"], { stdio: "ignore" }).status === 0; }
68
+ catch { return false; }
69
+ };
70
+ let script = null;
71
+ if (has("curl")) script = "curl -fsSL https://ollama.com/install.sh | sh";
72
+ else if (has("wget")) script = "wget -qO- https://ollama.com/install.sh | sh";
73
+ if (!script) return false;
74
+ try {
75
+ spawnSync("sh", ["-c", script], { stdio: "ignore", timeout });
76
+ } catch {}
77
+ return haveOllama();
78
+ }
79
+
80
+ function ensureBoot() {
81
+ if (process.env.OGPT_NO_AUTO_INSTALL) return [];
82
+ const need = missingPackages();
83
+ const wantOllama = !haveOllama();
84
+ if (!need.length && !wantOllama) return [];
85
+ console.log("Installing packages...");
86
+ if (need.length) npmInstall(need.map(([, pkg]) => pkg));
87
+ if (wantOllama) installOllama();
88
+ return missingPackages().map(([, pkg]) => pkg);
89
+ }
90
+
91
+ // Back-compat shim (deps-only, no Ollama check).
92
+ function ensurePackages() {
93
+ if (process.env.OGPT_NO_AUTO_INSTALL) return [];
94
+ const need = missingPackages();
95
+ if (!need.length) return [];
96
+ console.log("Installing packages...");
97
+ npmInstall(need.map(([, pkg]) => pkg));
98
+ return missingPackages().map(([, pkg]) => pkg);
99
+ }
100
+
101
+ module.exports = { ensureBoot, ensurePackages, installOllama, haveOllama,
102
+ npmInstall, PACKAGES };
package/src/cli.js CHANGED
@@ -12,7 +12,9 @@ const Platform = require("./platform");
12
12
  const catalog = require("./catalog");
13
13
  const web = require("./web");
14
14
 
15
- const VERSION = "1.1.0";
15
+ const VERSION = "0.1.5-beta";
16
+ const VERSION_TAG = "devices-all";
17
+ const VERSION_FULL = `${VERSION}(${VERSION_TAG})`;
16
18
 
17
19
  class Command {
18
20
  constructor(name, handler, description, usage, category, ai, aliases) {
@@ -144,14 +146,14 @@ class CLI {
144
146
  const lines = [""];
145
147
  for (const l of art) lines.push(`${B}${C}${center(l)}${R}`);
146
148
  lines.push("");
147
- lines.push(`${B}${W}${center(`AI coding assistant by KareemXD · v${VERSION}`)}${R}`);
149
+ lines.push(`${B}${W}${center(`AI coding assistant by KareemXD · v${VERSION_FULL}`)}${R}`);
148
150
  lines.push(`${B}${C}${center(`Active model: ${model} · ${Platform.ramGB()}GB RAM · tier ${tier}`)}${R}`);
149
151
  lines.push(`${D}${center("Type /help for commands · /quit to exit")}${R}`);
150
152
  lines.push("", "");
151
153
  const vpad = Math.max(0, Math.floor((rows - lines.length) / 2) - 1);
152
154
  process.stdout.write("\n".repeat(vpad) + lines.join("\n") + "\n\n");
153
155
  } else {
154
- const out = [...art, "", center(`OGPT v${VERSION} · model: ${model} · ${Platform.ramGB()}GB RAM · tier ${tier}`), center("Type /help for commands, /quit to exit"), ""];
156
+ const out = [...art, "", center(`OGPT v${VERSION_FULL} · model: ${model} · ${Platform.ramGB()}GB RAM · tier ${tier}`), center("Type /help for commands, /quit to exit"), ""];
155
157
  process.stdout.write(out.join("\n") + "\n");
156
158
  }
157
159
  }
@@ -1154,4 +1156,4 @@ function formatSize(bytes) {
1154
1156
  return bytes + " B";
1155
1157
  }
1156
1158
 
1157
- module.exports = { CLI };
1159
+ module.exports = { CLI, VERSION, VERSION_TAG, VERSION_FULL };
package/src/static.js CHANGED
@@ -1,2 +1,2 @@
1
1
  // Single-file token dashboard UI - identical to the Python build.
2
- module.exports = "<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>OGPT \u00b7 Token Dashboard</title>\n<script src=\"https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js\"></script>\n<style>\n:root{\n --bg:#0d0f14; --panel:#141822; --panel2:#181d2a; --line:#232a3a;\n --text:#e8ecf4; --dim:#8b93a7; --mag:#e34fd0; --cyan:#39c6e3;\n --green:#3ddc84; --red:#ff5c69; --amber:#ffc857; --radius:14px;\n --mono:\"SFMono-Regular\",Consolas,\"Liberation Mono\",Menlo,monospace;\n}\n*{box-sizing:border-box}\nbody{margin:0;background:\n radial-gradient(1200px 500px at 80% -10%, #1a1030 0%, transparent 60%),\n radial-gradient(900px 400px at -10% 110%, #06202b 0%, transparent 55%),\n var(--bg);\n color:var(--text);font:15px/1.5 -apple-system,\"Segoe UI\",Roboto,sans-serif;}\n.wrap{max-width:1180px;margin:0 auto;padding:18px 20px 40px}\n\n/* topbar */\n.topbar{display:flex;align-items:center;gap:12px;padding:14px 4px;border-bottom:1px solid var(--line)}\n.logo{font-weight:800;font-size:19px;letter-spacing:.3px;color:var(--mag)}\n.logo .dot{color:var(--cyan)}\n.chip{background:var(--panel);border:1px solid var(--line);border-radius:999px;\n padding:4px 12px;font-size:12.5px;color:var(--dim)}\n.chip b{color:var(--text)}\n.live{margin-left:auto;display:flex;align-items:center;gap:7px;font-size:12px;color:var(--green)}\n.live i{width:9px;height:9px;border-radius:50%;background:var(--green);\n box-shadow:0 0 0 0 rgba(61,220,132,.6);animation:pulse 1.6s infinite}\n@keyframes pulse{70%{box-shadow:0 0 0 8px rgba(61,220,132,0)}100%{box-shadow:0 0 0 0 rgba(61,220,132,0)}}\n\n/* cards */\n.cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px;margin:18px 0}\n.card{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);padding:14px 16px}\n.card .k{font-size:11.5px;text-transform:uppercase;letter-spacing:.08em;color:var(--dim)}\n.card .v{font-family:var(--mono);font-size:24px;font-weight:700;margin-top:4px}\n.card .s{font-size:11.5px;color:var(--dim);margin-top:2px}\n.v.mag{color:var(--mag)} .v.cyan{color:var(--cyan)} .v.green{color:var(--green)} .v.amber{color:var(--amber)}\n\n/* panels & charts */\n.grid2{display:grid;grid-template-columns:2fr 1fr;gap:12px;margin-bottom:12px}\n.panel{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);padding:14px 16px}\n.panel h3{margin:0 0 10px;font-size:13px;color:var(--dim);text-transform:uppercase;letter-spacing:.07em}\ncanvas{width:100%!important;height:240px!important}\n\n/* main columns */\n.main{display:grid;grid-template-columns:1fr 380px;gap:12px}\n@media(max-width:900px){.main,.grid2{grid-template-columns:1fr}}\n#feed{height:430px;overflow-y:auto;display:flex;flex-direction:column;gap:10px;padding-right:6px}\n.msg{border:1px solid var(--line);border-left:3px solid var(--cyan);border-radius:10px;\n background:var(--panel2);padding:10px 12px}\n.msg.user{border-left-color:var(--mag)}\n.msg .who{font-size:11.5px;font-weight:700;letter-spacing:.05em;text-transform:uppercase;color:var(--dim);margin-bottom:3px}\n.msg.user .who{color:var(--mag)} .msg.bot .who{color:var(--cyan)}\n.msg .txt{white-space:pre-wrap;word-wrap:break-word;font-size:14px}\n.meta{font-size:11px;color:var(--dim);font-family:var(--mono);margin-top:6px}\n.empty{color:var(--dim);text-align:center;padding:30px 0}\n\n/* chat composer */\n.composer{display:flex;flex-direction:column;gap:8px}\n#chat-in{width:100%;min-height:74px;resize:vertical;background:var(--panel2);color:var(--text);\n border:1px solid var(--line);border-radius:10px;padding:10px 12px;font:inherit}\n#chat-in:focus{outline:none;border-color:var(--mag)}\n.rowbtn{display:flex;gap:8px;align-items:center}\nbutton.send{background:linear-gradient(135deg,var(--mag),#8a4fe3);border:0;color:#fff;font-weight:700;\n padding:9px 20px;border-radius:999px;cursor:pointer;font-size:14px}\nbutton.send:disabled{opacity:.45;cursor:not-allowed}\n.hint{font-size:11.5px;color:var(--dim)}\n\n/* bottom tables */\n.bottom{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:12px}\n@media(max-width:900px){.bottom{grid-template-columns:1fr}}\ntable{width:100%;border-collapse:collapse;font-size:13px}\ntd,th{padding:7px 8px;border-bottom:1px solid var(--line);text-align:left}\nth{color:var(--dim);font-weight:600;font-size:11.5px;text-transform:uppercase;letter-spacing:.06em}\n.tools{display:flex;flex-wrap:wrap;gap:7px}\n.toolchip{background:var(--panel2);border:1px solid var(--line);border-radius:999px;\n padding:4px 11px;font-size:12px;color:var(--cyan);font-family:var(--mono)}\nfooter{margin-top:22px;color:var(--dim);font-size:12px;display:flex;justify-content:space-between;flex-wrap:wrap;gap:8px}\nfooter code{font-family:var(--mono);color:var(--cyan)}\n</style>\n</head>\n<body>\n<div class=\"wrap\">\n\n <div class=\"topbar\">\n <span class=\"logo\">\u25c6 OGPT<span class=\"dot\"> \u00b7 </span>Token Dashboard</span>\n <span class=\"chip\" id=\"chip-model\"><b>\u2014</b></span>\n <span class=\"chip\" id=\"chip-provider\">provider \u2014</span>\n <span class=\"live\"><i></i> LIVE</span>\n </div>\n\n <div class=\"cards\">\n <div class=\"card\"><div class=\"k\">Total tokens</div><div class=\"v mag\" id=\"c-total\">0</div><div class=\"s\" id=\"c-total-s\">prompt + completion</div></div>\n <div class=\"card\"><div class=\"k\">Prompt tokens</div><div class=\"v\" id=\"c-prompt\">0</div></div>\n <div class=\"card\"><div class=\"k\">Completion tokens</div><div class=\"v cyan\" id=\"c-completion\">0</div></div>\n <div class=\"card\"><div class=\"k\">Avg speed</div><div class=\"v green\" id=\"c-avgtps\">\u2013</div><div class=\"s\">tokens/sec</div></div>\n <div class=\"card\"><div class=\"k\">Best speed</div><div class=\"v green\" id=\"c-besttps\">\u2013</div></div>\n <div class=\"card\"><div class=\"k\">Last TTFT</div><div class=\"v amber\" id=\"c-ttft\">\u2013</div><div class=\"s\">time to first token</div></div>\n <div class=\"card\"><div class=\"k\">Messages</div><div class=\"v\" id=\"c-msgs\">0</div><div class=\"s\" id=\"c-runs\">0 runs tracked</div></div>\n <div class=\"card\"><div class=\"k\">Uptime</div><div class=\"v\" id=\"c-uptime\">0:00</div><div class=\"s\" id=\"c-session\">no session</div></div>\n </div>\n\n <div class=\"grid2\">\n <div class=\"panel\"><h3>Tokens per run</h3><canvas id=\"ch-runs\"></canvas></div>\n <div class=\"panel\"><h3>Token split</h3><canvas id=\"ch-split\"></canvas></div>\n </div>\n\n <div class=\"main\">\n <div class=\"panel\"><h3>Conversation</h3><div id=\"feed\"><div class=\"empty\">No messages yet.</div></div></div>\n <div class=\"panel composer-panel\">\n <h3>Chat with this agent</h3>\n <div class=\"composer\">\n <textarea id=\"chat-in\" placeholder=\"Ask anything\u2026 (shares the same brain as the terminal session)\"></textarea>\n <div class=\"rowbtn\">\n <button class=\"send\" id=\"chat-send\">Send \u23ce</button>\n <span class=\"hint\" id=\"chat-hint\">Streams from the live model.</span>\n </div>\n <div class=\"hint\">Enter to send \u00b7 Shift+Enter newline</div>\n </div>\n </div>\n </div>\n\n <div class=\"bottom\">\n <div class=\"panel\"><h3>Sessions</h3>\n <table><thead><tr><th>ID</th><th>Title</th><th>Msgs</th><th>Updated</th></tr></thead>\n <tbody id=\"sess-rows\"><tr><td colspan=\"4\" style=\"color:var(--dim)\">\u2014</td></tr></tbody></table>\n </div>\n <div class=\"panel\"><h3>Registered tools (<span id=\"tool-count\">0</span>)</h3>\n <div class=\"tools\" id=\"tools-list\"><span style=\"color:var(--dim)\">\u2014</span></div>\n </div>\n </div>\n\n <footer>\n <span>OGPT v1.1.0 \u00b7 dashboard bound to <code>127.0.0.1</code> only</span>\n <span>polling every 1.5s \u00b7 <code>/api/stats</code></span>\n </footer>\n</div>\n\n<script>\nconst $=id=>document.getElementById(id);\nconst fmt=n=>Number(n||0).toLocaleString();\nlet runsChart=null, splitChart=null;\nlet lastHistoryKey=\"\", busy=false;\n\nfunction fmtDur(s){s=Math.floor(s||0);const h=Math.floor(s/3600),m=Math.floor(s%3600/60),\n x=s%60;return (h?h+\":\":\"\")+m+\":\"+String(x).padStart(2,\"0\");}\nfunction setText(id,v){$(id).textContent=v;}\n\nasync function poll(){\n try{\n const d=await (await fetch(\"/api/stats\")).json();\n setText(\"chip-model\",\"\");$(\"chip-model\").innerHTML=\"<b>\"+d.model+\"</b>\";\n $(\"chip-provider\").textContent=\"provider \"+d.provider+\" \u00b7 \"+d.cwd;\n setText(\"c-total\",fmt(d.tokens.total));\n setText(\"c-total-s\",fmt(d.tokens.prompt)+\" prompt + \"+fmt(d.tokens.completion)+\" completion\");\n setText(\"c-prompt\",fmt(d.tokens.prompt));\n setText(\"c-completion\",fmt(d.tokens.completion));\n setText(\"c-avgtps\",d.avg_tps?d.avg_tps.toFixed(1):\"\u2013\");\n setText(\"c-besttps\",d.best_tps?d.best_tps.toFixed(1):\"\u2013\");\n const lr=d.last_run||{};\n setText(\"c-ttft\",(lr.ttft!=null)?lr.ttft.toFixed(2)+\"s\":\"\u2013\");\n setText(\"c-msgs\",fmt(d.messages));\n setText(\"c-runs\",fmt(d.runs_count)+\" runs tracked\");\n setText(\"c-uptime\",fmtDur(d.uptime_s));\n setText(\"c-session\",d.session?(\"session \"+d.session):\"no session\");\n updateCharts(d);\n }catch(e){/* server gone quiet */}\n}\n\nfunction ensureCharts(){\n if(!window.Chart||runsChart)return;\n Chart.defaults.color=\"#8b93a7\";Chart.defaults.borderColor=\"#232a3a\";\n runsChart=new Chart($(\"ch-runs\"),{type:\"line\",\n data:{labels:[],datasets:[{label:\"tokens\",data:[],borderColor:\"#e34fd0\",\n backgroundColor:\"rgba(227,79,208,.15)\",fill:true,tension:.35,pointRadius:2}]},\n options:{animation:false,maintainAspectRatio:false,\n scales:{y:{beginAtZero:true},x:{ticks:{maxTicksLimit:10}}}}});\n splitChart=new Chart($(\"ch-split\"),{type:\"doughnut\",\n data:{labels:[\"prompt\",\"completion\"],\n datasets:[{data:[0,0],backgroundColor:[\"#39c6e3\",\"#e34fd0\"],borderWidth:0}]},\n options:{animation:false,maintainAspectRatio:false,cutout:\"62%\",\n plugins:{legend:{position:\"bottom\"}}}});\n}\nfunction updateCharts(d){\n ensureCharts();if(!runsChart)return;\n const rs=d.runs||[];\n runsChart.data.labels=rs.map(r=>new Date(r.ts*1000).toLocaleTimeString());\n runsChart.data.datasets[0].data=rs.map(r=>r.tokens);\n runsChart.update(\"none\");\n splitChart.data.datasets[0].data=[d.tokens.prompt,d.tokens.completion];\n splitChart.update(\"none\");\n}\n\nasync function loadHistory(force){\n try{\n const msgs=await (await fetch(\"/api/history\")).json();\n const key=msgs.length+\":\"+((msgs[msgs.length-1]||{}).ts||0);\n if(!force&&key===lastHistoryKey)return;\n lastHistoryKey=key;\n const feed=$(\"feed\");feed.textContent=\"\";\n if(!msgs.length){feed.innerHTML='<div class=\"empty\">No messages yet.</div>';return;}\n for(const m of msgs){\n const div=document.createElement(\"div\");\n div.className=\"msg \"+(m.role===\"user\"?\"user\":\"bot\");\n const who=document.createElement(\"div\");who.className=\"who\";\n who.textContent=m.role===\"user\"?\"\u276f You\":\"\u25cf oGPT\";\n const txt=document.createElement(\"div\");txt.className=\"txt\";\n txt.textContent=m.content;\n div.append(who,txt);\n if(m.ts){const meta=document.createElement(\"div\");meta.className=\"meta\";\n meta.textContent=new Date(m.ts*1000).toLocaleString();div.append(meta);}\n feed.appendChild(div);\n }\n feed.scrollTop=feed.scrollHeight;\n }catch(e){}\n}\n\nfunction addBubble(kind){\n const div=document.createElement(\"div\");\n div.className=\"msg \"+kind;\n const who=document.createElement(\"div\");who.className=\"who\";\n who.textContent=kind===\"user\"?\"\u276f You\":\"\u25cf oGPT\";\n const txt=document.createElement(\"div\");txt.className=\"txt\";\n if(kind!==\"user\"){const m=document.createElement(\"div\");m.className=\"meta\";\n m.textContent=\"streaming\u2026\";txt.dataset.meta=m;txt.append(m);}\n div.append(who,txt);$(\"feed\").appendChild(div);\n $(\"feed\").scrollTop=$(\"feed\").scrollHeight;\n return txt;\n}\nfunction sendChat(){\n if(busy)return;\n const inp=$(\"chat-in\"),msg=inp.value.trim();\n if(!msg)return;\n inp.value=\"\";busy=true;$(\"chat-send\").disabled=true;\n addBubble(\"user\");\n const bot=addBubble(\"bot\");\n let acc=\"\";\n const es=new EventSource(\"/api/chat/stream?msg=\"+encodeURIComponent(msg));\n es.onmessage=e=>{\n let d;try{d=JSON.parse(e.data);}catch(_){return;}\n if(d.t){acc+=d.t;bot.textContent=acc;\n $(\"feed\").scrollTop=$(\"feed\").scrollHeight;}\n if(d.error){acc+=\"\\n[error] \"+d.error;bot.textContent=acc;}\n if(d.done){es.close();\n const s=d.stats||{};const bits=[];\n if(s.tokens)bits.push(s.tokens+\" tok\");\n if(s.tps)bits.push(s.tps+\" tok/s\");\n bot.dataset.meta&&(bot.querySelector(\".meta\")||{}).remove;\n const meta=document.createElement(\"div\");meta.className=\"meta\";\n meta.textContent=bits.length?bits.join(\" \u00b7 \"):\"done\";\n bot.parentElement.appendChild(meta);\n busy=false;$(\"chat-send\").disabled=false;loadHistory(true);}\n };\n es.onerror=()=>{es.close();busy=false;$(\"chat-send\").disabled=false;};\n}\n$(\"chat-send\").addEventListener(\"click\",sendChat);\n$(\"chat-in\").addEventListener(\"keydown\",ev=>{\n if(ev.key===\"Enter\"&&!ev.shiftKey){ev.preventDefault();sendChat();}\n});\n\nasync function loadBottom(){\n try{\n const rows=await (await fetch(\"/api/sessions\")).json();\n const tb=$(\"sess-rows\");tb.textContent=\"\";\n if(!rows.length){tb.innerHTML='<tr><td colspan=\"4\" style=\"color:var(--dim)\">\u2014</td></tr>';return;}\n for(const s of rows){\n const tr=document.createElement(\"tr\");\n [s.id,s.title,s.msgs,new Date((s.updated||0)*1000).toLocaleString()].forEach(v=>{\n const td=document.createElement(\"td\");td.textContent=v??\"\";tr.appendChild(td);});\n tb.appendChild(tr);\n }\n }catch(e){}\n try{\n const names=await (await fetch(\"/api/tools\")).json();\n $(\"tool-count\").textContent=names.length;\n const list=$(\"tools-list\");list.textContent=\"\";\n for(const n of names){const c=document.createElement(\"span\");\n c.className=\"toolchip\";c.textContent=n;list.appendChild(c);}\n }catch(e){}\n}\n\npoll();loadHistory();loadBottom();\nsetInterval(poll,1500);\nsetInterval(()=>loadHistory(false),5000);\nsetInterval(loadBottom,20000);\nensureCharts();\n</script>\n</body>\n</html>\n";
2
+ module.exports = "<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>OGPT \u00b7 Token Dashboard</title>\n<script src=\"https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js\"></script>\n<style>\n:root{\n --bg:#0d0f14; --panel:#141822; --panel2:#181d2a; --line:#232a3a;\n --text:#e8ecf4; --dim:#8b93a7; --mag:#e34fd0; --cyan:#39c6e3;\n --green:#3ddc84; --red:#ff5c69; --amber:#ffc857; --radius:14px;\n --mono:\"SFMono-Regular\",Consolas,\"Liberation Mono\",Menlo,monospace;\n}\n*{box-sizing:border-box}\nbody{margin:0;background:\n radial-gradient(1200px 500px at 80% -10%, #1a1030 0%, transparent 60%),\n radial-gradient(900px 400px at -10% 110%, #06202b 0%, transparent 55%),\n var(--bg);\n color:var(--text);font:15px/1.5 -apple-system,\"Segoe UI\",Roboto,sans-serif;}\n.wrap{max-width:1180px;margin:0 auto;padding:18px 20px 40px}\n\n/* topbar */\n.topbar{display:flex;align-items:center;gap:12px;padding:14px 4px;border-bottom:1px solid var(--line)}\n.logo{font-weight:800;font-size:19px;letter-spacing:.3px;color:var(--mag)}\n.logo .dot{color:var(--cyan)}\n.chip{background:var(--panel);border:1px solid var(--line);border-radius:999px;\n padding:4px 12px;font-size:12.5px;color:var(--dim)}\n.chip b{color:var(--text)}\n.live{margin-left:auto;display:flex;align-items:center;gap:7px;font-size:12px;color:var(--green)}\n.live i{width:9px;height:9px;border-radius:50%;background:var(--green);\n box-shadow:0 0 0 0 rgba(61,220,132,.6);animation:pulse 1.6s infinite}\n@keyframes pulse{70%{box-shadow:0 0 0 8px rgba(61,220,132,0)}100%{box-shadow:0 0 0 0 rgba(61,220,132,0)}}\n\n/* cards */\n.cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px;margin:18px 0}\n.card{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);padding:14px 16px}\n.card .k{font-size:11.5px;text-transform:uppercase;letter-spacing:.08em;color:var(--dim)}\n.card .v{font-family:var(--mono);font-size:24px;font-weight:700;margin-top:4px}\n.card .s{font-size:11.5px;color:var(--dim);margin-top:2px}\n.v.mag{color:var(--mag)} .v.cyan{color:var(--cyan)} .v.green{color:var(--green)} .v.amber{color:var(--amber)}\n\n/* panels & charts */\n.grid2{display:grid;grid-template-columns:2fr 1fr;gap:12px;margin-bottom:12px}\n.panel{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);padding:14px 16px}\n.panel h3{margin:0 0 10px;font-size:13px;color:var(--dim);text-transform:uppercase;letter-spacing:.07em}\ncanvas{width:100%!important;height:240px!important}\n\n/* main columns */\n.main{display:grid;grid-template-columns:1fr 380px;gap:12px}\n@media(max-width:900px){.main,.grid2{grid-template-columns:1fr}}\n#feed{height:430px;overflow-y:auto;display:flex;flex-direction:column;gap:10px;padding-right:6px}\n.msg{border:1px solid var(--line);border-left:3px solid var(--cyan);border-radius:10px;\n background:var(--panel2);padding:10px 12px}\n.msg.user{border-left-color:var(--mag)}\n.msg .who{font-size:11.5px;font-weight:700;letter-spacing:.05em;text-transform:uppercase;color:var(--dim);margin-bottom:3px}\n.msg.user .who{color:var(--mag)} .msg.bot .who{color:var(--cyan)}\n.msg .txt{white-space:pre-wrap;word-wrap:break-word;font-size:14px}\n.meta{font-size:11px;color:var(--dim);font-family:var(--mono);margin-top:6px}\n.empty{color:var(--dim);text-align:center;padding:30px 0}\n\n/* chat composer */\n.composer{display:flex;flex-direction:column;gap:8px}\n#chat-in{width:100%;min-height:74px;resize:vertical;background:var(--panel2);color:var(--text);\n border:1px solid var(--line);border-radius:10px;padding:10px 12px;font:inherit}\n#chat-in:focus{outline:none;border-color:var(--mag)}\n.rowbtn{display:flex;gap:8px;align-items:center}\nbutton.send{background:linear-gradient(135deg,var(--mag),#8a4fe3);border:0;color:#fff;font-weight:700;\n padding:9px 20px;border-radius:999px;cursor:pointer;font-size:14px}\nbutton.send:disabled{opacity:.45;cursor:not-allowed}\n.hint{font-size:11.5px;color:var(--dim)}\n\n/* bottom tables */\n.bottom{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:12px}\n@media(max-width:900px){.bottom{grid-template-columns:1fr}}\ntable{width:100%;border-collapse:collapse;font-size:13px}\ntd,th{padding:7px 8px;border-bottom:1px solid var(--line);text-align:left}\nth{color:var(--dim);font-weight:600;font-size:11.5px;text-transform:uppercase;letter-spacing:.06em}\n.tools{display:flex;flex-wrap:wrap;gap:7px}\n.toolchip{background:var(--panel2);border:1px solid var(--line);border-radius:999px;\n padding:4px 11px;font-size:12px;color:var(--cyan);font-family:var(--mono)}\nfooter{margin-top:22px;color:var(--dim);font-size:12px;display:flex;justify-content:space-between;flex-wrap:wrap;gap:8px}\nfooter code{font-family:var(--mono);color:var(--cyan)}\n</style>\n</head>\n<body>\n<div class=\"wrap\">\n\n <div class=\"topbar\">\n <span class=\"logo\">\u25c6 OGPT<span class=\"dot\"> \u00b7 </span>Token Dashboard</span>\n <span class=\"chip\" id=\"chip-model\"><b>\u2014</b></span>\n <span class=\"chip\" id=\"chip-provider\">provider \u2014</span>\n <span class=\"live\"><i></i> LIVE</span>\n </div>\n\n <div class=\"cards\">\n <div class=\"card\"><div class=\"k\">Total tokens</div><div class=\"v mag\" id=\"c-total\">0</div><div class=\"s\" id=\"c-total-s\">prompt + completion</div></div>\n <div class=\"card\"><div class=\"k\">Prompt tokens</div><div class=\"v\" id=\"c-prompt\">0</div></div>\n <div class=\"card\"><div class=\"k\">Completion tokens</div><div class=\"v cyan\" id=\"c-completion\">0</div></div>\n <div class=\"card\"><div class=\"k\">Avg speed</div><div class=\"v green\" id=\"c-avgtps\">\u2013</div><div class=\"s\">tokens/sec</div></div>\n <div class=\"card\"><div class=\"k\">Best speed</div><div class=\"v green\" id=\"c-besttps\">\u2013</div></div>\n <div class=\"card\"><div class=\"k\">Last TTFT</div><div class=\"v amber\" id=\"c-ttft\">\u2013</div><div class=\"s\">time to first token</div></div>\n <div class=\"card\"><div class=\"k\">Messages</div><div class=\"v\" id=\"c-msgs\">0</div><div class=\"s\" id=\"c-runs\">0 runs tracked</div></div>\n <div class=\"card\"><div class=\"k\">Uptime</div><div class=\"v\" id=\"c-uptime\">0:00</div><div class=\"s\" id=\"c-session\">no session</div></div>\n </div>\n\n <div class=\"grid2\">\n <div class=\"panel\"><h3>Tokens per run</h3><canvas id=\"ch-runs\"></canvas></div>\n <div class=\"panel\"><h3>Token split</h3><canvas id=\"ch-split\"></canvas></div>\n </div>\n\n <div class=\"main\">\n <div class=\"panel\"><h3>Conversation</h3><div id=\"feed\"><div class=\"empty\">No messages yet.</div></div></div>\n <div class=\"panel composer-panel\">\n <h3>Chat with this agent</h3>\n <div class=\"composer\">\n <textarea id=\"chat-in\" placeholder=\"Ask anything\u2026 (shares the same brain as the terminal session)\"></textarea>\n <div class=\"rowbtn\">\n <button class=\"send\" id=\"chat-send\">Send \u23ce</button>\n <span class=\"hint\" id=\"chat-hint\">Streams from the live model.</span>\n </div>\n <div class=\"hint\">Enter to send \u00b7 Shift+Enter newline</div>\n </div>\n </div>\n </div>\n\n <div class=\"bottom\">\n <div class=\"panel\"><h3>Sessions</h3>\n <table><thead><tr><th>ID</th><th>Title</th><th>Msgs</th><th>Updated</th></tr></thead>\n <tbody id=\"sess-rows\"><tr><td colspan=\"4\" style=\"color:var(--dim)\">\u2014</td></tr></tbody></table>\n </div>\n <div class=\"panel\"><h3>Registered tools (<span id=\"tool-count\">0</span>)</h3>\n <div class=\"tools\" id=\"tools-list\"><span style=\"color:var(--dim)\">\u2014</span></div>\n </div>\n </div>\n\n <footer>\n <span>OGPT v0.1.5-beta(devices-all) \u00b7 dashboard bound to <code>127.0.0.1</code> only</span>\n <span>polling every 1.5s \u00b7 <code>/api/stats</code></span>\n </footer>\n</div>\n\n<script>\nconst $=id=>document.getElementById(id);\nconst fmt=n=>Number(n||0).toLocaleString();\nlet runsChart=null, splitChart=null;\nlet lastHistoryKey=\"\", busy=false;\n\nfunction fmtDur(s){s=Math.floor(s||0);const h=Math.floor(s/3600),m=Math.floor(s%3600/60),\n x=s%60;return (h?h+\":\":\"\")+m+\":\"+String(x).padStart(2,\"0\");}\nfunction setText(id,v){$(id).textContent=v;}\n\nasync function poll(){\n try{\n const d=await (await fetch(\"/api/stats\")).json();\n setText(\"chip-model\",\"\");$(\"chip-model\").innerHTML=\"<b>\"+d.model+\"</b>\";\n $(\"chip-provider\").textContent=\"provider \"+d.provider+\" \u00b7 \"+d.cwd;\n setText(\"c-total\",fmt(d.tokens.total));\n setText(\"c-total-s\",fmt(d.tokens.prompt)+\" prompt + \"+fmt(d.tokens.completion)+\" completion\");\n setText(\"c-prompt\",fmt(d.tokens.prompt));\n setText(\"c-completion\",fmt(d.tokens.completion));\n setText(\"c-avgtps\",d.avg_tps?d.avg_tps.toFixed(1):\"\u2013\");\n setText(\"c-besttps\",d.best_tps?d.best_tps.toFixed(1):\"\u2013\");\n const lr=d.last_run||{};\n setText(\"c-ttft\",(lr.ttft!=null)?lr.ttft.toFixed(2)+\"s\":\"\u2013\");\n setText(\"c-msgs\",fmt(d.messages));\n setText(\"c-runs\",fmt(d.runs_count)+\" runs tracked\");\n setText(\"c-uptime\",fmtDur(d.uptime_s));\n setText(\"c-session\",d.session?(\"session \"+d.session):\"no session\");\n updateCharts(d);\n }catch(e){/* server gone quiet */}\n}\n\nfunction ensureCharts(){\n if(!window.Chart||runsChart)return;\n Chart.defaults.color=\"#8b93a7\";Chart.defaults.borderColor=\"#232a3a\";\n runsChart=new Chart($(\"ch-runs\"),{type:\"line\",\n data:{labels:[],datasets:[{label:\"tokens\",data:[],borderColor:\"#e34fd0\",\n backgroundColor:\"rgba(227,79,208,.15)\",fill:true,tension:.35,pointRadius:2}]},\n options:{animation:false,maintainAspectRatio:false,\n scales:{y:{beginAtZero:true},x:{ticks:{maxTicksLimit:10}}}}});\n splitChart=new Chart($(\"ch-split\"),{type:\"doughnut\",\n data:{labels:[\"prompt\",\"completion\"],\n datasets:[{data:[0,0],backgroundColor:[\"#39c6e3\",\"#e34fd0\"],borderWidth:0}]},\n options:{animation:false,maintainAspectRatio:false,cutout:\"62%\",\n plugins:{legend:{position:\"bottom\"}}}});\n}\nfunction updateCharts(d){\n ensureCharts();if(!runsChart)return;\n const rs=d.runs||[];\n runsChart.data.labels=rs.map(r=>new Date(r.ts*1000).toLocaleTimeString());\n runsChart.data.datasets[0].data=rs.map(r=>r.tokens);\n runsChart.update(\"none\");\n splitChart.data.datasets[0].data=[d.tokens.prompt,d.tokens.completion];\n splitChart.update(\"none\");\n}\n\nasync function loadHistory(force){\n try{\n const msgs=await (await fetch(\"/api/history\")).json();\n const key=msgs.length+\":\"+((msgs[msgs.length-1]||{}).ts||0);\n if(!force&&key===lastHistoryKey)return;\n lastHistoryKey=key;\n const feed=$(\"feed\");feed.textContent=\"\";\n if(!msgs.length){feed.innerHTML='<div class=\"empty\">No messages yet.</div>';return;}\n for(const m of msgs){\n const div=document.createElement(\"div\");\n div.className=\"msg \"+(m.role===\"user\"?\"user\":\"bot\");\n const who=document.createElement(\"div\");who.className=\"who\";\n who.textContent=m.role===\"user\"?\"\u276f You\":\"\u25cf oGPT\";\n const txt=document.createElement(\"div\");txt.className=\"txt\";\n txt.textContent=m.content;\n div.append(who,txt);\n if(m.ts){const meta=document.createElement(\"div\");meta.className=\"meta\";\n meta.textContent=new Date(m.ts*1000).toLocaleString();div.append(meta);}\n feed.appendChild(div);\n }\n feed.scrollTop=feed.scrollHeight;\n }catch(e){}\n}\n\nfunction addBubble(kind){\n const div=document.createElement(\"div\");\n div.className=\"msg \"+kind;\n const who=document.createElement(\"div\");who.className=\"who\";\n who.textContent=kind===\"user\"?\"\u276f You\":\"\u25cf oGPT\";\n const txt=document.createElement(\"div\");txt.className=\"txt\";\n if(kind!==\"user\"){const m=document.createElement(\"div\");m.className=\"meta\";\n m.textContent=\"streaming\u2026\";txt.dataset.meta=m;txt.append(m);}\n div.append(who,txt);$(\"feed\").appendChild(div);\n $(\"feed\").scrollTop=$(\"feed\").scrollHeight;\n return txt;\n}\nfunction sendChat(){\n if(busy)return;\n const inp=$(\"chat-in\"),msg=inp.value.trim();\n if(!msg)return;\n inp.value=\"\";busy=true;$(\"chat-send\").disabled=true;\n addBubble(\"user\");\n const bot=addBubble(\"bot\");\n let acc=\"\";\n const es=new EventSource(\"/api/chat/stream?msg=\"+encodeURIComponent(msg));\n es.onmessage=e=>{\n let d;try{d=JSON.parse(e.data);}catch(_){return;}\n if(d.t){acc+=d.t;bot.textContent=acc;\n $(\"feed\").scrollTop=$(\"feed\").scrollHeight;}\n if(d.error){acc+=\"\\n[error] \"+d.error;bot.textContent=acc;}\n if(d.done){es.close();\n const s=d.stats||{};const bits=[];\n if(s.tokens)bits.push(s.tokens+\" tok\");\n if(s.tps)bits.push(s.tps+\" tok/s\");\n bot.dataset.meta&&(bot.querySelector(\".meta\")||{}).remove;\n const meta=document.createElement(\"div\");meta.className=\"meta\";\n meta.textContent=bits.length?bits.join(\" \u00b7 \"):\"done\";\n bot.parentElement.appendChild(meta);\n busy=false;$(\"chat-send\").disabled=false;loadHistory(true);}\n };\n es.onerror=()=>{es.close();busy=false;$(\"chat-send\").disabled=false;};\n}\n$(\"chat-send\").addEventListener(\"click\",sendChat);\n$(\"chat-in\").addEventListener(\"keydown\",ev=>{\n if(ev.key===\"Enter\"&&!ev.shiftKey){ev.preventDefault();sendChat();}\n});\n\nasync function loadBottom(){\n try{\n const rows=await (await fetch(\"/api/sessions\")).json();\n const tb=$(\"sess-rows\");tb.textContent=\"\";\n if(!rows.length){tb.innerHTML='<tr><td colspan=\"4\" style=\"color:var(--dim)\">\u2014</td></tr>';return;}\n for(const s of rows){\n const tr=document.createElement(\"tr\");\n [s.id,s.title,s.msgs,new Date((s.updated||0)*1000).toLocaleString()].forEach(v=>{\n const td=document.createElement(\"td\");td.textContent=v??\"\";tr.appendChild(td);});\n tb.appendChild(tr);\n }\n }catch(e){}\n try{\n const names=await (await fetch(\"/api/tools\")).json();\n $(\"tool-count\").textContent=names.length;\n const list=$(\"tools-list\");list.textContent=\"\";\n for(const n of names){const c=document.createElement(\"span\");\n c.className=\"toolchip\";c.textContent=n;list.appendChild(c);}\n }catch(e){}\n}\n\npoll();loadHistory();loadBottom();\nsetInterval(poll,1500);\nsetInterval(()=>loadHistory(false),5000);\nsetInterval(loadBottom,20000);\nensureCharts();\n</script>\n</body>\n</html>\n";
package/src/tools.js CHANGED
@@ -424,6 +424,77 @@ function resolveBin(bin) {
424
424
  return bin; // let spawnSync produce its own ENOENT
425
425
  }
426
426
 
427
+ // ---- silent package auto-heal --------------------------------------------
428
+ // When AI-written code imports something that is not installed, we install
429
+ // it quietly (all installer output hidden) and retry once. The user only
430
+ // ever sees 'Installing packages...' via the short result line.
431
+
432
+ const PY_ALIASES = {
433
+ cv2: "opencv-python", PIL: "pillow", bs4: "beautifulsoup4",
434
+ yaml: "pyyaml", sklearn: "scikit-learn", dotenv: "python-dotenv",
435
+ Crypto: "pycryptodome", fitz: "PyMuPDF", docx: "python-docx",
436
+ pptx: "python-pptx",
437
+ };
438
+
439
+ function pyMissingModules(text) {
440
+ const names = [];
441
+ for (const m of String(text || "").matchAll(/No module named '(?:([\w.]+)'|([\w.]+)")/g)) {
442
+ const root = (m[1] || m[2] || "").split(".")[0];
443
+ if (root && !names.includes(root)) names.push(root);
444
+ }
445
+ return names;
446
+ }
447
+
448
+ function pipInstallQuiet(mods) {
449
+ const pkgs = mods.map((m) => PY_ALIASES[m] || m);
450
+ try {
451
+ const r = spawnSync(resolveBin("python3") || "python3",
452
+ ["-m", "pip", "install", "--quiet", "--disable-pip-version-check", ...pkgs],
453
+ { stdio: "ignore", timeout: 600000 });
454
+ return !r.error && r.status === 0;
455
+ } catch { return false; }
456
+ }
457
+
458
+ function nodeMissingModules(text) {
459
+ const names = [];
460
+ for (const m of String(text || "").matchAll(/Cannot find module '([^']+)'/g)) {
461
+ const spec = m[1];
462
+ if (!spec || /^([./]|node:|[A-Z]:\\)/.test(spec)) continue; // relative/builtin
463
+ if (!names.includes(spec)) names.push(spec);
464
+ }
465
+ return names;
466
+ }
467
+
468
+ function npmInstallQuiet(pkgs, cwd) {
469
+ try {
470
+ const r = spawnSync(resolveBin("npm") || "npm",
471
+ ["install", "--no-fund", "--no-audit", "--silent", ...pkgs],
472
+ { cwd: path.resolve(cwd || "."), stdio: "ignore", timeout: 600000 });
473
+ return !r.error && r.status === 0;
474
+ } catch { return false; }
475
+ }
476
+
477
+ function autoHealNode(result, rerun, cwd) {
478
+ // NOTE: runInterpreter reports success=true for nonzero exits (the process
479
+ // spawned fine), so key off the traceback text, not the success flag.
480
+ const text = (result.output || "") + "\n" + (result.error || "");
481
+ const failed = !result.success || /\[exit \d+\]/.test(text);
482
+ if (!failed) return result;
483
+ const mods = nodeMissingModules(text);
484
+ if (mods.length && npmInstallQuiet(mods, cwd)) {
485
+ const r2 = rerun();
486
+ if (r2.success) r2.output = `Installed ${mods.join(", ")}\n${r2.output}`;
487
+ return r2;
488
+ }
489
+ const pymods = pyMissingModules(text);
490
+ if (pymods.length && pipInstallQuiet(pymods)) {
491
+ const r2 = rerun();
492
+ if (r2.success) r2.output = `Installed ${pymods.join(", ")}\n${r2.output}`;
493
+ return r2;
494
+ }
495
+ return result;
496
+ }
497
+
427
498
  function runInterpreter(bin, argv, cwd, timeout) {
428
499
  const r = spawnSync(resolveBin(bin), argv, {
429
500
  cwd: path.resolve(cwd || "."), timeout: timeout || 30000,
@@ -659,8 +730,10 @@ const IMPLEMENTATIONS = {
659
730
  let content = String(args.content ?? "");
660
731
  if (!content.trim()) {
661
732
  // An empty .html file gets a professional HTML5 skeleton (page title
662
- // from the file name) instead of a blank 0-byte file.
663
- if (path.extname(p).toLowerCase() === ".html") {
733
+ // from the file name) instead of a blank 0-byte file. Empty .css/.js
734
+ // files get minimal modern starting points.
735
+ const ext = path.extname(p).toLowerCase();
736
+ if (ext === ".html") {
664
737
  let title = path.basename(p, ".html").replace(/[-_]/g, " ").trim();
665
738
  title = title ? title[0].toUpperCase() + title.slice(1) : "Document";
666
739
  content =
@@ -669,6 +742,25 @@ const IMPLEMENTATIONS = {
669
742
  " <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n" +
670
743
  ` <title>${title}</title>\n` +
671
744
  "</head>\n<body>\n\n</body>\n</html>\n";
745
+ } else if (ext === ".css") {
746
+ content =
747
+ "/* Styles */\n\n" +
748
+ ":root {\n" +
749
+ " --bg: #0f172a;\n" +
750
+ " --fg: #e2e8f0;\n" +
751
+ " --accent: #38bdf8;\n" +
752
+ "}\n\n" +
753
+ "*,\n*::before,\n*::after {\n" +
754
+ " box-sizing: border-box;\n" +
755
+ " margin: 0;\n" +
756
+ " padding: 0;\n}\n\n" +
757
+ "body {\n" +
758
+ " font-family: system-ui, sans-serif;\n" +
759
+ " background: var(--bg);\n" +
760
+ " color: var(--fg);\n" +
761
+ " line-height: 1.6;\n}\n";
762
+ } else if (ext === ".js") {
763
+ content = "// App scripts\n\"use strict\";\n";
672
764
  }
673
765
  }
674
766
  fs.mkdirSync(path.dirname(p), { recursive: true });
@@ -817,7 +909,17 @@ const IMPLEMENTATIONS = {
817
909
  return new ToolResult("", `File not found: ${file} (use a path relative to your workspace)`, false);
818
910
  }
819
911
  const argv = file ? [file] : ["-c", code];
820
- return runInterpreter("python3", argv, args.cwd || ".", parseInt(args.timeout || 60, 10) * 1000);
912
+ let r = runInterpreter("python3", argv, args.cwd || ".", parseInt(args.timeout || 60, 10) * 1000);
913
+ // Auto-heal: missing python packages get installed silently (the user
914
+ // only sees 'Installing packages...' via the tool flow) and we retry once.
915
+ if (!r.success) {
916
+ const mods = pyMissingModules((r.error || "") + (r.output || ""));
917
+ if (mods.length && pipInstallQuiet(mods)) {
918
+ r = runInterpreter("python3", argv, args.cwd || ".", parseInt(args.timeout || 60, 10) * 1000);
919
+ if (r.success) r.output = `Installed ${mods.join(", ")}\n${r.output}`;
920
+ }
921
+ }
922
+ return r;
821
923
  },
822
924
 
823
925
  run_node(args) {
@@ -837,15 +939,20 @@ const IMPLEMENTATIONS = {
837
939
  const tmp = path.join(os.tmpdir(), `ogpt-run-${Date.now()}.mjs`);
838
940
  fs.writeFileSync(tmp, code);
839
941
  try {
840
- return runInterpreter("node", [tmp], args.cwd || ".",
942
+ let r = runInterpreter("node", [tmp], args.cwd || ".",
841
943
  parseInt(args.timeout || 60, 10) * 1000);
944
+ r = autoHealNode(r, () => runInterpreter("node", [tmp], args.cwd || ".",
945
+ parseInt(args.timeout || 60, 10) * 1000), args.cwd || ".");
946
+ return r;
842
947
  } finally {
843
948
  try { fs.unlinkSync(tmp); } catch {}
844
949
  }
845
950
  } else {
846
951
  argv = ["-e", code];
847
952
  }
848
- return runInterpreter("node", argv, args.cwd || ".", parseInt(args.timeout || 60, 10) * 1000);
953
+ let r = runInterpreter("node", argv, args.cwd || ".", parseInt(args.timeout || 60, 10) * 1000);
954
+ return autoHealNode(r, () => runInterpreter("node", argv, args.cwd || ".",
955
+ parseInt(args.timeout || 60, 10) * 1000), args.cwd || ".");
849
956
  },
850
957
 
851
958
  install_packages(args) {
@@ -1164,4 +1271,4 @@ class ToolRegistry {
1164
1271
  }
1165
1272
  }
1166
1273
 
1167
- module.exports = { ToolRegistry, ToolDef, ToolResult, TOOLS };
1274
+ module.exports = { ToolRegistry, ToolDef, ToolResult, TOOLS, IMPLEMENTATIONS };
package/src/web.js CHANGED
@@ -111,7 +111,7 @@ function createDashboard(cli) {
111
111
  return json(res, 200, {
112
112
  ok: true,
113
113
  app: "OGPT",
114
- version: "1.1.0",
114
+ version: "0.1.5-beta(devices-all)",
115
115
  model: name,
116
116
  provider: "Ollama",
117
117
  uptime_s: Math.round((Date.now() - STARTED_AT) / 100) / 10,
@@ -140,7 +140,7 @@ function createDashboard(cli) {
140
140
  } catch {}
141
141
  return json(res, 200, {
142
142
  ok: true,
143
- version: "1.1.0",
143
+ version: "0.1.5-beta(devices-all)",
144
144
  model: name,
145
145
  model_id: name,
146
146
  provider: "Ollama",