openbot 0.1.25 → 0.1.27

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
@@ -7,12 +7,167 @@ import { execSync } from "node:child_process";
7
7
  import { tmpdir } from "node:os";
8
8
  import { saveConfig, resolvePath, DEFAULT_BASE_DIR } from "./config.js";
9
9
  import { startServer } from "./server.js";
10
- import { ensurePluginReady } from "./registry/plugin-loader.js";
10
+ import { ensurePluginReady, getPluginMetadata } from "./registry/plugin-loader.js";
11
+ import { readAgentConfig } from "./registry/yaml-agent-loader.js";
11
12
  const program = new Command();
12
13
  program
13
14
  .name("openbot")
14
15
  .description("OpenBot CLI - Secure and easy configuration")
15
- .version("0.1.23");
16
+ .version("0.1.26");
17
+ /**
18
+ * Check if a GitHub repository exists.
19
+ */
20
+ function checkGitHubRepo(repo) {
21
+ try {
22
+ execSync(`git ls-remote https://github.com/${repo}.git`, { stdio: "ignore" });
23
+ return true;
24
+ }
25
+ catch {
26
+ return false;
27
+ }
28
+ }
29
+ /**
30
+ * Check if an NPM package exists.
31
+ */
32
+ function checkNpmPackage(pkg) {
33
+ try {
34
+ execSync(`npm show ${pkg} version`, { stdio: "ignore" });
35
+ return true;
36
+ }
37
+ catch {
38
+ return false;
39
+ }
40
+ }
41
+ /**
42
+ * Install a plugin from a source (GitHub, local, or NPM).
43
+ */
44
+ async function installPlugin(source, quiet = false) {
45
+ const isGitHub = (source.includes("/") || source.startsWith("github:")) && !source.startsWith("/") && !source.startsWith(".");
46
+ const isNpm = source.startsWith("@") || source.startsWith("npm:");
47
+ const repoUrl = isGitHub
48
+ ? (source.startsWith("github:") ? `https://github.com/${source.slice(7)}.git` : `https://github.com/${source}.git`)
49
+ : source;
50
+ const tempDir = path.join(tmpdir(), `openbot-plugin-install-${Date.now()}`);
51
+ try {
52
+ if (!quiet)
53
+ console.log(`šŸ“¦ Installing plugin from: ${isNpm ? source : repoUrl}`);
54
+ // 1. Fetch to temp directory
55
+ if (isGitHub) {
56
+ execSync(`git clone --depth 1 ${repoUrl} ${tempDir}`, { stdio: quiet ? "ignore" : "inherit" });
57
+ }
58
+ else if (isNpm) {
59
+ const pkgName = source.startsWith("npm:") ? source.slice(4) : source;
60
+ await fs.mkdir(tempDir, { recursive: true });
61
+ execSync(`npm install ${pkgName} --prefix ${tempDir}`, { stdio: quiet ? "ignore" : "inherit" });
62
+ // Move from node_modules to tempDir root for consistency
63
+ const pkgFolder = path.join(tempDir, "node_modules", pkgName);
64
+ const moveTemp = path.join(tmpdir(), `openbot-npm-move-${Date.now()}`);
65
+ await fs.rename(pkgFolder, moveTemp);
66
+ await fs.rm(tempDir, { recursive: true, force: true });
67
+ await fs.rename(moveTemp, tempDir);
68
+ }
69
+ else {
70
+ const absoluteSource = path.resolve(source);
71
+ await fs.mkdir(tempDir, { recursive: true });
72
+ execSync(`cp -R ${absoluteSource}/. ${tempDir}`, { stdio: quiet ? "ignore" : "inherit" });
73
+ }
74
+ // 2. Identify name from metadata
75
+ const { name } = await getPluginMetadata(tempDir);
76
+ const baseDir = resolvePath(DEFAULT_BASE_DIR);
77
+ const targetDir = path.join(baseDir, "plugins", name);
78
+ // 3. Move to target directory
79
+ await fs.mkdir(path.dirname(targetDir), { recursive: true });
80
+ if (await fs.access(targetDir).then(() => true).catch(() => false)) {
81
+ if (!quiet)
82
+ console.log(`āš ļø Plugin "${name}" already exists. Overwriting...`);
83
+ await fs.rm(targetDir, { recursive: true, force: true });
84
+ }
85
+ await fs.rename(tempDir, targetDir);
86
+ if (!quiet)
87
+ console.log(`āœ… Moved to: ${targetDir}`);
88
+ // 4. Prepare
89
+ if (!quiet)
90
+ console.log(`āš™ļø Preparing plugin "${name}"...`);
91
+ await ensurePluginReady(targetDir);
92
+ if (!quiet)
93
+ console.log(`\nšŸŽ‰ Successfully installed plugin: ${name}`);
94
+ return name;
95
+ }
96
+ catch (err) {
97
+ if (!quiet)
98
+ console.error("\nāŒ Plugin installation failed:", err instanceof Error ? err.message : String(err));
99
+ try {
100
+ await fs.rm(tempDir, { recursive: true, force: true });
101
+ }
102
+ catch { /* ignore */ }
103
+ if (!quiet)
104
+ process.exit(1);
105
+ throw err;
106
+ }
107
+ }
108
+ /**
109
+ * Install an agent from a source (GitHub).
110
+ */
111
+ async function installAgent(source) {
112
+ const repoUrl = source.startsWith("github:") ? `https://github.com/${source.slice(7)}.git` : `https://github.com/${source}.git`;
113
+ const tempDir = path.join(tmpdir(), `openbot-agent-install-${Date.now()}`);
114
+ try {
115
+ console.log(`šŸ¤– Installing agent from: ${repoUrl}`);
116
+ // 1. Clone to temp directory
117
+ execSync(`git clone --depth 1 ${repoUrl} ${tempDir}`, { stdio: "inherit" });
118
+ // 2. Identify name from agent.yaml
119
+ const config = await readAgentConfig(tempDir);
120
+ const name = config.name || path.basename(source.replace(".git", ""));
121
+ const baseDir = resolvePath(DEFAULT_BASE_DIR);
122
+ const targetDir = path.join(baseDir, "agents", name);
123
+ // 3. Move to target directory
124
+ await fs.mkdir(path.dirname(targetDir), { recursive: true });
125
+ if (await fs.access(targetDir).then(() => true).catch(() => false)) {
126
+ console.log(`āš ļø Agent "${name}" already exists. Overwriting...`);
127
+ await fs.rm(targetDir, { recursive: true, force: true });
128
+ }
129
+ await fs.rename(tempDir, targetDir);
130
+ console.log(`āœ… Moved to: ${targetDir}`);
131
+ // 4. Recursive plugin installation
132
+ if (config.plugins && Array.isArray(config.plugins)) {
133
+ for (const pluginItem of config.plugins) {
134
+ const pluginName = typeof pluginItem === "string" ? pluginItem : pluginItem.name;
135
+ // Skip built-in plugins
136
+ if (["shell", "file-system"].includes(pluginName))
137
+ continue;
138
+ // Check if plugin exists locally
139
+ const pluginPath = path.join(baseDir, "plugins", pluginName);
140
+ const existsLocally = await fs.access(pluginPath).then(() => true).catch(() => false);
141
+ if (!existsLocally) {
142
+ console.log(`šŸ” Agent needs plugin "${pluginName}". Searching...`);
143
+ // Try GitHub
144
+ const ghRepo = `meetopenbot/plugin-${pluginName}`;
145
+ if (checkGitHubRepo(ghRepo)) {
146
+ await installPlugin(ghRepo);
147
+ continue;
148
+ }
149
+ // Try NPM
150
+ const npmPkg = `@melony/plugin-${pluginName}`;
151
+ if (checkNpmPackage(npmPkg)) {
152
+ await installPlugin(npmPkg);
153
+ continue;
154
+ }
155
+ console.warn(`āš ļø Could not find plugin "${pluginName}" for agent "${name}". You may need to install it manually.`);
156
+ }
157
+ }
158
+ }
159
+ console.log(`\nšŸŽ‰ Successfully installed agent: ${name}`);
160
+ return name;
161
+ }
162
+ catch (err) {
163
+ console.error("\nāŒ Agent installation failed:", err instanceof Error ? err.message : String(err));
164
+ try {
165
+ await fs.rm(tempDir, { recursive: true, force: true });
166
+ }
167
+ catch { /* ignore */ }
168
+ process.exit(1);
169
+ }
170
+ }
16
171
  program
17
172
  .command("configure")
18
173
  .description("Configure OpenBot model and settings")
@@ -72,62 +227,45 @@ program
72
227
  .action(async (options) => {
73
228
  await startServer(options);
74
229
  });
230
+ program
231
+ .command("add <name>")
232
+ .description("Add an agent or plugin by name (auto-resolves to GitHub/NPM)")
233
+ .action(async (name) => {
234
+ // 1. Try as Agent
235
+ const agentRepo = `meetopenbot/agent-${name}`;
236
+ if (checkGitHubRepo(agentRepo)) {
237
+ await installAgent(agentRepo);
238
+ return;
239
+ }
240
+ // 2. Try as Plugin
241
+ const baseDir = resolvePath(DEFAULT_BASE_DIR);
242
+ const pluginPath = path.join(baseDir, "plugins", name);
243
+ const existsLocally = await fs.access(pluginPath).then(() => true).catch(() => false);
244
+ if (existsLocally) {
245
+ console.log(`āœ… Plugin "${name}" is already installed locally.`);
246
+ return;
247
+ }
248
+ // Check GitHub Plugin
249
+ const pluginGhRepo = `meetopenbot/plugin-${name}`;
250
+ if (checkGitHubRepo(pluginGhRepo)) {
251
+ await installPlugin(pluginGhRepo);
252
+ return;
253
+ }
254
+ // Check NPM Plugin
255
+ const pluginNpmPkg = `@melony/plugin-${name}`;
256
+ if (checkNpmPackage(pluginNpmPkg)) {
257
+ await installPlugin(pluginNpmPkg);
258
+ return;
259
+ }
260
+ console.error(`āŒ Could not find agent or plugin named "${name}" in official repositories.`);
261
+ process.exit(1);
262
+ });
75
263
  const plugin = program.command("plugin").description("Manage OpenBot plugins");
76
264
  plugin
77
265
  .command("install <source>")
78
266
  .description("Install a shared plugin from GitHub (user/repo) or a local path")
79
267
  .action(async (source) => {
80
- const isGitHub = source.includes("/") && !source.startsWith("/") && !source.startsWith(".");
81
- const repoUrl = isGitHub ? `https://github.com/${source}.git` : source;
82
- const tempDir = path.join(tmpdir(), `openbot-plugin-install-${Date.now()}`);
83
- try {
84
- console.log(`šŸ“¦ Installing plugin from: ${repoUrl}`);
85
- // 1. Clone or copy to temp directory
86
- if (isGitHub) {
87
- execSync(`git clone --depth 1 ${repoUrl} ${tempDir}`, { stdio: "inherit" });
88
- }
89
- else {
90
- const absoluteSource = path.resolve(source);
91
- await fs.mkdir(tempDir, { recursive: true });
92
- execSync(`cp -R ${absoluteSource}/. ${tempDir}`, { stdio: "inherit" });
93
- }
94
- // 2. Identify name from package.json
95
- let name = path.basename(source.replace(".git", ""));
96
- const pkgPath = path.join(tempDir, "package.json");
97
- if (await fs.access(pkgPath).then(() => true).catch(() => false)) {
98
- try {
99
- const pkg = JSON.parse(await fs.readFile(pkgPath, "utf-8"));
100
- if (pkg.name)
101
- name = pkg.name.split("/").pop(); // Use last part of scoped names
102
- }
103
- catch {
104
- // Fallback to source basename
105
- }
106
- }
107
- const baseDir = resolvePath(DEFAULT_BASE_DIR);
108
- const targetDir = path.join(baseDir, "plugins", name);
109
- // 3. Move to target directory
110
- await fs.mkdir(path.dirname(targetDir), { recursive: true });
111
- if (await fs.access(targetDir).then(() => true).catch(() => false)) {
112
- console.log(`āš ļø Plugin "${name}" already exists. Overwriting...`);
113
- await fs.rm(targetDir, { recursive: true, force: true });
114
- }
115
- await fs.rename(tempDir, targetDir);
116
- console.log(`āœ… Moved to: ${targetDir}`);
117
- // 4. Prepare
118
- console.log(`āš™ļø Preparing plugin "${name}"...`);
119
- await ensurePluginReady(targetDir);
120
- console.log(`\nšŸŽ‰ Successfully installed plugin: ${name}`);
121
- console.log(`This plugin is now available to all agents.`);
122
- }
123
- catch (err) {
124
- console.error("\nāŒ Plugin installation failed:", err instanceof Error ? err.message : String(err));
125
- try {
126
- await fs.rm(tempDir, { recursive: true, force: true });
127
- }
128
- catch { /* ignore */ }
129
- process.exit(1);
130
- }
268
+ await installPlugin(source);
131
269
  });
132
270
  plugin
133
271
  .command("list")
@@ -149,18 +287,9 @@ plugin
149
287
  continue;
150
288
  if (entry.name.startsWith(".") || entry.name.startsWith("_"))
151
289
  continue;
152
- const pkgPath = path.join(pluginsDir, entry.name, "package.json");
153
- let description = "No description";
154
- let version = "0.0.0";
155
- try {
156
- const pkg = JSON.parse(await fs.readFile(pkgPath, "utf-8"));
157
- description = pkg.description || description;
158
- version = pkg.version || version;
159
- }
160
- catch {
161
- // Use defaults
162
- }
163
- plugins.push({ name: entry.name, version, description });
290
+ const pluginDir = path.join(pluginsDir, entry.name);
291
+ const { name, version, description } = await getPluginMetadata(pluginDir);
292
+ plugins.push({ name, version, description });
164
293
  }
165
294
  if (plugins.length === 0) {
166
295
  console.log("No shared plugins found.");
@@ -1,5 +1,7 @@
1
1
  import { saveConfig } from "../config.js";
2
2
  import { tabOnlyUI } from "../ui/layout.js";
3
+ import { exec } from "node:child_process";
4
+ import os from "node:os";
3
5
  /**
4
6
  * Handle settings updates (like API keys)
5
7
  */
@@ -27,3 +29,19 @@ export async function* updateSettingsHandler(event, { state }) {
27
29
  };
28
30
  }
29
31
  }
32
+ /**
33
+ * Handle opening an agent's folder in the native file explorer
34
+ */
35
+ export async function* openAgentFolderHandler(event) {
36
+ const { folder } = event.data;
37
+ if (folder) {
38
+ const command = os.platform() === "win32" ? `explorer "${folder}"` :
39
+ os.platform() === "darwin" ? `open "${folder}"` :
40
+ `xdg-open "${folder}"`;
41
+ exec(command, (error) => {
42
+ if (error) {
43
+ console.error(`Failed to open folder: ${error.message}`);
44
+ }
45
+ });
46
+ }
47
+ }
package/dist/open-bot.js CHANGED
@@ -6,7 +6,7 @@ import { llmPlugin } from "./plugins/llm/index.js";
6
6
  import { initHandler } from "./handlers/init.js";
7
7
  import { sessionChangeHandler } from "./handlers/session-change.js";
8
8
  import { tabChangeHandler } from "./handlers/tab-change.js";
9
- import { updateSettingsHandler } from "./handlers/settings.js";
9
+ import { updateSettingsHandler, openAgentFolderHandler } from "./handlers/settings.js";
10
10
  import { loadConfig, resolvePath, DEFAULT_BASE_DIR } from "./config.js";
11
11
  import { createModel } from "./models.js";
12
12
  import path from "node:path";
@@ -30,7 +30,6 @@ export async function createOpenBot(options) {
30
30
  const baseDir = config.baseDir || DEFAULT_BASE_DIR;
31
31
  const resolvedBaseDir = resolvePath(baseDir);
32
32
  const model = createModel(options);
33
- const userDataDir = path.join(resolvedBaseDir, "browser-data");
34
33
  // ─── Plugin Registry ─────────────────────────────────────────────
35
34
  // Register built-in plugins so YAML agents can reference them by name.
36
35
  const pluginRegistry = new PluginRegistry();
@@ -195,6 +194,7 @@ Example: If the user asks to "check the weather", and you see a 'browser' agent,
195
194
  .on("init", initHandler)
196
195
  .on("sessionChange", sessionChangeHandler)
197
196
  .on("tabChange", tabChangeHandler)
198
- .on("action:updateSettings", updateSettingsHandler);
197
+ .on("action:updateSettings", updateSettingsHandler)
198
+ .on("action:openAgentFolder", openAgentFolderHandler);
199
199
  return app;
200
200
  }
@@ -1,4 +1,4 @@
1
1
  export { PluginRegistry } from "./plugin-registry.js";
2
2
  export { AgentRegistry } from "./agent-registry.js";
3
- export { discoverYamlAgents } from "./yaml-agent-loader.js";
3
+ export { discoverYamlAgents, listYamlAgents } from "./yaml-agent-loader.js";
4
4
  export { loadPluginsFromDir } from "./plugin-loader.js";
@@ -2,6 +2,28 @@ import * as fs from "node:fs/promises";
2
2
  import * as path from "node:path";
3
3
  import { pathToFileURL } from "node:url";
4
4
  import { execSync } from "node:child_process";
5
+ /**
6
+ * Get metadata (name, description, version) from a plugin directory.
7
+ */
8
+ export async function getPluginMetadata(pluginDir) {
9
+ const pkgPath = path.join(pluginDir, "package.json");
10
+ const hasPackageJson = await fs.access(pkgPath).then(() => true).catch(() => false);
11
+ let name = path.basename(pluginDir);
12
+ let description = "No description";
13
+ let version = "0.0.0";
14
+ if (hasPackageJson) {
15
+ try {
16
+ const pkg = JSON.parse(await fs.readFile(pkgPath, "utf-8"));
17
+ name = (pkg.name?.split("/").pop()) || name;
18
+ description = pkg.description || description;
19
+ version = pkg.version || version;
20
+ }
21
+ catch {
22
+ // Fallback to defaults
23
+ }
24
+ }
25
+ return { name, description, version };
26
+ }
5
27
  /**
6
28
  * Ensure a plugin is ready to run by installing dependencies and building if necessary.
7
29
  */
@@ -25,6 +25,50 @@ function resolveConfigPaths(config) {
25
25
  }
26
26
  return config;
27
27
  }
28
+ /**
29
+ * Read and parse an agent.yaml file from a directory.
30
+ */
31
+ export async function readAgentConfig(agentDir) {
32
+ const yamlPath = path.join(agentDir, "agent.yaml");
33
+ const content = await fs.readFile(yamlPath, "utf-8");
34
+ return yaml.load(content);
35
+ }
36
+ /**
37
+ * Discover YAML-defined agents from a directory without loading plugins.
38
+ *
39
+ * @param agentsDir Absolute path to the agents directory (e.g. ~/.openbot/agents)
40
+ * @returns Array of agent metadata
41
+ */
42
+ export async function listYamlAgents(agentsDir) {
43
+ const agents = [];
44
+ try {
45
+ const entries = await fs.readdir(agentsDir, { withFileTypes: true });
46
+ for (const entry of entries) {
47
+ if (!entry.isDirectory())
48
+ continue;
49
+ if (entry.name.startsWith(".") || entry.name.startsWith("_"))
50
+ continue;
51
+ const agentDir = path.join(agentsDir, entry.name);
52
+ try {
53
+ const config = await readAgentConfig(agentDir);
54
+ if (config.name && config.description) {
55
+ agents.push({
56
+ name: config.name,
57
+ description: config.description,
58
+ folder: agentDir,
59
+ });
60
+ }
61
+ }
62
+ catch {
63
+ // Skip invalid agents
64
+ }
65
+ }
66
+ }
67
+ catch {
68
+ // Agents directory doesn't exist
69
+ }
70
+ return agents;
71
+ }
28
72
  /**
29
73
  * Discover and load YAML-defined agents from a directory.
30
74
  *
@@ -56,8 +100,7 @@ export async function discoverYamlAgents(agentsDir, pluginRegistry, defaultModel
56
100
  const yamlPath = path.join(agentsDir, entry.name, "agent.yaml");
57
101
  const agentDir = path.join(agentsDir, entry.name);
58
102
  try {
59
- const content = await fs.readFile(yamlPath, "utf-8");
60
- const config = yaml.load(content);
103
+ const config = await readAgentConfig(agentDir);
61
104
  // Validate required fields
62
105
  if (!config.name || !config.description || !config.plugins?.length || !config.systemPrompt) {
63
106
  console.warn(`[agents] "${entry.name}/agent.yaml": missing required fields (name, description, plugins, systemPrompt) — skipping`);
@@ -1,75 +1,106 @@
1
1
  import { ui } from "@melony/ui-kit";
2
- import { loadConfig } from "../config.js";
2
+ import { loadConfig, resolvePath, DEFAULT_BASE_DIR } from "../config.js";
3
+ import { listYamlAgents } from "../registry/index.js";
4
+ import path from "node:path";
3
5
  export const settingsUI = async () => {
4
6
  const config = loadConfig();
7
+ const baseDir = config.baseDir || DEFAULT_BASE_DIR;
8
+ const resolvedBaseDir = resolvePath(baseDir);
9
+ const agentsDir = path.join(resolvedBaseDir, "agents");
5
10
  const hasOpenAIKey = !!config.openaiApiKey;
6
11
  const hasAnthropicKey = !!config.anthropicApiKey;
7
- return ui.form({
8
- onSubmitAction: {
9
- type: "action:updateSettings",
10
- data: {}
11
- }
12
+ const agents = await listYamlAgents(agentsDir);
13
+ return ui.box({
14
+ width: "full",
15
+ height: "full",
16
+ overflow: "auto",
12
17
  }, [
13
- ui.box({
14
- width: "full",
15
- height: "full",
16
- padding: "xl",
17
- background: "background",
18
- }, [
19
- ui.col({ gap: "xl", width: "full" }, [
20
- ui.col({ gap: "xs" }, [
21
- ui.heading("Settings", 2),
22
- ui.text("Manage your OpenBot configuration", { color: "mutedForeground" }),
23
- ]),
24
- ui.divider(),
25
- ui.col({ gap: "md" }, [
26
- ui.heading("Model Configuration", 4),
27
- ui.col({ gap: "sm" }, [
28
- ui.input("model", undefined, {
29
- placeholder: "e.g. gpt-4o-mini",
30
- width: "full",
31
- defaultValue: config.model || "gpt-4o-mini"
32
- }),
33
- ]),
34
- ]),
35
- ui.col({ gap: "md" }, [
36
- ui.heading("API Keys", 4),
37
- ui.col({ gap: "lg" }, [
38
- // OpenAI Key
18
+ ui.col({ padding: "xl", gap: "xl", width: "full", flex: 1 }, [
19
+ ui.col({ gap: "xs" }, [
20
+ ui.heading("Settings", 2),
21
+ ui.text("Manage your OpenBot configuration", { color: "mutedForeground" }),
22
+ ]),
23
+ ui.divider(),
24
+ ui.form({
25
+ onSubmitAction: {
26
+ type: "action:updateSettings",
27
+ data: {}
28
+ }
29
+ }, [
30
+ ui.col({ gap: "xl" }, [
31
+ ui.col({ gap: "md" }, [
32
+ ui.heading("Model Configuration", 4),
39
33
  ui.col({ gap: "sm" }, [
40
- ui.node("label", { value: "OpenAI API Key" }),
41
- ui.input("openai_api_key", undefined, {
42
- placeholder: "sk-...",
43
- inputType: "password",
44
- defaultValue: hasOpenAIKey ? "••••••••••••••••" : "",
45
- width: "full"
34
+ ui.input("model", undefined, {
35
+ placeholder: "e.g. gpt-4o-mini",
36
+ width: "full",
37
+ defaultValue: config.model || "gpt-4o-mini"
46
38
  }),
47
39
  ]),
48
- // Anthropic Key
49
- ui.col({ gap: "sm" }, [
50
- ui.node("label", { value: "Anthropic API Key" }),
51
- ui.input("anthropic_api_key", undefined, {
52
- placeholder: "sk-ant-...",
53
- inputType: "password",
54
- defaultValue: hasAnthropicKey ? "••••••••••••••••" : "",
55
- width: "full"
56
- }),
40
+ ]),
41
+ ui.col({ gap: "md" }, [
42
+ ui.heading("API Keys", 4),
43
+ ui.col({ gap: "lg" }, [
44
+ // OpenAI Key
45
+ ui.col({ gap: "sm" }, [
46
+ ui.node("label", { value: "OpenAI API Key" }),
47
+ ui.input("openai_api_key", undefined, {
48
+ placeholder: "sk-...",
49
+ inputType: "password",
50
+ defaultValue: hasOpenAIKey ? "••••••••••••••••" : "",
51
+ width: "full"
52
+ }),
53
+ ]),
54
+ // Anthropic Key
55
+ ui.col({ gap: "sm" }, [
56
+ ui.node("label", { value: "Anthropic API Key" }),
57
+ ui.input("anthropic_api_key", undefined, {
58
+ placeholder: "sk-ant-...",
59
+ inputType: "password",
60
+ defaultValue: hasAnthropicKey ? "••••••••••••••••" : "",
61
+ width: "full"
62
+ }),
63
+ ])
57
64
  ])
65
+ ]),
66
+ ui.col({ gap: "md" }, [
67
+ ui.heading("Theme", 4),
68
+ ui.themeToggle(),
69
+ ]),
70
+ ui.row({ justify: "end" }, [
71
+ ui.button({
72
+ label: "Save Settings",
73
+ type: "submit",
74
+ variant: "primary"
75
+ })
58
76
  ])
59
- ]),
60
- ui.col({ gap: "md" }, [
61
- ui.heading("Theme", 4),
62
- ui.themeToggle(),
63
- ]),
64
- ui.divider(),
65
- ui.row({ justify: "end" }, [
66
- ui.button({
67
- label: "Save Settings",
68
- type: "submit",
69
- variant: "primary"
70
- })
71
77
  ])
78
+ ]),
79
+ ui.divider(),
80
+ ui.col({ gap: "md" }, [
81
+ ui.heading("Installed Agents", 4),
82
+ ui.text("Agents installed in ~/.openbot/agents", { color: "mutedForeground", size: "sm" }),
83
+ ui.col({ gap: "sm" }, agents.map(agent => (ui.box({
84
+ padding: "md",
85
+ background: "muted",
86
+ }, [
87
+ ui.row({ justify: "between", align: "center" }, [
88
+ ui.col({ gap: "xs" }, [
89
+ ui.text(agent.name, { weight: "bold" }),
90
+ ui.text(agent.description, { size: "sm", color: "mutedForeground" }),
91
+ ]),
92
+ ui.button({
93
+ label: "Open Folder",
94
+ variant: "secondary",
95
+ size: "sm",
96
+ onClickAction: {
97
+ type: "action:openAgentFolder",
98
+ data: { folder: agent.folder }
99
+ }
100
+ })
101
+ ])
102
+ ]))))
72
103
  ])
73
- ])
104
+ ]),
74
105
  ]);
75
106
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openbot",
3
- "version": "0.1.25",
3
+ "version": "0.1.27",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {