@krmxd/onegpt 0.0.0-beta → 0.0.2-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
@@ -24,6 +24,7 @@ ogpt
24
24
  git commit) - everything else just works
25
25
  - Token dashboard with live tok/s charts at `http://127.0.0.1:8756` (`/dash`)
26
26
  - Session history shared with the Python OGPT build (`/save`, `/load`, `/sessions`)
27
+ - resume where you left off: `ogpt --continue` (or `--session <id>`)
27
28
  - Anti-sloth guardrails: tiny local models are kept on-task until every
28
29
  requested file actually exists
29
30
 
@@ -37,5 +38,10 @@ ogpt
37
38
  | `/auto` | toggle auto-approve |
38
39
  | `/dash` | token dashboard |
39
40
  | `/new` | fresh chat |
41
+ | `/export [md|json] [file]` | save the chat as Markdown or JSON |
42
+ | `/todos` | show the current todo list |
43
+
44
+ CLI flags: `ogpt -m <model>` · `-c/--continue` · `--session <id>` ·
45
+ `--auto` · `--no-stream`.
40
46
 
41
47
  Config lives at `~/.config/ogpt/config.json`.
package/bin/ogpt.js CHANGED
@@ -1,8 +1,37 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
3
 
4
- const { CLI } = require("../src/cli");
5
- const { getConfig } = require("../src/config");
4
+ const [maj] = process.versions.node.split(".").map(Number);
5
+ 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);
9
+ }
10
+
11
+ let CLI, getConfig;
12
+ try {
13
+ ({ CLI } = require("../src/cli"));
14
+ ({ getConfig } = require("../src/config"));
15
+ } catch (e) {
16
+ console.error(`OGPT failed to start: ${e.message}`);
17
+ if (e.code === "MODULE_NOT_FOUND") {
18
+ console.error(`
19
+ The installation looks broken or out of date. Fix it with:
20
+
21
+ npm cache clean --force
22
+ npm i -g @krmxd/onegpt@latest --force
23
+
24
+ If you installed with 'npm link' and later moved or renamed the
25
+ source folder, re-link it:
26
+
27
+ cd <your-ogpt-repo>/nodejs && npm link
28
+
29
+ Or run straight from the repo without installing:
30
+
31
+ cd <your-ogpt-repo>/nodejs && node bin/ogpt.js`);
32
+ }
33
+ process.exit(1);
34
+ }
6
35
 
7
36
  async function main() {
8
37
  const args = process.argv.slice(2);
@@ -12,12 +41,18 @@ async function main() {
12
41
  let prompt = [];
13
42
  let noStream = false;
14
43
  let autoApprove = false;
44
+ let continueLast = false;
45
+ let sessionId = "";
15
46
 
16
47
  for (let i = 0; i < args.length; i++) {
17
48
  if (args[i] === "--no-stream" || args[i] === "-s") {
18
49
  noStream = true;
19
50
  } else if (args[i] === "--auto") {
20
51
  autoApprove = true;
52
+ } else if (args[i] === "--continue" || args[i] === "-c") {
53
+ continueLast = true;
54
+ } else if (args[i] === "--session") {
55
+ sessionId = args[++i] || "";
21
56
  } else if (args[i] === "--model" || args[i] === "-m") {
22
57
  const name = args[++i];
23
58
  cfg.set("active_model", cfg.resolveModel(name));
@@ -30,6 +65,8 @@ async function main() {
30
65
  Options:
31
66
  -m, --model <oGPT-name|id> Override model (e.g. -m oGPT-2a)
32
67
  -s, --no-stream Disable streaming
68
+ -c, --continue Resume your most recent chat
69
+ --session <id> Resume a specific session by ID
33
70
  --auto Auto-approve all tools
34
71
  -v, --version Show version
35
72
  -h, --help Show help`);
@@ -46,6 +83,35 @@ Options:
46
83
  if (autoApprove) {
47
84
  cli.tools._autoApprove = true;
48
85
  }
86
+ if (sessionId || continueLast) {
87
+ const fsMod = require("fs"), osMod = require("os"), pathMod = require("path");
88
+ const dir = pathMod.join(osMod.homedir(), ".config", "ogpt", "sessions");
89
+ let sid = sessionId;
90
+ if (!sid) {
91
+ try {
92
+ sid = fsMod.readdirSync(dir).filter((f) => f.endsWith(".meta.json"))
93
+ .map((f) => { try { return JSON.parse(fsMod.readFileSync(pathMod.join(dir, f), "utf-8")); } catch { return null; } })
94
+ .filter((m) => m && (m.msgs || 0) > 0)
95
+ .sort((a, b) => (b.updated || 0) - (a.updated || 0))[0]?.id || "";
96
+ } catch {}
97
+ }
98
+ const dim = (s) => `\x1b[2m${s}\x1b[0m`;
99
+ let data = null;
100
+ if (sid) {
101
+ try { data = JSON.parse(fsMod.readFileSync(pathMod.join(dir, `${sid}.json`), "utf-8")); } catch {}
102
+ }
103
+ if (Array.isArray(data) && data.length) {
104
+ cli.agent.history = data.map((d) => ({
105
+ role: d.role || "user", content: d.content || "", ts: Date.now() / 1000 }));
106
+ cli._sessionId = sid;
107
+ console.log(dim(continueLast && !sessionId
108
+ ? `Continued last chat: ${sid}` : `Resumed session: ${sid}`));
109
+ } else if (sessionId) {
110
+ console.error(`Session not found: ${sessionId}`);
111
+ process.exitCode = 1;
112
+ return;
113
+ }
114
+ }
49
115
 
50
116
  if (prompt.length) {
51
117
  await cli.runSingle(prompt.join(" "));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@krmxd/onegpt",
3
- "version": "0.0.0-beta",
3
+ "version": "0.0.2-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": {
@@ -21,9 +21,6 @@
21
21
  ],
22
22
  "author": "OGPT Team",
23
23
  "license": "MIT",
24
- "dependencies": {
25
- "ollama": "^0.5.0"
26
- },
27
24
  "engines": {
28
25
  "node": ">=18.0.0"
29
26
  },
package/src/cli.js CHANGED
@@ -284,6 +284,10 @@ class CLI {
284
284
  "List plugins", "/plugins", "system", true));
285
285
  reg(new Command("/mcp", () => this.info("No MCP servers configured."),
286
286
  "List MCP servers", "/mcp", "system", true));
287
+ reg(new Command("/export", (a) => this.cmdExport(a),
288
+ "Export the chat as Markdown or JSON", "/export [md|json] [file]", "general"));
289
+ reg(new Command("/todos", () => this.cmdTodos(),
290
+ "Show the current todo list", "/todos", "tools", true));
287
291
 
288
292
  reg(new Command("/tools", () => this.cmdTools(), "List available tools", "/tools", "tools", true));
289
293
  reg(new Command("/approve", (a) => { this.print(this.tools.toggleApprove(a[0] || "")); }, "Auto-approve a specific tool", "/approve <tool>", "tools"));
@@ -683,8 +687,46 @@ class CLI {
683
687
  this.print(`## Project Structure\n\`\`\`\n${lines.join("\n")}\n\`\`\``);
684
688
  }
685
689
 
686
- cmdSession(args) {
687
- const sub = (args && args[0]) || "list";
690
+ cmdExport(args) {
691
+ const fmt = args && args[0] === "json" ? "json" : "md";
692
+ const file = (args && args[1]) ||
693
+ `ogpt-chat-${new Date().toISOString().slice(0, 19).replace(/[:T]/g, "-")}.${fmt}`;
694
+ try {
695
+ const msgs = this.agent.history.filter((m) => m.role !== "tool" &&
696
+ !String(m.content || "").startsWith("[Conversation summary]"));
697
+ if (!msgs.length) { this.info("Nothing to export - no conversation yet."); return; }
698
+ let body;
699
+ if (fmt === "json") {
700
+ body = JSON.stringify({ exported: new Date().toISOString(),
701
+ model: this.cfg.displayName(), messages: msgs }, null, 2);
702
+ } else {
703
+ body = `# OGPT chat export\n\n_${new Date().toLocaleString()} · ${this.cfg.displayName()} · ${msgs.length} messages_\n\n`
704
+ + msgs.map((m) => `## ${m.role === "user" ? "You" : this.cfg.displayName()}\n\n${m.content}\n`).join("\n");
705
+ }
706
+ fs.writeFileSync(file, body);
707
+ this.success(`Exported ${msgs.length} messages to ${file}`);
708
+ } catch (e) {
709
+ this.error(`Export failed: ${e.message}`);
710
+ }
711
+ }
712
+
713
+ _todosFile() {
714
+ return path.join(os.homedir(), ".config", "ogpt", "todos.json");
715
+ }
716
+
717
+ cmdTodos() {
718
+ let todos = [];
719
+ try {
720
+ const data = JSON.parse(fs.readFileSync(this._todosFile(), "utf-8"));
721
+ todos = Array.isArray(data) ? data : data.todos || [];
722
+ } catch {}
723
+ if (!todos.length) { this.info("No todos."); return; }
724
+ const rows = todos.map((t, i) =>
725
+ [String(i + 1), t.done ? "[x]" : "[ ]", String(t.text || "")]);
726
+ this._table("Todos", ["#", "Done", "Task"], rows);
727
+ }
728
+
729
+ cmdSession(args) { const sub = (args && args[0]) || "list";
688
730
  if (sub === "list") return this.cmdSessions();
689
731
  if (sub === "save") return this.cmdSave();
690
732
  if (sub === "new" || sub === "fresh") return this.cmdNew(args.slice(1));
@@ -716,12 +758,16 @@ class CLI {
716
758
  const metaPath = path.join(dir, `${this._sessionId}.meta.json`);
717
759
  try { meta = JSON.parse(fs.readFileSync(metaPath, "utf-8")); } catch {}
718
760
  if (!meta.id) {
719
- meta = { id: this._sessionId,
720
- title: `Session ${new Date().toISOString().slice(0, 16).replace("T", " ")}`,
721
- created: Date.now() / 1000, ...meta };
761
+ meta = { id: this._sessionId, title: "", created: Date.now() / 1000, ...meta };
722
762
  }
723
763
  meta.updated = Date.now() / 1000;
724
764
  meta.msgs = data.length;
765
+ // Auto-title from the first real user message (same rule as Python).
766
+ const existing = (meta.title || "").trim();
767
+ if (!existing || existing.startsWith("Session ")) {
768
+ const first = data.find((m) => m.role === "user" && String(m.content || "").trim());
769
+ if (first) meta.title = String(first.content).trim().slice(0, 50);
770
+ }
725
771
  fs.writeFileSync(metaPath, JSON.stringify(meta));
726
772
  return this._sessionId;
727
773
  } catch {
package/src/tools.js CHANGED
@@ -397,8 +397,35 @@ async function runShell(command, cwd = ".", timeout = 30000) {
397
397
 
398
398
  // Run code with an interpreter directly (argv, never a shell) so user code
399
399
  // can't be mangled by quoting and packages install safely.
400
+ const _BIN_FALLBACKS = {
401
+ node: ["/usr/bin/node", "/usr/local/bin/node",
402
+ "/data/data/com.termux/files/usr/bin/node"],
403
+ python3: ["/usr/bin/python3", "/usr/local/bin/python3",
404
+ "/data/data/com.termux/files/usr/bin/python3"],
405
+ python: ["/usr/bin/python", "/data/data/com.termux/files/usr/bin/python"],
406
+ pip: ["/usr/bin/pip", "/usr/local/bin/pip",
407
+ "/data/data/com.termux/files/usr/bin/pip"],
408
+ pip3: ["/usr/bin/pip3", "/data/data/com.termux/files/usr/bin/pip3"],
409
+ npm: ["/usr/bin/npm", "/usr/local/bin/npm",
410
+ "/data/data/com.termux/files/usr/bin/npm"],
411
+ };
412
+
413
+ function resolveBin(bin) {
414
+ // spawnSync needs an absolute path when PATH is stripped in some
415
+ // sandboxes/Termux sessions - probe fallbacks before giving up.
416
+ try {
417
+ const found = require("child_process").execSync(
418
+ `command -v ${bin} 2>/dev/null`, { encoding: "utf-8", timeout: 3000 }).trim();
419
+ if (found) return found;
420
+ } catch {}
421
+ for (const p of _BIN_FALLBACKS[bin] || []) {
422
+ try { if (fs.existsSync(p)) return p; } catch {}
423
+ }
424
+ return bin; // let spawnSync produce its own ENOENT
425
+ }
426
+
400
427
  function runInterpreter(bin, argv, cwd, timeout) {
401
- const r = spawnSync(bin, argv, {
428
+ const r = spawnSync(resolveBin(bin), argv, {
402
429
  cwd: path.resolve(cwd || "."), timeout: timeout || 30000,
403
430
  encoding: "utf-8", maxBuffer: 16 * 1024 * 1024,
404
431
  env: { ...process.env, PYTHONDONTWRITEBYTECODE: "1" },
@@ -786,6 +813,9 @@ const IMPLEMENTATIONS = {
786
813
  if (!code.trim() && !file) {
787
814
  return new ToolResult("", "Provide 'code' (Python source to run) or 'path' (a .py file)", false);
788
815
  }
816
+ if (file && !fs.existsSync(path.resolve(args.cwd || ".", file))) {
817
+ return new ToolResult("", `File not found: ${file} (use a path relative to your workspace)`, false);
818
+ }
789
819
  const argv = file ? [file] : ["-c", code];
790
820
  return runInterpreter("python3", argv, args.cwd || ".", parseInt(args.timeout || 60, 10) * 1000);
791
821
  },
@@ -796,7 +826,25 @@ const IMPLEMENTATIONS = {
796
826
  if (!code.trim() && !file) {
797
827
  return new ToolResult("", "Provide 'code' (JavaScript source to run) or 'path' (a .js file)", false);
798
828
  }
799
- const argv = file ? [file] : ["-e", code];
829
+ let argv;
830
+ if (file) {
831
+ if (!fs.existsSync(path.resolve(args.cwd || ".", file))) {
832
+ return new ToolResult("", `File not found: ${file} (use a path relative to your workspace)`, false);
833
+ }
834
+ argv = [file];
835
+ } else if (/^\s*import\s|^\s*export\s|await\s+import\(/m.test(code)) {
836
+ // ESM code can't run through -e; stage it as a temp .mjs module.
837
+ const tmp = path.join(os.tmpdir(), `ogpt-run-${Date.now()}.mjs`);
838
+ fs.writeFileSync(tmp, code);
839
+ try {
840
+ return runInterpreter("node", [tmp], args.cwd || ".",
841
+ parseInt(args.timeout || 60, 10) * 1000);
842
+ } finally {
843
+ try { fs.unlinkSync(tmp); } catch {}
844
+ }
845
+ } else {
846
+ argv = ["-e", code];
847
+ }
800
848
  return runInterpreter("node", argv, args.cwd || ".", parseInt(args.timeout || 60, 10) * 1000);
801
849
  },
802
850