@krmxd/onegpt 0.1.6-beta → 0.1.7-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
@@ -7,10 +7,25 @@ commands, tools and session format as the Python version.
7
7
  ## Install
8
8
 
9
9
  ```bash
10
+ # universal launcher-installer (repo folder):
11
+ sh ../install.sh # or: sh install.sh at the repo root
12
+ ```
13
+
14
+ ```bash
15
+ # zero-install (no PATH changes needed):
16
+ npx @krmxd/onegpt
17
+
18
+ # permanent `ogpt` command:
10
19
  npm i -g @krmxd/onegpt
11
- ogpt
20
+ ogpt # alias: ogpt-node
12
21
  ```
13
22
 
23
+ > Used `npm i @krmxd/onegpt` without `-g`? npm doesn't put local bins on
24
+ > your PATH - launch with `npx @krmxd/onegpt` (OGPT prints this tip too).
25
+ >
26
+ > First launch auto-installs anything missing - including **Ollama** -
27
+ > hidden behind one line: `Installing packages...`
28
+
14
29
  ## Troubleshooting
15
30
 
16
31
  **`ModuleNotFoundError: No module named 'ogpt'` from a path like
package/bin/ogpt.js CHANGED
@@ -1,11 +1,37 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
3
 
4
+ const fs = require("fs");
5
+ const path = require("path");
6
+ const { spawn, spawnSync } = require("child_process");
7
+
8
+ // ---------------------------------------------------------------------------
9
+ // Cross-build hand-off: if this Node build can't run here (old Node, broken
10
+ // files) but the Python build can, exec it transparently - `ogpt` just works.
11
+ function pythonFallback(reason) {
12
+ const candidates = process.platform === "win32"
13
+ ? ["python", "python3"] : ["python3", "python"];
14
+ for (const bin of candidates) {
15
+ const probe = spawnSync(bin, ["-c", "import ogpt"],
16
+ { encoding: "utf-8", timeout: 60000 });
17
+ if (probe.error || probe.status !== 0) continue;
18
+ console.error(`(node build unavailable: ${reason} - using the Python build)`);
19
+ const child = spawn(bin, ["-m", "ogpt", ...process.argv.slice(2)],
20
+ { stdio: "inherit" });
21
+ child.on("exit", (c) => process.exit(c || 0));
22
+ child.on("error", () => process.exit(1));
23
+ return true;
24
+ }
25
+ return false;
26
+ }
27
+
4
28
  const [maj] = process.versions.node.split(".").map(Number);
5
29
  if (maj < 18) {
6
- console.error(`OGPT needs Node.js 18+ - you are running ${process.versions.node}.`);
7
- console.error("Update Node, then try again: https://nodejs.org");
8
- process.exit(1);
30
+ if (!pythonFallback(`Node.js ${process.versions.node} is too old (need 18+)`)) {
31
+ console.error(`OGPT needs Node.js 18+ - you are running ${process.versions.node}.`);
32
+ console.error("Update Node, then try again: https://nodejs.org");
33
+ process.exit(1);
34
+ }
9
35
  }
10
36
 
11
37
  let CLI, getConfig, VERSION_FULL;
@@ -13,6 +39,7 @@ try {
13
39
  ({ CLI, VERSION_FULL } = require("../src/cli"));
14
40
  ({ getConfig } = require("../src/config"));
15
41
  } catch (e) {
42
+ if (pythonFallback(e.message)) process.exit(0);
16
43
  console.error(`OGPT failed to start: ${e.message}`);
17
44
  if (e.code === "MODULE_NOT_FOUND") { console.error(`
18
45
  The installation looks broken or out of date. Fix it with:
@@ -36,7 +63,31 @@ Or run straight from the repo without installing:
36
63
  // 'Installing packages...' line; no-op when everything is already present.
37
64
  try { require("../src/bootstrap").ensureBoot(); } catch {}
38
65
 
66
+ // Local installs (npm i without -g) put no `ogpt` shortcut on PATH, so users
67
+ // end up launching by full file path. Detect that once and teach the fix.
68
+ function maybeHintLocalInstall() {
69
+ try {
70
+ const p = __dirname.replace(/\\/g, "/");
71
+ if (!p.includes("node_modules") || p.includes("_npx")) return;
72
+ const dirs = (process.env.PATH || "").split(path.delimiter);
73
+ const onPath = dirs.some((d) => {
74
+ try { return d && (fs.existsSync(path.join(d, "ogpt")) ||
75
+ fs.existsSync(path.join(d, "ogpt.cmd"))); } catch { return false; }
76
+ });
77
+ if (onPath) return;
78
+ const flagDir = require("os").homedir() + "/.config/ogpt";
79
+ fs.mkdirSync(flagDir, { recursive: true });
80
+ const flag = path.join(flagDir, ".local-install-hint");
81
+ if (fs.existsSync(flag)) return;
82
+ fs.writeFileSync(flag, new Date().toISOString());
83
+ console.log("\nTip: OGPT was launched by file path - no 'ogpt' command is on your PATH.");
84
+ console.log(" Run it anywhere: npx @krmxd/onegpt");
85
+ console.log(" Install shortcut: npm i -g @krmxd/onegpt\n");
86
+ } catch {}
87
+ }
88
+
39
89
  async function main() {
90
+ maybeHintLocalInstall();
40
91
  const args = process.argv.slice(2);
41
92
  const cfg = getConfig();
42
93
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@krmxd/onegpt",
3
- "version": "0.1.6-beta",
3
+ "version": "0.1.7-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": {
@@ -9,6 +9,7 @@
9
9
  },
10
10
  "scripts": {
11
11
  "start": "node bin/ogpt.js",
12
+ "postinstall": "node scripts/postinstall.js",
12
13
  "test": "node --test src/__tests__/*.test.js"
13
14
  },
14
15
  "keywords": [
@@ -28,6 +29,7 @@
28
29
  "files": [
29
30
  "src/",
30
31
  "bin/",
31
- "README.md"
32
+ "README.md",
33
+ "scripts/"
32
34
  ]
33
35
  }
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+
3
+ // Runs after `npm install`. Teaches local installs how to launch OGPT -
4
+ // npm only links bins onto PATH for GLOBAL installs, which is why
5
+ // `ogpt` "doesn't exist" after a plain `npm i @krmxd/onegpt`.
6
+ try {
7
+ const isGlobal = process.env.npm_config_global === "true";
8
+ if (isGlobal) {
9
+ console.log("OGPT installed. Launch it with: ogpt");
10
+ } else {
11
+ console.log("");
12
+ console.log("OGPT was installed locally (no 'ogpt' command on your PATH).");
13
+ console.log(" Run it right now: npx ogpt");
14
+ console.log(" Or make it global: npm i -g @krmxd/onegpt");
15
+ console.log(" From inside this folder: node node_modules/@krmxd/onegpt/bin/ogpt.js");
16
+ console.log("");
17
+ }
18
+ } catch {}
package/src/bootstrap.js CHANGED
@@ -54,7 +54,19 @@ function npmInstall(pkgs, cwd, timeout = 600000) {
54
54
  } catch { return false; }
55
55
  }
56
56
 
57
+ function isTermux() {
58
+ return (process.env.PREFIX || "").includes("/com.termux/")
59
+ || fs.existsSync("/data/data/com.termux");
60
+ }
61
+
57
62
  function installOllama(timeout = 1200000) {
63
+ if (isTermux()) {
64
+ try {
65
+ spawnSync("pkg", ["install", "-y", "ollama"],
66
+ { stdio: "ignore", timeout });
67
+ } catch {}
68
+ if (haveOllama()) return true;
69
+ }
58
70
  if (process.platform === "win32") {
59
71
  try {
60
72
  spawnSync("winget", ["install", "--silent",
@@ -67,6 +79,21 @@ function installOllama(timeout = 1200000) {
67
79
  try { return spawnSync(c, ["--version"], { stdio: "ignore" }).status === 0; }
68
80
  catch { return false; }
69
81
  };
82
+ // No fetcher at all? Try to get curl via the system package manager.
83
+ if (!has("curl") && !has("wget")) {
84
+ for (const [bin, args] of [
85
+ ["apt-get", ["install", "-y", "curl"]],
86
+ ["apk", ["add", "curl"]],
87
+ ["dnf", ["install", "-y", "curl"]],
88
+ ]) {
89
+ try {
90
+ if (spawnSync(bin, ["--version"], { stdio: "ignore" }).status === 0) {
91
+ spawnSync(bin, args, { stdio: "ignore", timeout });
92
+ break;
93
+ }
94
+ } catch {}
95
+ }
96
+ }
70
97
  let script = null;
71
98
  if (has("curl")) script = "curl -fsSL https://ollama.com/install.sh | sh";
72
99
  else if (has("wget")) script = "wget -qO- https://ollama.com/install.sh | sh";
@@ -77,15 +104,24 @@ function installOllama(timeout = 1200000) {
77
104
  return haveOllama();
78
105
  }
79
106
 
80
- function ensureBoot() {
107
+ function ensureBoot({ have = haveOllama, install = installOllama } = {}) {
81
108
  if (process.env.OGPT_NO_AUTO_INSTALL) return [];
82
109
  const need = missingPackages();
83
- const wantOllama = !haveOllama();
110
+ const wantOllama = !have();
84
111
  if (!need.length && !wantOllama) return [];
85
112
  console.log("Installing packages...");
86
113
  if (need.length) npmInstall(need.map(([, pkg]) => pkg));
87
- if (wantOllama) installOllama();
88
- return missingPackages().map(([, pkg]) => pkg);
114
+ if (wantOllama) install();
115
+ const still = missingPackages().map(([, pkg]) => pkg);
116
+ if (still.length) {
117
+ console.log("Some packages could not be installed automatically.");
118
+ console.log("Try manually: npm install " + still.join(" "));
119
+ }
120
+ if (wantOllama && !have()) {
121
+ console.log("(Ollama could not be installed automatically -");
122
+ console.log(" grab it from https://ollama.com/download to use local models)");
123
+ }
124
+ return still;
89
125
  }
90
126
 
91
127
  // Back-compat shim (deps-only, no Ollama check).
@@ -99,4 +135,4 @@ function ensurePackages() {
99
135
  }
100
136
 
101
137
  module.exports = { ensureBoot, ensurePackages, installOllama, haveOllama,
102
- npmInstall, PACKAGES };
138
+ npmInstall, isTermux, PACKAGES };
package/src/cli.js CHANGED
@@ -12,9 +12,11 @@ const Platform = require("./platform");
12
12
  const catalog = require("./catalog");
13
13
  const web = require("./web");
14
14
 
15
- const VERSION = "0.1.5-beta";
15
+ // Single source of truth: package.json version + device-build tag.
16
+ const PKG = require("../package.json");
16
17
  const VERSION_TAG = "devices-all";
17
- const VERSION_FULL = `${VERSION}(${VERSION_TAG})`;
18
+ const VERSION = PKG.version;
19
+ const VERSION_FULL = `${PKG.version}(${VERSION_TAG})`;
18
20
 
19
21
  class Command {
20
22
  constructor(name, handler, description, usage, category, ai, aliases) {
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 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";
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.7-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/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: "0.1.5-beta(devices-all)",
114
+ version: "0.1.7-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: "0.1.5-beta(devices-all)",
143
+ version: "0.1.7-beta(devices-all)",
144
144
  model: name,
145
145
  model_id: name,
146
146
  provider: "Ollama",