openbot 0.1.27 → 0.1.29

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.
@@ -1,6 +1,7 @@
1
1
  import { llmPlugin } from "../plugins/llm/index.js";
2
2
  import { shellPlugin, shellToolDefinitions } from "../plugins/shell/index.js";
3
3
  import { fileSystemPlugin, fileSystemToolDefinitions } from "../plugins/file-system/index.js";
4
+ import approvalPlugin from "../plugins/approval/index.js";
4
5
  const DEFAULT_SYSTEM_PROMPT = `You are an OS Agent with access to the shell and file system.
5
6
  Your job is to help the user with file operations and command execution.
6
7
  You can read, write, list, and delete files, as well as execute shell commands.
@@ -11,12 +12,19 @@ export const osAgent = (options) => (builder) => {
11
12
  builder
12
13
  .use(shellPlugin({ cwd }))
13
14
  .use(fileSystemPlugin({ baseDir: "/" }))
15
+ .use(approvalPlugin({
16
+ rules: [
17
+ { action: "action:executeCommand", message: "The agent wants to execute a terminal command. Please review carefully." },
18
+ { action: "action:writeFile", message: "The agent wants to write to a file." },
19
+ { action: "action:deleteFile", message: "The agent wants to delete a file." },
20
+ ],
21
+ }))
14
22
  .use(llmPlugin({
15
23
  model,
16
24
  system: systemPrompt,
17
25
  toolDefinitions: {
18
26
  ...shellToolDefinitions,
19
- ...fileSystemToolDefinitions,
27
+ ...fileSystemToolDefinitions
20
28
  },
21
29
  promptInputType: "agent:os:input",
22
30
  actionResultInputType: "agent:os:result",
package/dist/cli.js CHANGED
@@ -13,7 +13,7 @@ const program = new Command();
13
13
  program
14
14
  .name("openbot")
15
15
  .description("OpenBot CLI - Secure and easy configuration")
16
- .version("0.1.26");
16
+ .version("0.1.29");
17
17
  /**
18
18
  * Check if a GitHub repository exists.
19
19
  */
package/dist/open-bot.js CHANGED
@@ -14,8 +14,9 @@ import { z } from "zod";
14
14
  // Plugin imports for the registry
15
15
  import { shellPlugin, shellToolDefinitions } from "./plugins/shell/index.js";
16
16
  import { fileSystemPlugin, fileSystemToolDefinitions } from "./plugins/file-system/index.js";
17
+ import { approvalPlugin } from "./plugins/approval/index.js";
17
18
  // Registry
18
- import { PluginRegistry, AgentRegistry, discoverYamlAgents, loadPluginsFromDir } from "./registry/index.js";
19
+ import { PluginRegistry, AgentRegistry, discoverYamlAgents, discoverTsAgents, loadPluginsFromDir } from "./registry/index.js";
19
20
  /**
20
21
  * Create the OpenBot manager agent.
21
22
  *
@@ -45,6 +46,12 @@ export async function createOpenBot(options) {
45
46
  toolDefinitions: fileSystemToolDefinitions,
46
47
  factory: () => fileSystemPlugin({ baseDir: "/" }),
47
48
  });
49
+ pluginRegistry.register({
50
+ name: "approval",
51
+ description: "Require user approval for specific actions",
52
+ toolDefinitions: {},
53
+ factory: (options) => approvalPlugin(options),
54
+ });
48
55
  // ─── Shared Plugins ──────────────────────────────────────────────
49
56
  // Load community/user plugins from ~/.openbot/plugins/
50
57
  const sharedPlugins = await loadPluginsFromDir(path.join(resolvedBaseDir, "plugins"));
@@ -74,8 +81,12 @@ export async function createOpenBot(options) {
74
81
  subscribe: ["manager:completion"],
75
82
  });
76
83
  // Discover community / user agents from ~/.openbot/agents/
77
- const yamlAgents = await discoverYamlAgents(path.join(resolvedBaseDir, "agents"), pluginRegistry, model, options);
78
- for (const agent of yamlAgents) {
84
+ const agentsDir = path.join(resolvedBaseDir, "agents");
85
+ const [yamlAgents, tsAgents] = await Promise.all([
86
+ discoverYamlAgents(agentsDir, pluginRegistry, model, options),
87
+ discoverTsAgents(agentsDir, model, options),
88
+ ]);
89
+ for (const agent of [...yamlAgents, ...tsAgents]) {
79
90
  agentRegistry.register(agent);
80
91
  }
81
92
  // ─── Compose the Melony App ──────────────────────────────────────
@@ -124,6 +135,61 @@ export async function createOpenBot(options) {
124
135
  return `- **${a.name}**: ${a.description}${tools ? `\n Capabilities:\n${tools}` : ""}`;
125
136
  })
126
137
  .join("\n\n");
138
+ // 2.5 Command Prefix Router
139
+ // Allows bypassing the manager using "/agent task" (e.g. "/os list files")
140
+ app.on("user:text", async function* (event, { state }) {
141
+ const content = event.data.content.trim();
142
+ if (content.startsWith("/")) {
143
+ const firstSpace = content.indexOf(" ");
144
+ const prefix = firstSpace === -1 ? content.slice(1) : content.slice(1, firstSpace);
145
+ const task = firstSpace === -1 ? "" : content.slice(firstSpace + 1).trim();
146
+ if (agentNames.includes(prefix)) {
147
+ // Direct route to specialized agent
148
+ state.lastDirectAgent = prefix;
149
+ yield {
150
+ type: `agent:${prefix}:input`,
151
+ data: { content: task },
152
+ };
153
+ return;
154
+ }
155
+ }
156
+ // Default: send to manager for reasoning/delegation
157
+ state.lastDirectAgent = undefined;
158
+ yield { type: "manager:input", data: event.data };
159
+ });
160
+ // 2.6 Global Tool Result Dispatcher
161
+ // Routes tool results to the appropriate agent's internal loop.
162
+ // taskResult means that the loop is complete.
163
+ app.on("action:taskResult", async function* (event, { state, suspend }) {
164
+ const s = state;
165
+ // 1. Direct route (prefix command)
166
+ if (s.lastDirectAgent) {
167
+ // suspend not to go to infinite loop
168
+ // suspend({
169
+ // type: `agent:${s.lastDirectAgent}:result`,
170
+ // data: event.data,
171
+ // } as ChatEvent);
172
+ yield {
173
+ type: `agent:${s.lastDirectAgent}:result`,
174
+ data: event.data,
175
+ };
176
+ }
177
+ // 2. Delegation route (manager-led)
178
+ // Find which agent has a pending task
179
+ const pendingAgentName = Object.keys(s.pendingAgentTasks || {}).find((name) => s.pendingAgentTasks[name]);
180
+ if (pendingAgentName) {
181
+ yield {
182
+ type: `agent:${pendingAgentName}:result`,
183
+ data: event.data,
184
+ };
185
+ return;
186
+ }
187
+ // 3. Manager route (brain tools, etc.)
188
+ yield {
189
+ type: "manager:result",
190
+ data: event.data,
191
+ };
192
+ });
127
193
  app.use(llmPlugin({
128
194
  model: model,
129
195
  system: async (context) => {
@@ -132,21 +198,23 @@ export async function createOpenBot(options) {
132
198
  ]);
133
199
  return `${brainPrompt}
134
200
 
135
- ## Delegation & Specialized Agents
136
- You are the **Manager Agent**. Your primary role is to orchestrate tasks by delegating them to specialized agents when appropriate.
137
- If a task falls outside your core capabilities (memory and orchestration), you **MUST** use the \`delegateTask\` tool.
201
+ ## Core Role: Manager Agent
202
+ You are the **Manager Agent**. You have exactly two jobs:
203
+ 1. **Task Delegation**: Orchestrate tasks by delegating them to specialized agents.
204
+ 2. **Memory & Identity**: Manage your long-term memory and identity using your core brain tools (\`remember\`, \`recall\`, \`updateIdentity\`, etc.).
205
+
206
+ ### Delegation & Reporting Guidelines:
207
+ - **Delegate by Default**: If a task requires capabilities outside of memory/identity management (like shell access, file operations, web browsing), you **MUST** delegate to the appropriate agent.
208
+ - **Be Concise**: When a sub-agent completes a task, provide a **nice, concise summary**. Sub-agents provide detailed outputs in the background; your response should be a brief, high-level answer to the user. Do not repeat details unless necessary.
209
+ - **Detailed Delegation**: When calling \`delegateTask\`, provide a thorough description of the task so the sub-agent has full context.
138
210
 
139
211
  ### Available Agents:
140
212
  ${agentDescriptions}
141
213
 
142
- ### Delegation Guidelines:
143
- 1. **Choose the Best Expert**: Analyze the user's request and select the agent whose description most closely matches the required expertise.
144
- 2. **Task Description**: When delegating, provide a clear and detailed task description. Include any context the agent might need to succeed.
145
- 3. **No "I Can't"**: If an agent is available that can handle a request, do not tell the user you cannot do it. Simply delegate.
146
- 4. **Summary**: Once an agent returns a result, summarize the findings or actions for the user.
147
-
148
- Example: If the user asks to "check the weather", and you see a 'browser' agent, delegate the task to it.`;
214
+ Remember: You are the orchestrator. Let the specialized agents do the work, and you provide the concise final answer.`;
149
215
  },
216
+ promptInputType: "manager:input",
217
+ actionResultInputType: "manager:result",
150
218
  completionEventType: "manager:completion",
151
219
  toolDefinitions: {
152
220
  ...brainToolDefinitions,
@@ -177,9 +245,10 @@ Example: If the user asks to "check the weather", and you see a 'browser' agent,
177
245
  const s = state;
178
246
  const pending = s.pendingAgentTasks?.[agent.name];
179
247
  if (pending) {
248
+ // This was a delegated task — bridge back to the manager's tool call
180
249
  delete s.pendingAgentTasks[agent.name];
181
250
  yield {
182
- type: "action:taskResult",
251
+ type: "manager:result",
183
252
  data: {
184
253
  action: "delegateTask",
185
254
  toolCallId: pending.toolCallId,
@@ -187,6 +256,14 @@ Example: If the user asks to "check the weather", and you see a 'browser' agent,
187
256
  },
188
257
  };
189
258
  }
259
+ else {
260
+ // This was a direct command (prefix) — show output directly as assistant text
261
+ yield {
262
+ type: "assistant:text",
263
+ data: { content: event.data.content },
264
+ meta: { agent: agent.name }
265
+ };
266
+ }
190
267
  });
191
268
  }
192
269
  // 6. Init handlers
@@ -0,0 +1,99 @@
1
+ import { generateId } from "melony";
2
+ import { ui } from "@melony/ui-kit/server";
3
+ /**
4
+ * Approval Plugin for OpenBot.
5
+ * Intercepts specific actions and requires user approval before proceeding.
6
+ * Optimized using the new melony intercept() feature.
7
+ */
8
+ export const approvalPlugin = (options) => (builder) => {
9
+ const { rules = [] } = options;
10
+ // Register an interceptor that runs before any handlers.
11
+ // This is the correct way to handle HITL/Approval in Melony.
12
+ builder.intercept(async (event, { state, suspend }) => {
13
+ // Skip if already approved or if it's an internal approval event
14
+ // We cast event to any to access the meta property which is used for internal state tracking
15
+ const meta = event.meta;
16
+ if (meta?.approved ||
17
+ event.type === "action:approve" ||
18
+ event.type === "action:deny" ||
19
+ event.type === "ui" ||
20
+ event.type.endsWith(":status")) {
21
+ return;
22
+ }
23
+ const rule = rules.find(r => event.type.startsWith(r.action));
24
+ if (!rule)
25
+ return;
26
+ const approvalId = `approve_${generateId()}`;
27
+ if (!state.pendingApprovals) {
28
+ state.pendingApprovals = {};
29
+ }
30
+ state.pendingApprovals[approvalId] = event;
31
+ // Use suspend(event) to emit the UI and halt execution of any handlers for this event.
32
+ // This effectively "pauses" the run for user input.
33
+ suspend(ui.event(ui.card({
34
+ title: "Approval Required",
35
+ description: rule.message || `Approval required for: ${event.type}`,
36
+ }, [
37
+ ui.text(JSON.stringify(event.data, null, 2), { size: "xs" }),
38
+ ui.row({ gap: "sm" }, [
39
+ ui.button({
40
+ label: "Approve",
41
+ variant: "primary",
42
+ onClickAction: {
43
+ type: "action:approve",
44
+ data: { id: approvalId }
45
+ }
46
+ }),
47
+ ui.button({
48
+ label: "Deny",
49
+ variant: "outline",
50
+ onClickAction: {
51
+ type: "action:deny",
52
+ data: { id: approvalId }
53
+ }
54
+ }),
55
+ ]),
56
+ ])));
57
+ });
58
+ // Handle Approval response from user
59
+ builder.on("action:approve", async function* (event, { state }) {
60
+ const { id } = event.data;
61
+ const originalEvent = state.pendingApprovals?.[id];
62
+ if (originalEvent) {
63
+ delete state.pendingApprovals[id];
64
+ yield ui.event(ui.status("Action approved", "success"));
65
+ // Re-emit the original event with approved: true.
66
+ // The interceptor will see it, but bypass because of meta.approved.
67
+ // Then the appropriate handlers for the event will finally run.
68
+ yield {
69
+ ...originalEvent,
70
+ meta: {
71
+ ...originalEvent.meta,
72
+ approved: true,
73
+ },
74
+ };
75
+ }
76
+ });
77
+ // Handle Denial response from user
78
+ builder.on("action:deny", async function* (event, { state }) {
79
+ const { id } = event.data;
80
+ const originalEvent = state.pendingApprovals?.[id];
81
+ if (originalEvent) {
82
+ delete state.pendingApprovals[id];
83
+ yield ui.event(ui.status("Action denied", "error"));
84
+ // If it was a tool call (action:*), return a taskResult error so the LLM knows it failed
85
+ if (originalEvent.data?.toolCallId) {
86
+ yield {
87
+ type: "action:taskResult",
88
+ data: {
89
+ action: originalEvent.type.replace("action:", ""),
90
+ toolCallId: originalEvent.data.toolCallId,
91
+ result: { error: "Action denied by user" },
92
+ success: false,
93
+ },
94
+ };
95
+ }
96
+ }
97
+ });
98
+ };
99
+ export default approvalPlugin;
@@ -22,10 +22,11 @@ export const llmPlugin = (options) => (builder) => {
22
22
  state.messages.push(newMessage);
23
23
  // Evaluate dynamic system prompt if it's a function
24
24
  const systemPrompt = typeof system === "function" ? await system(context) : system;
25
+ const recentMessages = getRecentHistory(state.messages, 20);
25
26
  const result = streamText({
26
27
  model,
27
28
  system: systemPrompt,
28
- messages: getRecentHistory(state.messages, 20).map(m => m.role === "system" ? { role: "user", content: `System: ${m.content}` } : m),
29
+ messages: recentMessages,
29
30
  tools: toolDefinitions,
30
31
  });
31
32
  let assistantText = "";
@@ -1,4 +1,5 @@
1
1
  export { PluginRegistry } from "./plugin-registry.js";
2
2
  export { AgentRegistry } from "./agent-registry.js";
3
3
  export { discoverYamlAgents, listYamlAgents } from "./yaml-agent-loader.js";
4
+ export { discoverTsAgents } from "./ts-agent-loader.js";
4
5
  export { loadPluginsFromDir } from "./plugin-loader.js";
@@ -39,7 +39,7 @@ export async function ensurePluginReady(pluginDir) {
39
39
  // 1. Install dependencies if node_modules is missing
40
40
  if (!hasNodeModules) {
41
41
  console.log(`[plugins] Installing dependencies for ${path.basename(pluginDir)}...`);
42
- execSync("npm install --production", { cwd: pluginDir, stdio: "inherit" });
42
+ execSync("npm install", { cwd: pluginDir, stdio: "inherit" });
43
43
  }
44
44
  // 2. Run build if dist is missing but build script exists
45
45
  const distPath = path.join(pluginDir, "dist");
@@ -0,0 +1,82 @@
1
+ import * as fs from "node:fs/promises";
2
+ import * as path from "node:path";
3
+ import { pathToFileURL } from "node:url";
4
+ import { ensurePluginReady } from "./plugin-loader.js";
5
+ /**
6
+ * Discover and load TS-defined agents from a directory.
7
+ *
8
+ * Scans each subdirectory for a package.json and an index file.
9
+ *
10
+ * @param agentsDir Absolute path to the agents directory (e.g. ~/.openbot/agents)
11
+ * @param defaultModel Language model to use for agent LLMs
12
+ * @param options Optional API keys for creating specific models
13
+ * @returns Array of discovered agent entries ready for registration
14
+ */
15
+ export async function discoverTsAgents(agentsDir, defaultModel, options) {
16
+ const agents = [];
17
+ try {
18
+ const entries = await fs.readdir(agentsDir, { withFileTypes: true });
19
+ for (const entry of entries) {
20
+ if (!entry.isDirectory())
21
+ continue;
22
+ if (entry.name.startsWith(".") || entry.name.startsWith("_"))
23
+ continue;
24
+ const agentDir = path.join(agentsDir, entry.name);
25
+ // We only consider it a TS agent if it doesn't have an agent.yaml
26
+ // (This avoids double-loading if someone has both for some reason)
27
+ const yamlPath = path.join(agentDir, "agent.yaml");
28
+ const hasYaml = await fs.access(yamlPath).then(() => true).catch(() => false);
29
+ if (hasYaml)
30
+ continue;
31
+ // Check for package.json to see if it's a package
32
+ const pkgPath = path.join(agentDir, "package.json");
33
+ const hasPackageJson = await fs.access(pkgPath).then(() => true).catch(() => false);
34
+ if (!hasPackageJson)
35
+ continue;
36
+ try {
37
+ // 1. Ensure dependencies and build are ready
38
+ await ensurePluginReady(agentDir);
39
+ // 2. Find index file
40
+ let indexPath;
41
+ const possibleIndices = ["dist/index.js", "index.js", "index.ts"];
42
+ for (const file of possibleIndices) {
43
+ try {
44
+ const fullPath = path.join(agentDir, file);
45
+ await fs.access(fullPath);
46
+ indexPath = fullPath;
47
+ break;
48
+ }
49
+ catch {
50
+ continue;
51
+ }
52
+ }
53
+ if (!indexPath)
54
+ continue;
55
+ // 3. Import and instantiate
56
+ const moduleUrl = pathToFileURL(indexPath).href;
57
+ const module = await import(moduleUrl);
58
+ // Support 'agent', 'plugin', 'default', or 'entry'
59
+ const definition = module.agent || module.plugin || module.default || module.entry;
60
+ if (definition && typeof definition.factory === "function") {
61
+ const name = definition.name || entry.name;
62
+ const description = definition.description || "TS Agent";
63
+ agents.push({
64
+ name,
65
+ description,
66
+ plugin: definition.factory({ ...options, model: defaultModel }),
67
+ capabilities: definition.capabilities,
68
+ subscribe: definition.subscribe,
69
+ });
70
+ console.log(`[agents] Loaded TS agent: ${name} — ${description}`);
71
+ }
72
+ }
73
+ catch (err) {
74
+ console.warn(`[agents] Failed to load TS agent package "${entry.name}":`, err);
75
+ }
76
+ }
77
+ }
78
+ catch {
79
+ // Agents directory doesn't exist
80
+ }
81
+ return agents;
82
+ }
@@ -32,9 +32,9 @@ export const settingsUI = async () => {
32
32
  ui.heading("Model Configuration", 4),
33
33
  ui.col({ gap: "sm" }, [
34
34
  ui.input("model", undefined, {
35
- placeholder: "e.g. gpt-4o-mini",
35
+ placeholder: "provider/model (e.g. openai/gpt-4o)",
36
36
  width: "full",
37
- defaultValue: config.model || "gpt-4o-mini"
37
+ defaultValue: config.model || "openai/gpt-4o-mini"
38
38
  }),
39
39
  ]),
40
40
  ]),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openbot",
3
- "version": "0.1.27",
3
+ "version": "0.1.29",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {
@@ -18,7 +18,7 @@
18
18
  "express": "^4.19.2",
19
19
  "gray-matter": "^4.0.3",
20
20
  "js-yaml": "^4.1.1",
21
- "melony": "^0.2.8",
21
+ "melony": "^0.2.9",
22
22
  "zod": "^4.3.5"
23
23
  },
24
24
  "devDependencies": {