openbot 0.1.27 → 0.1.28

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.28");
17
17
  /**
18
18
  * Check if a GitHub repository exists.
19
19
  */
package/dist/open-bot.js CHANGED
@@ -14,6 +14,7 @@ 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
19
  import { PluginRegistry, AgentRegistry, discoverYamlAgents, loadPluginsFromDir } from "./registry/index.js";
19
20
  /**
@@ -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"));
@@ -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;
@@ -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");
@@ -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.28",
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": {