openbot 0.2.5 → 0.2.7

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/dist/cli.js CHANGED
@@ -3,19 +3,34 @@ import { Command } from "commander";
3
3
  import * as readline from "node:readline/promises";
4
4
  import * as fs from "node:fs/promises";
5
5
  import * as path from "node:path";
6
+ import { spawn } from "node:child_process";
6
7
  import { saveConfig, resolvePath, DEFAULT_BASE_DIR } from "./config.js";
7
8
  import { startServer } from "./server.js";
8
9
  import { getPluginMetadata } from "./registry/plugin-loader.js";
9
10
  import { checkGitHubRepo, checkNpmPackage, parsePluginInstallSource, parseAgentInstallSource, installPluginFromSource, installAgentFromSource, } from "./installers.js";
10
11
  const program = new Command();
12
+ const REQUIRED_NODE_VERSION = "20.12.0";
13
+ function checkNodeVersion() {
14
+ const [major, minor, patch] = process.versions.node.split(".").map(Number);
15
+ const [reqMajor, reqMinor, reqPatch] = REQUIRED_NODE_VERSION.split(".").map(Number);
16
+ const isOld = major < reqMajor ||
17
+ (major === reqMajor && minor < reqMinor) ||
18
+ (major === reqMajor && minor === reqMinor && patch < reqPatch);
19
+ if (isOld) {
20
+ console.warn(`\n⚠️ WARNING: You are using Node.js ${process.version}.`);
21
+ console.warn(` OpenBot works best with Node.js >=${REQUIRED_NODE_VERSION}.`);
22
+ console.warn(` You may encounter "ERR_REQUIRE_ESM" or other compatibility issues on older versions.\n`);
23
+ }
24
+ }
25
+ checkNodeVersion();
11
26
  program
12
27
  .name("openbot")
13
28
  .description("OpenBot CLI - Secure and easy configuration")
14
- .version("0.2.5");
15
- async function installPlugin(source, quiet = false) {
29
+ .version("0.2.7");
30
+ async function installPlugin(source, id, quiet = false) {
16
31
  try {
17
32
  const parsed = parsePluginInstallSource(source);
18
- const name = await installPluginFromSource(parsed, { quiet });
33
+ const name = await installPluginFromSource(parsed, { quiet, id });
19
34
  return name;
20
35
  }
21
36
  catch (err) {
@@ -26,10 +41,10 @@ async function installPlugin(source, quiet = false) {
26
41
  throw err;
27
42
  }
28
43
  }
29
- async function installAgent(source) {
44
+ async function installAgent(source, id) {
30
45
  try {
31
46
  const parsed = parseAgentInstallSource(source);
32
- const name = await installAgentFromSource(parsed);
47
+ const name = await installAgentFromSource(parsed, { id });
33
48
  return name;
34
49
  }
35
50
  catch (err) {
@@ -37,6 +52,9 @@ async function installAgent(source) {
37
52
  process.exit(1);
38
53
  }
39
54
  }
55
+ function shellEscape(arg) {
56
+ return `'${arg.replace(/'/g, `'\\''`)}'`;
57
+ }
40
58
  program
41
59
  .command("configure")
42
60
  .description("Configure OpenBot model and settings")
@@ -85,6 +103,8 @@ program
85
103
  console.log("Alternatively, you can set the environment variable:");
86
104
  console.log(provider === "openai" ? " export OPENAI_API_KEY=your-key" : " export ANTHROPIC_API_KEY=your-key");
87
105
  console.log("------------------------------------------");
106
+ console.log("\n🚀 TIP: Use 'openbot up' to start the server and web UI together.");
107
+ console.log("------------------------------------------\n");
88
108
  rl.close();
89
109
  });
90
110
  program
@@ -96,6 +116,43 @@ program
96
116
  .action(async (options) => {
97
117
  await startServer(options);
98
118
  });
119
+ program
120
+ .command("up")
121
+ .description("Start OpenBot server and web dashboard together")
122
+ .option("-p, --port <number>", "Port to listen on")
123
+ .option("--openai-api-key <key>", "OpenAI API Key")
124
+ .option("--anthropic-api-key <key>", "Anthropic API Key")
125
+ .action(async (options) => {
126
+ const serverArgs = ["openbot", "server"];
127
+ if (options.port)
128
+ serverArgs.push("--port", String(options.port));
129
+ if (options.openaiApiKey)
130
+ serverArgs.push("--openai-api-key", options.openaiApiKey);
131
+ if (options.anthropicApiKey)
132
+ serverArgs.push("--anthropic-api-key", options.anthropicApiKey);
133
+ const serverCommand = serverArgs.map(shellEscape).join(" ");
134
+ await new Promise((resolve, reject) => {
135
+ const child = spawn("npx", [
136
+ "-y",
137
+ "concurrently",
138
+ "--kill-others",
139
+ "--names",
140
+ "SERVER,WEBUI",
141
+ "--prefix",
142
+ "{name}",
143
+ "--prefix-colors",
144
+ "blue.bold,green.bold",
145
+ serverCommand,
146
+ "openbot-web",
147
+ ], { stdio: "inherit" });
148
+ child.on("error", reject);
149
+ child.on("exit", (code) => {
150
+ if (typeof code === "number")
151
+ process.exitCode = code;
152
+ resolve();
153
+ });
154
+ });
155
+ });
99
156
  program
100
157
  .command("add <name>")
101
158
  .description("Add an agent or plugin by name (auto-resolves to GitHub/NPM)")
@@ -103,7 +160,7 @@ program
103
160
  // 1. Try as Agent
104
161
  const agentRepo = `meetopenbot/agent-${name}`;
105
162
  if (checkGitHubRepo(agentRepo)) {
106
- await installAgent(agentRepo);
163
+ await installAgent(agentRepo, `agent-${name}`);
107
164
  return;
108
165
  }
109
166
  // 2. Try as Plugin
@@ -119,13 +176,13 @@ program
119
176
  // Check GitHub Plugin
120
177
  const pluginGhRepo = `meetopenbot/plugin-${name}`;
121
178
  if (checkGitHubRepo(pluginGhRepo)) {
122
- await installPlugin(pluginGhRepo);
179
+ await installPlugin(pluginGhRepo, `plugin-${name}`);
123
180
  return;
124
181
  }
125
182
  // Check NPM Plugin
126
183
  const pluginNpmPkg = `@melony/plugin-${name}`;
127
184
  if (checkNpmPackage(pluginNpmPkg)) {
128
- await installPlugin(pluginNpmPkg);
185
+ await installPlugin(pluginNpmPkg, `plugin-${name}`);
129
186
  return;
130
187
  }
131
188
  console.error(`❌ Could not find agent or plugin named "${name}" in official repositories.`);
@@ -71,7 +71,7 @@ export async function setupPluginRegistry(resolvedBaseDir, model, options) {
71
71
  // ── Custom agents and plugins ────────────────────────────────────
72
72
  const agentsDir = path.join(resolvedBaseDir, "agents");
73
73
  const pluginsDir = path.join(resolvedBaseDir, "plugins");
74
- await discoverPlugins(agentsDir, registry, model, options);
75
74
  await discoverPlugins(pluginsDir, registry, model, options);
75
+ await discoverPlugins(agentsDir, registry, model, options);
76
76
  return registry;
77
77
  }
@@ -3,7 +3,7 @@ import * as path from "node:path";
3
3
  import { execFileSync } from "node:child_process";
4
4
  import { tmpdir } from "node:os";
5
5
  import { resolvePath, DEFAULT_BASE_DIR, loadConfig } from "./config.js";
6
- import { getPluginMetadata, readAgentConfig, ensurePluginReady } from "./registry/plugin-loader.js";
6
+ import { readAgentConfig, ensurePluginReady } from "./registry/plugin-loader.js";
7
7
  const BUILT_IN_PLUGIN_NAMES = new Set(["shell", "file-system", "approval"]);
8
8
  function run(command, args, options) {
9
9
  execFileSync(command, args, {
@@ -15,9 +15,6 @@ function log(message, quiet) {
15
15
  if (!quiet)
16
16
  console.log(message);
17
17
  }
18
- function githubRepoToCloneUrl(repo) {
19
- return `https://github.com/${repo}.git`;
20
- }
21
18
  function getBaseDir() {
22
19
  const cfg = loadConfig();
23
20
  const baseDir = cfg.baseDir || DEFAULT_BASE_DIR;
@@ -28,7 +25,8 @@ async function directoryExists(targetPath) {
28
25
  }
29
26
  export function checkGitHubRepo(repo) {
30
27
  try {
31
- run("git", ["ls-remote", githubRepoToCloneUrl(repo)], { quiet: true });
28
+ const url = `https://github.com/${repo}.git`;
29
+ run("git", ["ls-remote", url], { quiet: true });
32
30
  return true;
33
31
  }
34
32
  catch {
@@ -44,7 +42,7 @@ export function checkNpmPackage(pkg) {
44
42
  return false;
45
43
  }
46
44
  }
47
- export function parsePluginInstallSource(source) {
45
+ export function parseSource(source) {
48
46
  const normalized = source.trim();
49
47
  const isGithub = (normalized.includes("/") || normalized.startsWith("github:"))
50
48
  && !normalized.startsWith("/")
@@ -64,26 +62,33 @@ export function parsePluginInstallSource(source) {
64
62
  }
65
63
  return { type: "local", value: path.resolve(normalized) };
66
64
  }
67
- export function parseAgentInstallSource(source) {
68
- const normalized = source.trim();
69
- if (normalized.startsWith("github:")) {
70
- return { type: "github", value: normalized.slice(7) };
71
- }
72
- return { type: "github", value: normalized };
73
- }
74
- export async function installPluginFromSource(source, options = {}) {
65
+ export async function installExtension(type, source, options = {}) {
75
66
  const quiet = !!options.quiet;
76
- const tempDir = path.join(tmpdir(), `openbot-plugin-install-${Date.now()}`);
77
67
  const baseDir = getBaseDir();
78
- const pluginRoot = path.join(baseDir, "plugins");
79
- await fs.mkdir(pluginRoot, { recursive: true });
68
+ const targetRoot = path.join(baseDir, type === "agent" ? "agents" : "plugins");
69
+ await fs.mkdir(targetRoot, { recursive: true });
70
+ // 1. Determine the folder name (the "id") - ALWAYS based on source or explicit id
71
+ let id = options.id;
72
+ if (!id) {
73
+ if (source.type === "github") {
74
+ id = path.basename(source.value); // e.g. "agent-browser"
75
+ }
76
+ else if (source.type === "npm") {
77
+ id = source.value.split("/").pop(); // e.g. "@melony/plugin-test" -> "plugin-test"
78
+ }
79
+ else {
80
+ id = path.basename(source.value);
81
+ }
82
+ }
83
+ const targetDir = path.join(targetRoot, id);
84
+ const tempDir = path.join(tmpdir(), `openbot-install-${Date.now()}-${id}`);
80
85
  try {
86
+ log(`📦 Installing ${type} "${id}" from ${source.type}...`, quiet);
81
87
  if (source.type === "github") {
82
- log(`📦 Installing plugin from: ${githubRepoToCloneUrl(source.value)}`, quiet);
83
- run("git", ["clone", "--depth", "1", githubRepoToCloneUrl(source.value), tempDir], { quiet });
88
+ const url = `https://github.com/${source.value}.git`;
89
+ run("git", ["clone", "--depth", "1", url, tempDir], { quiet });
84
90
  }
85
91
  else if (source.type === "npm") {
86
- log(`📦 Installing plugin from: ${source.value}`, quiet);
87
92
  await fs.mkdir(tempDir, { recursive: true });
88
93
  run("npm", ["install", source.value, "--prefix", tempDir], { quiet });
89
94
  const pkgFolder = path.join(tempDir, "node_modules", source.value);
@@ -93,55 +98,34 @@ export async function installPluginFromSource(source, options = {}) {
93
98
  await fs.rename(moveTemp, tempDir);
94
99
  }
95
100
  else {
96
- log(`📦 Installing plugin from: ${source.value}`, quiet);
97
101
  await fs.mkdir(tempDir, { recursive: true });
98
102
  await fs.cp(source.value, tempDir, { recursive: true });
99
103
  }
100
- const { name } = await getPluginMetadata(tempDir);
101
- const targetDir = path.join(pluginRoot, name);
102
104
  if (await directoryExists(targetDir)) {
103
- log(`⚠️ Plugin "${name}" already exists. Overwriting...`, quiet);
105
+ log(`⚠️ Removing existing folder: ${targetDir}`, quiet);
104
106
  await fs.rm(targetDir, { recursive: true, force: true });
105
107
  }
106
108
  await fs.rename(tempDir, targetDir);
107
- log(`✅ Moved to: ${targetDir}`, quiet);
108
- log(`⚙️ Preparing plugin "${name}"...`, quiet);
109
+ log(`✅ Installed to: ${targetDir}`, quiet);
110
+ // Prepare dependencies and build
109
111
  await ensurePluginReady(targetDir);
110
- log(`\n🎉 Successfully installed plugin: ${name}`, quiet);
111
- return name;
112
- }
113
- catch (error) {
114
- await fs.rm(tempDir, { recursive: true, force: true }).catch(() => undefined);
115
- throw error;
116
- }
117
- }
118
- export async function installAgentFromSource(source, options = {}) {
119
- const quiet = !!options.quiet;
120
- const tempDir = path.join(tmpdir(), `openbot-agent-install-${Date.now()}`);
121
- const baseDir = getBaseDir();
122
- const agentRoot = path.join(baseDir, "agents");
123
- await fs.mkdir(agentRoot, { recursive: true });
124
- try {
125
- log(`🤖 Installing agent from: ${githubRepoToCloneUrl(source.value)}`, quiet);
126
- run("git", ["clone", "--depth", "1", githubRepoToCloneUrl(source.value), tempDir], { quiet });
127
- const config = await readAgentConfig(tempDir);
128
- const name = config.name || path.basename(source.value).replace(/^agent-/, "");
129
- const targetDir = path.join(agentRoot, name);
130
- if (await directoryExists(targetDir)) {
131
- log(`⚠️ Agent "${name}" already exists. Overwriting...`, quiet);
132
- await fs.rm(targetDir, { recursive: true, force: true });
112
+ // If it's an agent, check for missing plugins
113
+ if (type === "agent") {
114
+ await installMissingPluginsFromAgent(targetDir, { quiet });
133
115
  }
134
- await fs.rename(tempDir, targetDir);
135
- log(`✅ Moved to: ${targetDir}`, quiet);
136
- await installMissingPluginsFromAgent(targetDir, { quiet });
137
- log(`\n🎉 Successfully installed agent: ${name}`, quiet);
138
- return name;
116
+ log(`🎉 Successfully installed ${type}: ${id}`, quiet);
117
+ return id;
139
118
  }
140
119
  catch (error) {
141
120
  await fs.rm(tempDir, { recursive: true, force: true }).catch(() => undefined);
142
121
  throw error;
143
122
  }
144
123
  }
124
+ // Backward compatibility aliases
125
+ export const installPluginFromSource = (source, opts) => installExtension("plugin", source, opts);
126
+ export const installAgentFromSource = (source, opts) => installExtension("agent", source, opts);
127
+ export const parsePluginInstallSource = parseSource;
128
+ export const parseAgentInstallSource = parseSource;
145
129
  export async function installMissingPluginsFromAgent(agentFolder, options = {}) {
146
130
  const quiet = !!options.quiet;
147
131
  const config = await readAgentConfig(agentFolder);
@@ -150,21 +134,23 @@ export async function installMissingPluginsFromAgent(agentFolder, options = {})
150
134
  const pluginName = typeof pluginItem === "string" ? pluginItem : pluginItem.name;
151
135
  if (!pluginName || BUILT_IN_PLUGIN_NAMES.has(pluginName))
152
136
  continue;
137
+ // Check if it already exists as a folder in plugins/
153
138
  const pluginPath = path.join(baseDir, "plugins", pluginName);
154
- const existsLocally = await directoryExists(pluginPath);
155
- if (existsLocally)
139
+ const prefixedPluginPath = path.join(baseDir, "plugins", `plugin-${pluginName}`);
140
+ if (await (directoryExists(pluginPath)) || await (directoryExists(prefixedPluginPath))) {
156
141
  continue;
142
+ }
157
143
  log(`🔍 Agent needs plugin "${pluginName}". Searching...`, quiet);
158
144
  const ghRepo = `meetopenbot/plugin-${pluginName}`;
159
145
  if (checkGitHubRepo(ghRepo)) {
160
- await installPluginFromSource({ type: "github", value: ghRepo }, { quiet });
146
+ await installExtension("plugin", { type: "github", value: ghRepo }, { quiet });
161
147
  continue;
162
148
  }
163
149
  const npmPkg = `@melony/plugin-${pluginName}`;
164
150
  if (checkNpmPackage(npmPkg)) {
165
- await installPluginFromSource({ type: "npm", value: npmPkg }, { quiet });
151
+ await installExtension("plugin", { type: "npm", value: npmPkg }, { quiet });
166
152
  continue;
167
153
  }
168
- log(`⚠️ Could not find plugin "${pluginName}" for this agent. You may need to install it manually.`, quiet);
154
+ log(`⚠️ Could not find plugin "${pluginName}" for this agent.`, quiet);
169
155
  }
170
156
  }
@@ -67,7 +67,7 @@ export async function installMarketplaceAgent(agentId) {
67
67
  if (agent.source.type !== "github") {
68
68
  throw new Error(`Marketplace agent "${agentId}" has unsupported source type "${agent.source.type}"`);
69
69
  }
70
- const installedName = await installAgentFromSource({ type: "github", value: agent.source.value });
70
+ const installedName = await installAgentFromSource({ type: "github", value: agent.source.value }, { id: agent.id });
71
71
  return { installedName, agent };
72
72
  }
73
73
  export async function installMarketplacePlugin(pluginId) {
@@ -75,6 +75,6 @@ export async function installMarketplacePlugin(pluginId) {
75
75
  const plugin = registry.plugins.find((entry) => entry.id === pluginId);
76
76
  if (!plugin)
77
77
  throw new Error(`Plugin "${pluginId}" was not found in marketplace`);
78
- const installedName = await installPluginFromSource(plugin.source);
78
+ const installedName = await installPluginFromSource(plugin.source, { id: plugin.id });
79
79
  return { installedName, plugin };
80
80
  }
@@ -8,6 +8,13 @@ import { llmPlugin } from "../plugins/llm/index.js";
8
8
  import { createModel } from "../models.js";
9
9
  import { resolvePath, DEFAULT_AGENT_MD } from "../config.js";
10
10
  // ── Helpers ──────────────────────────────────────────────────────────
11
+ function toTitleCaseFromSlug(value) {
12
+ return value
13
+ .split(/[-_]+/)
14
+ .filter(Boolean)
15
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
16
+ .join(" ") || "Agent";
17
+ }
11
18
  async function fileExists(filePath) {
12
19
  return fs.access(filePath).then(() => true).catch(() => false);
13
20
  }
@@ -138,7 +145,7 @@ async function loadToolPluginsFromDir(dir) {
138
145
  if (!indexPath)
139
146
  continue;
140
147
  try {
141
- const module = await import(pathToFileURL(indexPath).href);
148
+ const module = await import(pathToFileURL(indexPath).href + `?update=${Date.now()}`);
142
149
  const entryData = module.plugin || module.default || module.entry;
143
150
  if (entryData && typeof entryData.factory === "function") {
144
151
  plugins.push({
@@ -212,12 +219,16 @@ export async function discoverPlugins(dir, registry, defaultModel, options) {
212
219
  if (!indexPath)
213
220
  continue;
214
221
  try {
215
- const module = await import(pathToFileURL(indexPath).href);
222
+ const module = await import(pathToFileURL(indexPath).href + `?update=${Date.now()}`);
216
223
  const codeAgentDef = module.agent;
217
224
  const entryData = module.plugin || module.default || module.entry;
218
225
  if (codeAgentDef && typeof codeAgentDef.factory === "function") {
219
226
  const meta = await getPluginMetadata(pluginDir);
220
- const name = codeAgentDef.name || meta.name || "Unnamed Agent";
227
+ const folderName = path.basename(pluginDir);
228
+ let name = codeAgentDef.name || meta.name;
229
+ if (!name || /^Unnamed\s+(Plugin|Tool|Agent)$/i.test(name)) {
230
+ name = toTitleCaseFromSlug(folderName);
231
+ }
221
232
  const description = codeAgentDef.description || meta.description || "Code Agent";
222
233
  registry.register({
223
234
  name,
@@ -232,8 +243,13 @@ export async function discoverPlugins(dir, registry, defaultModel, options) {
232
243
  }
233
244
  else if (entryData && typeof entryData.factory === "function") {
234
245
  const meta = await getPluginMetadata(pluginDir);
246
+ const folderName = path.basename(pluginDir);
247
+ let name = entryData.name || meta.name;
248
+ if (!name || /^Unnamed\s+(Plugin|Tool|Agent)$/i.test(name)) {
249
+ name = toTitleCaseFromSlug(folderName);
250
+ }
235
251
  const pluginEntry = {
236
- name: entryData.name || meta.name || "Unnamed Tool",
252
+ name,
237
253
  description: entryData.description || meta.description || "Tool plugin",
238
254
  type: "tool",
239
255
  plugin: entryData.factory,
@@ -261,12 +277,15 @@ export async function discoverPlugins(dir, registry, defaultModel, options) {
261
277
  const indexPath = await findIndexFile(agentDir);
262
278
  if (!indexPath)
263
279
  continue;
264
- const module = await import(pathToFileURL(indexPath).href);
280
+ const module = await import(pathToFileURL(indexPath).href + `?update=${Date.now()}`);
265
281
  const definition = module.agent || module.plugin || module.default || module.entry;
266
282
  if (definition && typeof definition.factory === "function") {
267
283
  const config = await readAgentConfig(agentDir);
268
284
  const meta = await getPluginMetadata(agentDir);
269
- const name = config.name || definition.name || meta.name || "Unnamed Agent";
285
+ let name = config.name || definition.name || meta.name;
286
+ if (!name || /^Unnamed\s+(Plugin|Tool|Agent)$/i.test(name)) {
287
+ name = toTitleCaseFromSlug(folderName);
288
+ }
270
289
  const description = definition.description || config.description || "TS Agent";
271
290
  registry.register({
272
291
  name,
@@ -284,7 +303,10 @@ export async function discoverPlugins(dir, registry, defaultModel, options) {
284
303
  // Declarative Agent — AGENT.md only, auto-wrapped with llmPlugin.
285
304
  const config = await readAgentConfig(agentDir);
286
305
  const meta = await getPluginMetadata(agentDir);
287
- const resolvedName = config.name || meta.name || "Unnamed Agent";
306
+ let resolvedName = config.name || meta.name;
307
+ if (!resolvedName || /^Unnamed\s+(Plugin|Tool|Agent)$/i.test(resolvedName)) {
308
+ resolvedName = toTitleCaseFromSlug(folderName);
309
+ }
288
310
  const resolvedDescription = config.description || meta.description || "No description";
289
311
  const agentModel = config.model
290
312
  ? createModel({ ...options, model: config.model })
@@ -365,7 +387,7 @@ export async function listPlugins(dir) {
365
387
  continue;
366
388
  }
367
389
  try {
368
- const module = await import(pathToFileURL(indexPath).href);
390
+ const module = await import(pathToFileURL(indexPath).href + `?update=${Date.now()}`);
369
391
  const codeAgentDef = module.agent;
370
392
  const toolEntry = module.plugin || module.default || module.entry;
371
393
  if (codeAgentDef && typeof codeAgentDef.factory === "function") {
package/dist/server.js CHANGED
@@ -7,6 +7,7 @@ import { createOpenBot } from "./open-bot.js";
7
7
  import { loadConfig, saveConfig, isConfigured, resolvePath, DEFAULT_BASE_DIR } from "./config.js";
8
8
  import { loadSession, saveSession, logEvent, loadEvents, listSessions } from "./session.js";
9
9
  import { listPlugins } from "./registry/index.js";
10
+ import { readAgentConfig } from "./registry/plugin-loader.js";
10
11
  import { exec } from "node:child_process";
11
12
  import os from "node:os";
12
13
  import path from "node:path";
@@ -21,6 +22,8 @@ import { getMarketplaceRegistry, installMarketplaceAgent, installMarketplacePlug
21
22
  import { getVersionStatus } from "./version.js";
22
23
  export async function startServer(options = {}) {
23
24
  const config = loadConfig();
25
+ const baseDir = config.baseDir || DEFAULT_BASE_DIR;
26
+ const resolvedBaseDir = resolvePath(baseDir);
24
27
  const PORT = Number(options.port ?? config.port ?? process.env.PORT ?? 4001);
25
28
  const app = express();
26
29
  const createRuntime = () => createOpenBot({
@@ -61,12 +64,16 @@ export async function startServer(options = {}) {
61
64
  void reloadRuntime();
62
65
  }, 800);
63
66
  };
64
- const openBotDir = path.join(os.homedir(), ".openbot");
67
+ const openBotDir = resolvedBaseDir;
68
+ const agentsDir = path.join(openBotDir, "agents");
69
+ const pluginsDir = path.join(openBotDir, "plugins");
70
+ await fs.mkdir(agentsDir, { recursive: true });
71
+ await fs.mkdir(pluginsDir, { recursive: true });
65
72
  const watcher = chokidar.watch([
66
73
  path.join(openBotDir, "config.json"),
67
74
  path.join(openBotDir, "AGENT.md"),
68
- path.join(openBotDir, "agents", "**", "*"),
69
- path.join(openBotDir, "plugins", "**", "*"),
75
+ agentsDir,
76
+ pluginsDir,
70
77
  ], {
71
78
  ignoreInitial: true,
72
79
  awaitWriteFinish: {
@@ -412,6 +419,7 @@ export async function startServer(options = {}) {
412
419
  updates.anthropicApiKey = anthropic_api_key.trim();
413
420
  if (Object.keys(updates).length > 0) {
414
421
  saveConfig(updates);
422
+ scheduleReload();
415
423
  }
416
424
  res.json({ success: true });
417
425
  });
@@ -523,6 +531,7 @@ export async function startServer(options = {}) {
523
531
  }
524
532
  try {
525
533
  const result = await installMarketplaceAgent(id.trim());
534
+ scheduleReload();
526
535
  res.json({ success: true, installedName: result.installedName, item: result.agent });
527
536
  }
528
537
  catch (error) {
@@ -537,6 +546,7 @@ export async function startServer(options = {}) {
537
546
  }
538
547
  try {
539
548
  const result = await installMarketplacePlugin(id.trim());
549
+ scheduleReload();
540
550
  res.json({ success: true, installedName: result.installedName, item: result.plugin });
541
551
  }
542
552
  catch (error) {
@@ -604,6 +614,7 @@ export async function startServer(options = {}) {
604
614
  }
605
615
  const consolidated = matter.stringify(md, frontmatter);
606
616
  await fs.writeFile(mdPath, consolidated, "utf-8");
617
+ scheduleReload();
607
618
  res.json({ success: true });
608
619
  }
609
620
  catch (err) {
@@ -779,12 +790,32 @@ export async function startServer(options = {}) {
779
790
  const baseDir = cfg.baseDir || DEFAULT_BASE_DIR;
780
791
  const resolvedBaseDir = resolvePath(baseDir);
781
792
  const defaultName = cfg.name || "OpenBot";
793
+ // 1. Resolve agent folder
794
+ let agentFolder = null;
795
+ if (name === defaultName || name === "default") {
796
+ agentFolder = resolvedBaseDir;
797
+ }
798
+ else {
799
+ agentFolder = await resolveAgentFolder(name, resolvedBaseDir);
800
+ }
801
+ // 2. Check for remote image in AGENT.md if folder exists
802
+ if (agentFolder) {
803
+ try {
804
+ const { image } = await readAgentConfig(agentFolder);
805
+ if (image && (image.startsWith("http://") || image.startsWith("https://"))) {
806
+ return res.redirect(image);
807
+ }
808
+ }
809
+ catch {
810
+ // ignore
811
+ }
812
+ }
782
813
  const extensions = [".png", ".jpg", ".jpeg", ".svg", ".webp", ".gif"];
783
814
  const fileNames = ["avatar", "icon", "image", "logo"];
784
815
  const searchDirs = [
785
816
  (name === defaultName || name === "default")
786
817
  ? path.join(resolvedBaseDir, "assets")
787
- : path.join(resolvedBaseDir, "agents", name, "assets"),
818
+ : (agentFolder ? path.join(agentFolder, "assets") : path.join(resolvedBaseDir, "agents", name, "assets")),
788
819
  path.join(process.cwd(), "server", "src", "agents", name, "assets"),
789
820
  path.join(process.cwd(), "server", "src", "assets", "agents", name),
790
821
  path.join(process.cwd(), "server", "src", "agents", "assets"),
@@ -793,7 +824,8 @@ export async function startServer(options = {}) {
793
824
  for (const dir of searchDirs) {
794
825
  for (const fileName of fileNames) {
795
826
  for (const ext of extensions) {
796
- const baseName = (dir.endsWith("assets") && !dir.includes(name)) ? name : fileName;
827
+ const isAgentSpecificDir = dir.includes(name) || (agentFolder && dir.includes(agentFolder));
828
+ const baseName = (dir.endsWith("assets") && !isAgentSpecificDir) ? name : fileName;
797
829
  const p = path.join(dir, `${baseName}${ext}`);
798
830
  try {
799
831
  await fs.access(p);
@@ -868,6 +900,7 @@ export async function startServer(options = {}) {
868
900
  console.log(`OpenBot server listening at http://localhost:${PORT}`);
869
901
  console.log(` - Chat endpoint: POST /api/chat`);
870
902
  console.log(` - REST endpoints: /api/config, /api/sessions, /api/agents`);
903
+ console.log(`\n🚀 TIP: Use 'openbot up' to run both the server and web dashboard together.`);
871
904
  if (options.openaiApiKey)
872
905
  console.log(" - Using OpenAI API Key from CLI");
873
906
  if (options.anthropicApiKey)
package/package.json CHANGED
@@ -1,12 +1,10 @@
1
1
  {
2
2
  "name": "openbot",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
4
4
  "private": false,
5
5
  "type": "module",
6
- "scripts": {
7
- "dev": "tsx watch src/cli.ts server",
8
- "build": "tsc",
9
- "start": "node dist/cli.js server"
6
+ "engines": {
7
+ "node": ">=20.12.0"
10
8
  },
11
9
  "bin": {
12
10
  "openbot": "./dist/cli.js"
@@ -33,5 +31,10 @@
33
31
  "@types/node": "^20.10.1",
34
32
  "tsx": "^4.21.0",
35
33
  "typescript": "^5.9.3"
34
+ },
35
+ "scripts": {
36
+ "dev": "tsx watch src/cli.ts server",
37
+ "build": "tsc",
38
+ "start": "node dist/cli.js server"
36
39
  }
37
- }
40
+ }