openbot 0.1.23 → 0.1.25

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,8 +1,6 @@
1
1
  import { llmPlugin } from "../plugins/llm/index.js";
2
2
  import { shellPlugin, shellToolDefinitions } from "../plugins/shell/index.js";
3
- import { shellUIPlugin } from "../plugins/shell/ui.js";
4
3
  import { fileSystemPlugin, fileSystemToolDefinitions } from "../plugins/file-system/index.js";
5
- import { fileSystemUIPlugin } from "../plugins/file-system/ui.js";
6
4
  const DEFAULT_SYSTEM_PROMPT = `You are an OS Agent with access to the shell and file system.
7
5
  Your job is to help the user with file operations and command execution.
8
6
  You can read, write, list, and delete files, as well as execute shell commands.
@@ -12,9 +10,7 @@ export const osAgent = (options) => (builder) => {
12
10
  const { model, cwd = process.cwd(), systemPrompt = DEFAULT_SYSTEM_PROMPT } = options;
13
11
  builder
14
12
  .use(shellPlugin({ cwd }))
15
- .use(shellUIPlugin())
16
13
  .use(fileSystemPlugin({ baseDir: "/" }))
17
- .use(fileSystemUIPlugin())
18
14
  .use(llmPlugin({
19
15
  model,
20
16
  system: systemPrompt,
@@ -0,0 +1,32 @@
1
+ import { generateText } from "ai";
2
+ export const topicAgent = (options) => (builder) => {
3
+ builder.on("manager:completion", async function* (event, { state }) {
4
+ // Only title if it doesn't have one and there's history
5
+ if (state.title || !state.messages || state.messages.length === 0) {
6
+ return;
7
+ }
8
+ // Don't title if there are too few messages
9
+ if (state.messages.length < 2) {
10
+ return;
11
+ }
12
+ try {
13
+ const { text } = await generateText({
14
+ model: options.model,
15
+ system: "You are a Topic Agent. Create a very concise (3-5 words) title for the conversation based on the user's intent. Do not use quotes or special characters.",
16
+ prompt: `Analyze these messages and provide a title: ${JSON.stringify(state.messages.slice(0, 6))}`,
17
+ });
18
+ const newTitle = text.replace(/["']/g, "").trim();
19
+ if (newTitle) {
20
+ state.title = newTitle;
21
+ // Notify the client to refresh the sessions list in the sidebar
22
+ yield {
23
+ type: "client:invalidate",
24
+ data: { tags: ["sessions"] }
25
+ };
26
+ }
27
+ }
28
+ catch (error) {
29
+ console.error("[topic-agent] Failed to generate title:", error);
30
+ }
31
+ });
32
+ };
package/dist/cli.js CHANGED
@@ -1,8 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from "commander";
3
3
  import * as readline from "node:readline/promises";
4
- import { saveConfig } from "./config.js";
4
+ import * as fs from "node:fs/promises";
5
+ import * as path from "node:path";
6
+ import { execSync } from "node:child_process";
7
+ import { tmpdir } from "node:os";
8
+ import { saveConfig, resolvePath, DEFAULT_BASE_DIR } from "./config.js";
5
9
  import { startServer } from "./server.js";
10
+ import { ensurePluginReady } from "./registry/plugin-loader.js";
6
11
  const program = new Command();
7
12
  program
8
13
  .name("openbot")
@@ -20,13 +25,13 @@ program
20
25
  console.log("🍎 OpenBot Configuration");
21
26
  console.log("------------------------------------------");
22
27
  const models = [
23
- { name: "GPT-5 Nano (OpenAI)", value: "openai:gpt-5-nano" },
24
- { name: "GPT-4o (OpenAI)", value: "openai:gpt-4o" },
25
- { name: "GPT-4o-mini (OpenAI)", value: "openai:gpt-4o-mini" },
26
- { name: "Claude Opus 4.5 (Anthropic)", value: "anthropic:claude-opus-4-5-20251101" },
27
- { name: "Claude Sonnet 4.5 (Anthropic)", value: "anthropic:claude-sonnet-4-5-20250929" },
28
- { name: "Claude 3.7 Sonnet (Anthropic)", value: "anthropic:claude-3-7-sonnet-20250219" },
29
- { name: "Claude 3.5 Sonnet (Anthropic)", value: "anthropic:claude-3-5-sonnet-20240620" },
28
+ { name: "GPT-5 Nano (OpenAI)", value: "openai/gpt-5-nano" },
29
+ { name: "GPT-4o (OpenAI)", value: "openai/gpt-4o" },
30
+ { name: "GPT-4o-mini (OpenAI)", value: "openai/gpt-4o-mini" },
31
+ { name: "Claude Opus 4.5 (Anthropic)", value: "anthropic/claude-opus-4-5-20251101" },
32
+ { name: "Claude Sonnet 4.5 (Anthropic)", value: "anthropic/claude-sonnet-4-5-20250929" },
33
+ { name: "Claude 3.7 Sonnet (Anthropic)", value: "anthropic/claude-3-7-sonnet-20250219" },
34
+ { name: "Claude 3.5 Sonnet (Anthropic)", value: "anthropic/claude-3-5-sonnet-20240620" },
30
35
  ];
31
36
  console.log("Please choose a model:");
32
37
  models.forEach((m, i) => console.log(`${i + 1}) ${m.name}`));
@@ -67,4 +72,105 @@ program
67
72
  .action(async (options) => {
68
73
  await startServer(options);
69
74
  });
75
+ const plugin = program.command("plugin").description("Manage OpenBot plugins");
76
+ plugin
77
+ .command("install <source>")
78
+ .description("Install a shared plugin from GitHub (user/repo) or a local path")
79
+ .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
+ }
131
+ });
132
+ plugin
133
+ .command("list")
134
+ .description("List all installed shared plugins")
135
+ .action(async () => {
136
+ const baseDir = resolvePath(DEFAULT_BASE_DIR);
137
+ const pluginsDir = path.join(baseDir, "plugins");
138
+ try {
139
+ await fs.access(pluginsDir);
140
+ }
141
+ catch {
142
+ console.log("No shared plugins found.");
143
+ return;
144
+ }
145
+ const entries = await fs.readdir(pluginsDir, { withFileTypes: true });
146
+ const plugins = [];
147
+ for (const entry of entries) {
148
+ if (!entry.isDirectory())
149
+ continue;
150
+ if (entry.name.startsWith(".") || entry.name.startsWith("_"))
151
+ 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 });
164
+ }
165
+ if (plugins.length === 0) {
166
+ console.log("No shared plugins found.");
167
+ return;
168
+ }
169
+ console.log("\n🔌 Installed Shared Plugins:");
170
+ console.log("------------------------------------------");
171
+ for (const p of plugins) {
172
+ console.log(`${p.name.padEnd(20)} (${p.version}) - ${p.description}`);
173
+ }
174
+ console.log("------------------------------------------\n");
175
+ });
70
176
  program.parse();
@@ -0,0 +1,29 @@
1
+ import { saveConfig } from "../config.js";
2
+ import { tabOnlyUI } from "../ui/layout.js";
3
+ /**
4
+ * Handle settings updates (like API keys)
5
+ */
6
+ export async function* updateSettingsHandler(event, { state }) {
7
+ const { openai_api_key, anthropic_api_key, model } = event.data;
8
+ const updates = {};
9
+ if (model) {
10
+ updates.model = model.trim();
11
+ }
12
+ if (openai_api_key && openai_api_key !== "••••••••••••••••") {
13
+ updates.openaiApiKey = openai_api_key.trim();
14
+ }
15
+ if (anthropic_api_key && anthropic_api_key !== "••••••••••••••••") {
16
+ updates.anthropicApiKey = anthropic_api_key.trim();
17
+ }
18
+ if (Object.keys(updates).length > 0) {
19
+ saveConfig(updates);
20
+ // Refresh the settings UI to show the "saved" state (masking the keys)
21
+ yield {
22
+ type: "ui",
23
+ meta: {
24
+ type: "content",
25
+ },
26
+ data: await tabOnlyUI({ tab: "settings" })
27
+ };
28
+ }
29
+ }
package/dist/models.js CHANGED
@@ -3,16 +3,9 @@ import { anthropic } from "@ai-sdk/anthropic";
3
3
  import { loadConfig } from "./config.js";
4
4
  /**
5
5
  * Parse model string to extract provider and model ID
6
- * Supports formats: "provider:model", "provider/model", or just "model" (defaults to openai)
6
+ * Supports formats: "provider/model" or just "model" (defaults to openai)
7
7
  */
8
8
  export function parseModelString(modelString) {
9
- // Check for provider:model format
10
- if (modelString.includes(":")) {
11
- const [provider, modelId] = modelString.split(":");
12
- if (provider === "openai" || provider === "anthropic") {
13
- return { provider: provider, modelId };
14
- }
15
- }
16
9
  // Check for provider/model format
17
10
  if (modelString.includes("/")) {
18
11
  const [provider, modelId] = modelString.split("/");
package/dist/open-bot.js CHANGED
@@ -1,25 +1,21 @@
1
1
  import { melony } from "melony";
2
2
  import { osAgent } from "./agents/os-agent.js";
3
- import { browserAgent } from "./agents/browser-agent.js";
4
- import { browserUIPlugin } from "./plugins/browser/ui.js";
3
+ import { topicAgent } from "./agents/topic-agent.js";
5
4
  import { brainPlugin, brainToolDefinitions, createBrainPromptBuilder } from "./plugins/brain/index.js";
6
- import { brainUIPlugin } from "./plugins/brain/ui.js";
7
5
  import { llmPlugin } from "./plugins/llm/index.js";
8
6
  import { initHandler } from "./handlers/init.js";
9
7
  import { sessionChangeHandler } from "./handlers/session-change.js";
10
8
  import { tabChangeHandler } from "./handlers/tab-change.js";
9
+ import { updateSettingsHandler } from "./handlers/settings.js";
11
10
  import { loadConfig, resolvePath, DEFAULT_BASE_DIR } from "./config.js";
12
11
  import { createModel } from "./models.js";
13
12
  import path from "node:path";
14
13
  import { z } from "zod";
15
14
  // Plugin imports for the registry
16
15
  import { shellPlugin, shellToolDefinitions } from "./plugins/shell/index.js";
17
- import { shellUIPlugin } from "./plugins/shell/ui.js";
18
16
  import { fileSystemPlugin, fileSystemToolDefinitions } from "./plugins/file-system/index.js";
19
- import { fileSystemUIPlugin } from "./plugins/file-system/ui.js";
20
- import { browserPlugin, browserToolDefinitions } from "./plugins/browser/index.js";
21
17
  // Registry
22
- import { PluginRegistry, AgentRegistry, discoverYamlAgents } from "./registry/index.js";
18
+ import { PluginRegistry, AgentRegistry, discoverYamlAgents, loadPluginsFromDir } from "./registry/index.js";
23
19
  /**
24
20
  * Create the OpenBot manager agent.
25
21
  *
@@ -43,44 +39,40 @@ export async function createOpenBot(options) {
43
39
  description: "Execute shell commands",
44
40
  toolDefinitions: shellToolDefinitions,
45
41
  factory: () => shellPlugin({ cwd: process.cwd() }),
46
- uiFactory: () => shellUIPlugin(),
47
42
  });
48
43
  pluginRegistry.register({
49
44
  name: "file-system",
50
45
  description: "Read, write, list, and delete files",
51
46
  toolDefinitions: fileSystemToolDefinitions,
52
47
  factory: () => fileSystemPlugin({ baseDir: "/" }),
53
- uiFactory: () => fileSystemUIPlugin(),
54
- });
55
- pluginRegistry.register({
56
- name: "browser",
57
- description: "Browse the web and interact with pages",
58
- toolDefinitions: browserToolDefinitions,
59
- factory: () => browserPlugin({
60
- headless: true,
61
- userDataDir,
62
- channel: "chrome",
63
- model: model,
64
- }),
65
- uiFactory: () => browserUIPlugin(),
66
48
  });
49
+ // ─── Shared Plugins ──────────────────────────────────────────────
50
+ // Load community/user plugins from ~/.openbot/plugins/
51
+ const sharedPlugins = await loadPluginsFromDir(path.join(resolvedBaseDir, "plugins"));
52
+ for (const p of sharedPlugins) {
53
+ pluginRegistry.register(p);
54
+ console.log(`[plugins] Loaded shared plugin: ${p.name}`);
55
+ }
67
56
  // ─── Agent Registry ──────────────────────────────────────────────
68
57
  // Register built-in agents, then discover YAML agents from ~/.openbot/agents/.
69
58
  const agentRegistry = new AgentRegistry();
70
59
  agentRegistry.register({
71
60
  name: "os",
72
61
  description: "Handles shell commands and file system operations",
62
+ capabilities: {
63
+ ...Object.fromEntries(Object.entries(shellToolDefinitions).map(([k, v]) => [k, v.description])),
64
+ ...Object.fromEntries(Object.entries(fileSystemToolDefinitions).map(([k, v]) => [
65
+ k,
66
+ v.description,
67
+ ])),
68
+ },
73
69
  plugin: osAgent({ model: model }),
74
70
  });
75
71
  agentRegistry.register({
76
- name: "browser",
77
- description: "Browses the web, extracts data, and interacts with pages",
78
- plugin: browserAgent({
79
- model: model,
80
- headless: true,
81
- userDataDir,
82
- channel: "chrome",
83
- }),
72
+ name: "topic",
73
+ description: "Automatically titles threads",
74
+ plugin: topicAgent({ model: model }),
75
+ subscribe: ["manager:completion"],
84
76
  });
85
77
  // Discover community / user agents from ~/.openbot/agents/
86
78
  const yamlAgents = await discoverYamlAgents(path.join(resolvedBaseDir, "agents"), pluginRegistry, model, options);
@@ -94,35 +86,76 @@ export async function createOpenBot(options) {
94
86
  // 1. Register all agent plugins
95
87
  for (const agent of allAgents) {
96
88
  app.use(agent.plugin);
89
+ // Choreography bridge: Auto-wire subscriptions
90
+ if (agent.subscribe) {
91
+ for (const eventType of agent.subscribe) {
92
+ app.on(eventType, async function* (event, { state }) {
93
+ // Avoid self-triggering if the event has agent meta
94
+ if (event.meta?.agent === agent.name)
95
+ return;
96
+ yield {
97
+ type: `agent:${agent.name}:input`,
98
+ data: {
99
+ content: `Event observed: ${event.type}\nData: ${JSON.stringify(event.data)}`,
100
+ },
101
+ meta: {
102
+ background: true,
103
+ agent: agent.name
104
+ },
105
+ };
106
+ });
107
+ }
108
+ }
97
109
  }
98
110
  // 2. Register global plugins (brain, UI, etc.)
99
111
  const buildBrainPrompt = createBrainPromptBuilder(baseDir);
100
112
  app
101
- .use(browserUIPlugin())
102
113
  .use(brainPlugin({
103
114
  baseDir: resolvedBaseDir,
104
115
  allowSoulModification: false,
105
- }))
106
- .use(brainUIPlugin());
116
+ }));
107
117
  // 3. Build dynamic delegation tool from the agent registry
108
118
  const agentDescriptions = allAgents
109
- .map((a) => `- ${a.name}: ${a.description}`)
110
- .join("\n");
119
+ .map((a) => {
120
+ const tools = a.capabilities
121
+ ? Object.entries(a.capabilities)
122
+ .map(([name, desc]) => ` - ${name}: ${desc}`)
123
+ .join("\n")
124
+ : "";
125
+ return `- **${a.name}**: ${a.description}${tools ? `\n Capabilities:\n${tools}` : ""}`;
126
+ })
127
+ .join("\n\n");
111
128
  app.use(llmPlugin({
112
129
  model: model,
113
130
  system: async (context) => {
114
131
  const [brainPrompt] = await Promise.all([
115
132
  buildBrainPrompt(context),
116
133
  ]);
117
- return `${brainPrompt}`;
134
+ return `${brainPrompt}
135
+
136
+ ## Delegation & Specialized Agents
137
+ You are the **Manager Agent**. Your primary role is to orchestrate tasks by delegating them to specialized agents when appropriate.
138
+ If a task falls outside your core capabilities (memory and orchestration), you **MUST** use the \`delegateTask\` tool.
139
+
140
+ ### Available Agents:
141
+ ${agentDescriptions}
142
+
143
+ ### Delegation Guidelines:
144
+ 1. **Choose the Best Expert**: Analyze the user's request and select the agent whose description most closely matches the required expertise.
145
+ 2. **Task Description**: When delegating, provide a clear and detailed task description. Include any context the agent might need to succeed.
146
+ 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.
147
+ 4. **Summary**: Once an agent returns a result, summarize the findings or actions for the user.
148
+
149
+ Example: If the user asks to "check the weather", and you see a 'browser' agent, delegate the task to it.`;
118
150
  },
151
+ completionEventType: "manager:completion",
119
152
  toolDefinitions: {
120
153
  ...brainToolDefinitions,
121
154
  delegateTask: {
122
- description: `Delegate a task to a specialized agent.\n\nAvailable agents:\n${agentDescriptions}`,
155
+ description: `Delegate a specialized task to another agent. Use this whenever a task matches the capabilities of one of the available agents.`,
123
156
  inputSchema: z.object({
124
157
  agent: z.enum(agentNames).describe("The specialized agent to use"),
125
- task: z.string().describe("The task description"),
158
+ task: z.string().describe("The detailed task description for the agent"),
126
159
  }),
127
160
  },
128
161
  },
@@ -161,6 +194,7 @@ export async function createOpenBot(options) {
161
194
  app
162
195
  .on("init", initHandler)
163
196
  .on("sessionChange", sessionChangeHandler)
164
- .on("tabChange", tabChangeHandler);
197
+ .on("tabChange", tabChangeHandler)
198
+ .on("action:updateSettings", updateSettingsHandler);
165
199
  return app;
166
200
  }
@@ -17,17 +17,18 @@ const DEFAULT_SOUL = `# Soul
17
17
  `;
18
18
  const DEFAULT_IDENTITY = `# Identity
19
19
 
20
- I am OpenBot, a self-evolving AI assistant built on the OpenBot framework.
20
+ I am the Manager Agent and central orchestrator of this AI system. My name and specific personality are defined by the user in this IDENTITY.md file.
21
21
 
22
22
  ## Personality
23
23
  - Friendly and approachable
24
24
  - Technically competent
25
25
  - Eager to learn and adapt
26
+ - Professional manager and delegator
26
27
 
27
28
  ## Capabilities
28
- - Shell command execution
29
- - File system operations
30
- - Skill-based task execution
29
+ - Task Orchestration & Delegation
30
+ - Long-term Memory & Knowledge Management
31
+ - Executing specialized tasks via expert agents (Web, OS, etc.)
31
32
  - Self-modification and learning
32
33
  `;
33
34
  // --- Factory ---
@@ -1,3 +1,4 @@
1
+ import { ui } from "@melony/ui-kit/server";
1
2
  import * as fs from "node:fs/promises";
2
3
  import * as path from "node:path";
3
4
  import { createIdentityModule } from "./identity.js";
@@ -6,7 +7,6 @@ import { buildBrainPrompt } from "./prompt.js";
6
7
  // Re-exports
7
8
  export { brainToolDefinitions } from "./types.js";
8
9
  export { buildBrainPrompt } from "./prompt.js";
9
- // --- Helpers ---
10
10
  function expandPath(p) {
11
11
  if (p.startsWith("~/")) {
12
12
  return path.join(process.env.HOME || "", p.slice(2));
@@ -265,5 +265,8 @@ export const brainPlugin = (options) => (builder) => {
265
265
  };
266
266
  }
267
267
  });
268
+ builder.on("brain:status", async function* (event) {
269
+ yield ui.event(ui.status(event.data.message, event.data.severity));
270
+ });
268
271
  };
269
272
  export default brainPlugin;
@@ -18,26 +18,26 @@ export async function buildBrainPrompt(baseDir, modules, context) {
18
18
  // 1. Environment context
19
19
  const now = new Date();
20
20
  parts.push(`## Environment
21
- You are running as a global system agent.
21
+ You are the **Manager Agent**, the central orchestrator of this system.
22
22
  - **Current Time**: ${now.toLocaleString()} (${Intl.DateTimeFormat().resolvedOptions().timeZone})
23
23
  - **Current Working Directory (CWD)**: ${currentCwd}
24
24
  - **System Access**: You have access to the entire file system (root: /).
25
25
  - **Bot Home (Internal State)**: ${baseDir}
26
26
 
27
+ ### Delegation Policy:
28
+ You are designed to delegate specialized tasks to expert agents. Analyze the "Available Agents" list provided in your instructions and delegate whenever a user request matches an agent's expertise. You should not claim you cannot perform a task if a suitable agent is available.
29
+
27
30
  ### Path Rules:
28
31
  1. **Shell Commands**: All commands (executeCommand) run in the CWD: ${currentCwd}.
29
32
  2. **File Operations**: Relative paths in readFile, writeFile, listFiles, etc. resolve against the CWD.
30
- 3. **Changing Directory**: Use \`cd <path>\` in executeCommand to move. Your CWD is persisted across turns.
31
- 4. **Skills/Memory**: To access your own skills and memory, use absolute paths starting with "${baseDir}/".
32
-
33
- When you want to execute skill scripts, always use the full path to the skill directory.`);
33
+ 3. **Changing Directory**: Use \`cd <path>\` in executeCommand to move. Your CWD is persisted across turns.`);
34
34
  // 2. Identity (small, always included)
35
35
  const soul = await modules.identity.getSoul();
36
36
  if (soul)
37
- parts.push(soul);
37
+ parts.push(`<soul>\n${soul}\n</soul>`);
38
38
  const identity = await modules.identity.getIdentity();
39
39
  if (identity)
40
- parts.push(identity);
40
+ parts.push(`<identity>\n${identity}\n</identity>`);
41
41
  // 3. Recent memories (lean — just a few to keep context fresh)
42
42
  const recentFacts = await modules.memory.getRecentFacts(3);
43
43
  if (recentFacts.length > 0) {
@@ -44,7 +44,7 @@ export const brainToolDefinitions = {
44
44
  },
45
45
  // Identity tools
46
46
  updateIdentity: {
47
- description: "Update your identity file to refine your personality and traits.",
47
+ description: "Update your identity file to refine your personality and traits. Start it with # Identity.",
48
48
  inputSchema: z.object({
49
49
  content: z.string().describe("New content for IDENTITY.md"),
50
50
  }),
@@ -1,3 +1,4 @@
1
+ import { ui } from "@melony/ui-kit/server";
1
2
  import { z } from "zod";
2
3
  import * as fs from "node:fs/promises";
3
4
  import * as path from "node:path";
@@ -163,4 +164,7 @@ export const fileSystemPlugin = (options = {}) => (builder) => {
163
164
  };
164
165
  }
165
166
  });
167
+ builder.on("file-system:status", async function* (event) {
168
+ yield ui.event(ui.status(event.data.message, event.data.severity));
169
+ });
166
170
  };
@@ -1,5 +1,4 @@
1
1
  import { streamText } from "ai";
2
- import { ui } from "@melony/ui-kit";
3
2
  /**
4
3
  * Builds a simple history summary from recent messages.
5
4
  * Keeps the last N messages as simple role/content pairs.
@@ -53,7 +52,16 @@ export const llmPlugin = (options) => (builder) => {
53
52
  }
54
53
  }
55
54
  const usage = await result.usage;
56
- yield ui.event(ui.status(`Usage: ${usage.totalTokens} tokens`, "info"));
55
+ if (!state.usage) {
56
+ state.usage = {
57
+ inputTokens: 0,
58
+ outputTokens: 0,
59
+ totalTokens: 0,
60
+ };
61
+ }
62
+ state.usage.inputTokens += usage.inputTokens ?? 0;
63
+ state.usage.outputTokens += usage.outputTokens ?? 0;
64
+ state.usage.totalTokens += usage.totalTokens ?? 0;
57
65
  // Emit tool call events
58
66
  for (const call of toolCalls) {
59
67
  yield {
@@ -1,3 +1,4 @@
1
+ import { ui } from "@melony/ui-kit/server";
1
2
  import { z } from "zod";
2
3
  import { exec } from "node:child_process";
3
4
  import { promisify } from "node:util";
@@ -92,4 +93,7 @@ export const shellPlugin = (options = {}) => (builder) => {
92
93
  };
93
94
  }
94
95
  });
96
+ builder.on("shell:status", async function* (event) {
97
+ yield ui.event(ui.status(event.data.message, event.data.severity));
98
+ });
95
99
  };
@@ -1,3 +1,4 @@
1
+ import { ui } from "@melony/ui-kit/server";
1
2
  import * as fs from "node:fs/promises";
2
3
  import * as path from "node:path";
3
4
  import matter from "gray-matter";
@@ -271,5 +272,13 @@ export const skillsPlugin = (options) => (builder) => {
271
272
  };
272
273
  }
273
274
  });
275
+ builder.on("skills:status", async function* (event) {
276
+ yield ui.event(ui.status(event.data.message, event.data.severity));
277
+ });
278
+ builder.on("skills:loaded", async function* (event) {
279
+ yield ui.event(ui.resourceCard(event.data.title, "", [
280
+ ui.text(event.data.instructions),
281
+ ]));
282
+ });
274
283
  };
275
284
  export default skillsPlugin;
@@ -1,3 +1,4 @@
1
1
  export { PluginRegistry } from "./plugin-registry.js";
2
2
  export { AgentRegistry } from "./agent-registry.js";
3
3
  export { discoverYamlAgents } from "./yaml-agent-loader.js";
4
+ export { loadPluginsFromDir } from "./plugin-loader.js";