openbot 0.1.20 → 0.1.23
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/README.md +1 -1
- package/dist/agents/browser-agent.js +31 -0
- package/dist/agents/os-agent.js +31 -0
- package/dist/cli.js +13 -3
- package/dist/handlers/init.js +14 -2
- package/dist/handlers/session-change.js +21 -0
- package/dist/handlers/tab-change.js +14 -0
- package/dist/models.js +53 -0
- package/dist/open-bot.js +166 -0
- package/dist/plugins/agent/index.js +81 -0
- package/dist/plugins/brain/identity.js +76 -0
- package/dist/plugins/brain/index.js +269 -0
- package/dist/plugins/brain/memory.js +120 -0
- package/dist/plugins/brain/prompt.js +64 -0
- package/dist/plugins/brain/types.js +60 -0
- package/dist/plugins/brain/ui.js +7 -0
- package/dist/plugins/browser/index.js +629 -0
- package/dist/plugins/browser/ui.js +13 -0
- package/dist/plugins/file-system/index.js +166 -0
- package/dist/plugins/file-system/ui.js +6 -0
- package/dist/plugins/llm/index.js +81 -0
- package/dist/plugins/meta-agent/index.js +570 -0
- package/dist/plugins/meta-agent/ui.js +11 -0
- package/dist/plugins/shell/index.js +95 -0
- package/dist/plugins/shell/ui.js +6 -0
- package/dist/plugins/skills/index.js +275 -0
- package/dist/plugins/skills/types.js +50 -0
- package/dist/plugins/skills/ui.js +12 -0
- package/dist/registry/agent-registry.js +35 -0
- package/dist/registry/index.js +3 -0
- package/dist/registry/plugin-registry.js +27 -0
- package/dist/registry/yaml-agent-loader.js +100 -0
- package/dist/server.js +30 -31
- package/dist/ui/header.js +4 -7
- package/dist/ui/layout.js +7 -7
- package/dist/ui/navigation.js +4 -2
- package/dist/ui/sidebar.js +42 -11
- package/dist/ui/thread.js +10 -8
- package/package.json +12 -13
- package/dist/agent.js +0 -110
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import matter from "gray-matter";
|
|
4
|
+
// Re-exports
|
|
5
|
+
export { skillsToolDefinitions } from "./types.js";
|
|
6
|
+
// --- Helpers ---
|
|
7
|
+
function expandPath(p) {
|
|
8
|
+
if (p.startsWith("~/")) {
|
|
9
|
+
return path.join(process.env.HOME || "", p.slice(2));
|
|
10
|
+
}
|
|
11
|
+
return p;
|
|
12
|
+
}
|
|
13
|
+
// --- Skills Module (internal) ---
|
|
14
|
+
function createSkillsModule(skillsDir) {
|
|
15
|
+
async function list() {
|
|
16
|
+
const skills = [];
|
|
17
|
+
try {
|
|
18
|
+
const folders = await fs.readdir(skillsDir);
|
|
19
|
+
for (const folder of folders) {
|
|
20
|
+
if (folder.startsWith("_") || folder.startsWith("."))
|
|
21
|
+
continue;
|
|
22
|
+
const skillPath = path.join(skillsDir, folder, "SKILL.md");
|
|
23
|
+
try {
|
|
24
|
+
const content = await fs.readFile(skillPath, "utf-8");
|
|
25
|
+
const { data } = matter(content);
|
|
26
|
+
skills.push({
|
|
27
|
+
id: folder,
|
|
28
|
+
title: data.title || folder,
|
|
29
|
+
description: data.description || "No description",
|
|
30
|
+
version: data.version,
|
|
31
|
+
tools: data.tools,
|
|
32
|
+
triggers: data.triggers,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
// Invalid skill, skip
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// Skills directory doesn't exist yet
|
|
42
|
+
}
|
|
43
|
+
return skills;
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
async initialize() {
|
|
47
|
+
await fs.mkdir(skillsDir, { recursive: true });
|
|
48
|
+
},
|
|
49
|
+
list,
|
|
50
|
+
async load(skillId) {
|
|
51
|
+
const skillPath = path.join(skillsDir, skillId, "SKILL.md");
|
|
52
|
+
const content = await fs.readFile(skillPath, "utf-8");
|
|
53
|
+
const { data, content: body } = matter(content);
|
|
54
|
+
return { meta: data, instructions: body.trim() };
|
|
55
|
+
},
|
|
56
|
+
async create(id, title, description, content) {
|
|
57
|
+
if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(id)) {
|
|
58
|
+
throw new Error("Skill id must be kebab-case (e.g., 'my-skill')");
|
|
59
|
+
}
|
|
60
|
+
const skillDir = path.join(skillsDir, id);
|
|
61
|
+
await fs.mkdir(skillDir, { recursive: true });
|
|
62
|
+
const skillContent = `---
|
|
63
|
+
title: ${title}
|
|
64
|
+
description: ${description}
|
|
65
|
+
version: 1.0.0
|
|
66
|
+
createdAt: ${new Date().toISOString()}
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
${content}`;
|
|
70
|
+
await fs.writeFile(path.join(skillDir, "SKILL.md"), skillContent, "utf-8");
|
|
71
|
+
return `skills/${id}/SKILL.md`;
|
|
72
|
+
},
|
|
73
|
+
async update(id, content, title, description) {
|
|
74
|
+
const skillPath = path.join(skillsDir, id, "SKILL.md");
|
|
75
|
+
const existingContent = await fs.readFile(skillPath, "utf-8");
|
|
76
|
+
const { data: existingData } = matter(existingContent);
|
|
77
|
+
const newTitle = title || existingData.title || id;
|
|
78
|
+
const newDescription = description || existingData.description || "No description";
|
|
79
|
+
// Bump patch version
|
|
80
|
+
let version = existingData.version || "1.0.0";
|
|
81
|
+
const parts = version.split(".");
|
|
82
|
+
if (parts.length === 3) {
|
|
83
|
+
parts[2] = (parseInt(parts[2]) + 1).toString();
|
|
84
|
+
version = parts.join(".");
|
|
85
|
+
}
|
|
86
|
+
const skillContent = `---
|
|
87
|
+
title: ${newTitle}
|
|
88
|
+
description: ${newDescription}
|
|
89
|
+
version: ${version}
|
|
90
|
+
updatedAt: ${new Date().toISOString()}
|
|
91
|
+
createdAt: ${existingData.createdAt || new Date().toISOString()}
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
${content}`;
|
|
95
|
+
await fs.writeFile(skillPath, skillContent, "utf-8");
|
|
96
|
+
return version;
|
|
97
|
+
},
|
|
98
|
+
async getIndex() {
|
|
99
|
+
const skills = await list();
|
|
100
|
+
if (skills.length > 0) {
|
|
101
|
+
return `## Available Skills
|
|
102
|
+
|
|
103
|
+
You have the following skills available. Use \`loadSkill\` with the skill id to get full instructions before executing.
|
|
104
|
+
|
|
105
|
+
${skills.map((s) => `- **${s.title}** (\`${s.id}\`): ${s.description}`).join("\n")}`;
|
|
106
|
+
}
|
|
107
|
+
return `## Skills
|
|
108
|
+
|
|
109
|
+
You have no skills yet. When you learn reusable patterns, create skills using \`createSkill\` so you can use them again later.`;
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
// --- Prompt Builder ---
|
|
114
|
+
/**
|
|
115
|
+
* Create a prompt builder that generates the skills section
|
|
116
|
+
* of the system prompt. Use alongside the brain prompt builder.
|
|
117
|
+
*/
|
|
118
|
+
export function createSkillsPromptBuilder(baseDir) {
|
|
119
|
+
const expandedBase = expandPath(baseDir);
|
|
120
|
+
const skills = createSkillsModule(path.join(expandedBase, "skills"));
|
|
121
|
+
return async () => skills.getIndex();
|
|
122
|
+
}
|
|
123
|
+
// --- Plugin ---
|
|
124
|
+
/**
|
|
125
|
+
* Skills Plugin for Melony
|
|
126
|
+
*
|
|
127
|
+
* Manages reusable skill definitions: load, create, update, list.
|
|
128
|
+
* Fully independent from the brain plugin.
|
|
129
|
+
*/
|
|
130
|
+
export const skillsPlugin = (options) => (builder) => {
|
|
131
|
+
const expandedBase = expandPath(options.baseDir);
|
|
132
|
+
const skills = createSkillsModule(path.join(expandedBase, "skills"));
|
|
133
|
+
// ─── Initialization ───────────────────────────────────────────────
|
|
134
|
+
builder.on("init", async function* () {
|
|
135
|
+
await skills.initialize();
|
|
136
|
+
yield {
|
|
137
|
+
type: "skills:status",
|
|
138
|
+
data: { message: "Skills initialized", severity: "success" },
|
|
139
|
+
};
|
|
140
|
+
});
|
|
141
|
+
// ─── Load ─────────────────────────────────────────────────────────
|
|
142
|
+
builder.on("action:loadSkill", async function* (event) {
|
|
143
|
+
const { skillId, toolCallId } = event.data;
|
|
144
|
+
try {
|
|
145
|
+
const { meta, instructions } = await skills.load(skillId);
|
|
146
|
+
yield {
|
|
147
|
+
type: "skills:loaded",
|
|
148
|
+
data: {
|
|
149
|
+
skillId,
|
|
150
|
+
title: meta.title || skillId,
|
|
151
|
+
instructions: meta.description || "No description",
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
yield {
|
|
155
|
+
type: "action:taskResult",
|
|
156
|
+
data: {
|
|
157
|
+
action: "loadSkill",
|
|
158
|
+
toolCallId,
|
|
159
|
+
result: { id: skillId, meta, instructions },
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
yield {
|
|
165
|
+
type: "action:taskResult",
|
|
166
|
+
data: {
|
|
167
|
+
action: "loadSkill",
|
|
168
|
+
toolCallId,
|
|
169
|
+
result: { error: `Skill "${skillId}" not found` },
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
// ─── List ─────────────────────────────────────────────────────────
|
|
175
|
+
builder.on("action:listSkills", async function* (event) {
|
|
176
|
+
const { toolCallId } = event.data;
|
|
177
|
+
const skillsList = await skills.list();
|
|
178
|
+
yield {
|
|
179
|
+
type: "action:taskResult",
|
|
180
|
+
data: {
|
|
181
|
+
action: "listSkills",
|
|
182
|
+
toolCallId,
|
|
183
|
+
result: { skills: skillsList },
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
});
|
|
187
|
+
// ─── Create ───────────────────────────────────────────────────────
|
|
188
|
+
builder.on("action:createSkill", async function* (event) {
|
|
189
|
+
const { id, title, description, content, toolCallId } = event.data;
|
|
190
|
+
try {
|
|
191
|
+
const skillPath = await skills.create(id, title, description, content);
|
|
192
|
+
yield {
|
|
193
|
+
type: "skills:status",
|
|
194
|
+
data: { message: `Skill "${title}" created`, severity: "success" },
|
|
195
|
+
};
|
|
196
|
+
yield {
|
|
197
|
+
type: "action:taskResult",
|
|
198
|
+
data: {
|
|
199
|
+
action: "createSkill",
|
|
200
|
+
toolCallId,
|
|
201
|
+
result: {
|
|
202
|
+
success: true,
|
|
203
|
+
path: skillPath,
|
|
204
|
+
message: `Skill "${title}" created successfully`,
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
yield {
|
|
211
|
+
type: "skills:status",
|
|
212
|
+
data: {
|
|
213
|
+
message: `Failed to create skill: ${error.message}`,
|
|
214
|
+
severity: "error",
|
|
215
|
+
},
|
|
216
|
+
};
|
|
217
|
+
yield {
|
|
218
|
+
type: "action:taskResult",
|
|
219
|
+
data: {
|
|
220
|
+
action: "createSkill",
|
|
221
|
+
toolCallId,
|
|
222
|
+
result: { error: error.message },
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
// ─── Update ───────────────────────────────────────────────────────
|
|
228
|
+
builder.on("action:updateSkill", async function* (event) {
|
|
229
|
+
const { id, title, description, content, toolCallId } = event.data;
|
|
230
|
+
try {
|
|
231
|
+
const version = await skills.update(id, content, title, description);
|
|
232
|
+
yield {
|
|
233
|
+
type: "skills:status",
|
|
234
|
+
data: {
|
|
235
|
+
message: `Skill updated to v${version}`,
|
|
236
|
+
severity: "success",
|
|
237
|
+
},
|
|
238
|
+
};
|
|
239
|
+
yield {
|
|
240
|
+
type: "action:taskResult",
|
|
241
|
+
data: {
|
|
242
|
+
action: "updateSkill",
|
|
243
|
+
toolCallId,
|
|
244
|
+
result: {
|
|
245
|
+
success: true,
|
|
246
|
+
path: `skills/${id}/SKILL.md`,
|
|
247
|
+
version,
|
|
248
|
+
message: `Skill updated to version ${version}`,
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
catch (error) {
|
|
254
|
+
const errorMsg = error.code === "ENOENT"
|
|
255
|
+
? `Skill "${id}" does not exist`
|
|
256
|
+
: error.message;
|
|
257
|
+
yield {
|
|
258
|
+
type: "skills:status",
|
|
259
|
+
data: {
|
|
260
|
+
message: `Failed to update skill: ${errorMsg}`,
|
|
261
|
+
severity: "error",
|
|
262
|
+
},
|
|
263
|
+
};
|
|
264
|
+
yield {
|
|
265
|
+
type: "action:taskResult",
|
|
266
|
+
data: {
|
|
267
|
+
action: "updateSkill",
|
|
268
|
+
toolCallId,
|
|
269
|
+
result: { error: errorMsg },
|
|
270
|
+
},
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
};
|
|
275
|
+
export default skillsPlugin;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
// --- Tool Definitions ---
|
|
3
|
+
export const skillsToolDefinitions = {
|
|
4
|
+
loadSkill: {
|
|
5
|
+
description: "Load a skill's full instructions when you need to use it. Call this before executing a skill.",
|
|
6
|
+
inputSchema: z.object({
|
|
7
|
+
skillId: z
|
|
8
|
+
.string()
|
|
9
|
+
.describe("The skill folder name (e.g., 'code-review')"),
|
|
10
|
+
}),
|
|
11
|
+
},
|
|
12
|
+
createSkill: {
|
|
13
|
+
description: "Create a new skill from learned knowledge. Use when you discover a reusable pattern.",
|
|
14
|
+
inputSchema: z.object({
|
|
15
|
+
id: z
|
|
16
|
+
.string()
|
|
17
|
+
.describe("Skill folder name in kebab-case (e.g., 'web-search')"),
|
|
18
|
+
title: z.string().describe("Human-readable skill title"),
|
|
19
|
+
description: z
|
|
20
|
+
.string()
|
|
21
|
+
.describe("Brief description of what the skill does"),
|
|
22
|
+
content: z
|
|
23
|
+
.string()
|
|
24
|
+
.describe("Full skill instructions in markdown"),
|
|
25
|
+
}),
|
|
26
|
+
},
|
|
27
|
+
updateSkill: {
|
|
28
|
+
description: "Update an existing skill with new knowledge or improvements.",
|
|
29
|
+
inputSchema: z.object({
|
|
30
|
+
id: z
|
|
31
|
+
.string()
|
|
32
|
+
.describe("The skill folder name (e.g., 'code-review')"),
|
|
33
|
+
title: z
|
|
34
|
+
.string()
|
|
35
|
+
.optional()
|
|
36
|
+
.describe("New title for the skill"),
|
|
37
|
+
description: z
|
|
38
|
+
.string()
|
|
39
|
+
.optional()
|
|
40
|
+
.describe("New description for the skill"),
|
|
41
|
+
content: z
|
|
42
|
+
.string()
|
|
43
|
+
.describe("Updated full skill instructions in markdown"),
|
|
44
|
+
}),
|
|
45
|
+
},
|
|
46
|
+
listSkills: {
|
|
47
|
+
description: "List all available skills with their metadata.",
|
|
48
|
+
inputSchema: z.object({}),
|
|
49
|
+
},
|
|
50
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { ui } from "@melony/ui-kit/server";
|
|
2
|
+
// --- UI Plugin ---
|
|
3
|
+
export const skillsUIPlugin = () => (builder) => {
|
|
4
|
+
builder.on("skills:status", async function* (event) {
|
|
5
|
+
yield ui.event(ui.status(event.data.message, event.data.severity));
|
|
6
|
+
});
|
|
7
|
+
builder.on("skills:loaded", async function* (event) {
|
|
8
|
+
yield ui.event(ui.resourceCard(event.data.title, "", [
|
|
9
|
+
ui.text(event.data.instructions),
|
|
10
|
+
]));
|
|
11
|
+
});
|
|
12
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Registry
|
|
3
|
+
*
|
|
4
|
+
* Collects all available agents (built-in + discovered).
|
|
5
|
+
* Used by the manager to dynamically build the delegation tool schema
|
|
6
|
+
* and wire up generic bridge-back handlers.
|
|
7
|
+
*/
|
|
8
|
+
export class AgentRegistry {
|
|
9
|
+
constructor() {
|
|
10
|
+
this.agents = new Map();
|
|
11
|
+
}
|
|
12
|
+
register(entry) {
|
|
13
|
+
if (this.agents.has(entry.name)) {
|
|
14
|
+
console.warn(`Agent "${entry.name}" is already registered — overwriting`);
|
|
15
|
+
}
|
|
16
|
+
this.agents.set(entry.name, entry);
|
|
17
|
+
}
|
|
18
|
+
get(name) {
|
|
19
|
+
return this.agents.get(name);
|
|
20
|
+
}
|
|
21
|
+
has(name) {
|
|
22
|
+
return this.agents.has(name);
|
|
23
|
+
}
|
|
24
|
+
getAll() {
|
|
25
|
+
return Array.from(this.agents.values());
|
|
26
|
+
}
|
|
27
|
+
/** Returns agent names as a tuple suitable for z.enum() */
|
|
28
|
+
getNames() {
|
|
29
|
+
const names = Array.from(this.agents.keys());
|
|
30
|
+
if (names.length === 0) {
|
|
31
|
+
throw new Error("No agents registered — at least one agent is required");
|
|
32
|
+
}
|
|
33
|
+
return names;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin Registry
|
|
3
|
+
*
|
|
4
|
+
* Maps plugin names to their factories and tool definitions.
|
|
5
|
+
* Built-in plugins are registered at startup; community plugins
|
|
6
|
+
* can be added via npm packages or local directories (future).
|
|
7
|
+
*/
|
|
8
|
+
export class PluginRegistry {
|
|
9
|
+
constructor() {
|
|
10
|
+
this.plugins = new Map();
|
|
11
|
+
}
|
|
12
|
+
register(entry) {
|
|
13
|
+
this.plugins.set(entry.name, entry);
|
|
14
|
+
}
|
|
15
|
+
get(name) {
|
|
16
|
+
return this.plugins.get(name);
|
|
17
|
+
}
|
|
18
|
+
has(name) {
|
|
19
|
+
return this.plugins.has(name);
|
|
20
|
+
}
|
|
21
|
+
getAll() {
|
|
22
|
+
return Array.from(this.plugins.values());
|
|
23
|
+
}
|
|
24
|
+
getNames() {
|
|
25
|
+
return Array.from(this.plugins.keys());
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import yaml from "js-yaml";
|
|
4
|
+
import { llmPlugin } from "../plugins/llm/index.js";
|
|
5
|
+
import { createModel } from "../models.js";
|
|
6
|
+
/**
|
|
7
|
+
* Discover and load YAML-defined agents from a directory.
|
|
8
|
+
*
|
|
9
|
+
* Scans `agentsDir` for subdirectories containing an `agent.yaml` file,
|
|
10
|
+
* parses each one, and composes a Melony plugin from the referenced plugins.
|
|
11
|
+
*
|
|
12
|
+
* @param agentsDir Absolute path to the agents directory (e.g. ~/.openbot/agents)
|
|
13
|
+
* @param pluginRegistry Registry of available plugins
|
|
14
|
+
* @param defaultModel Language model to use for agent LLMs if not specified in YAML
|
|
15
|
+
* @param options Optional API keys for creating specific models
|
|
16
|
+
* @returns Array of discovered agent entries ready for registration
|
|
17
|
+
*/
|
|
18
|
+
export async function discoverYamlAgents(agentsDir, pluginRegistry, defaultModel, options) {
|
|
19
|
+
const agents = [];
|
|
20
|
+
// Ensure the agents directory exists
|
|
21
|
+
try {
|
|
22
|
+
await fs.mkdir(agentsDir, { recursive: true });
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
// Best effort
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
const entries = await fs.readdir(agentsDir, { withFileTypes: true });
|
|
29
|
+
for (const entry of entries) {
|
|
30
|
+
if (!entry.isDirectory())
|
|
31
|
+
continue;
|
|
32
|
+
if (entry.name.startsWith(".") || entry.name.startsWith("_"))
|
|
33
|
+
continue;
|
|
34
|
+
const yamlPath = path.join(agentsDir, entry.name, "agent.yaml");
|
|
35
|
+
try {
|
|
36
|
+
const content = await fs.readFile(yamlPath, "utf-8");
|
|
37
|
+
const config = yaml.load(content);
|
|
38
|
+
// Validate required fields
|
|
39
|
+
if (!config.name || !config.description || !config.plugins?.length || !config.systemPrompt) {
|
|
40
|
+
console.warn(`[agents] "${entry.name}/agent.yaml": missing required fields (name, description, plugins, systemPrompt) — skipping`);
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
// Use agent-specific model if defined, otherwise use default
|
|
44
|
+
const agentModel = config.model
|
|
45
|
+
? createModel({ ...options, model: config.model })
|
|
46
|
+
: defaultModel;
|
|
47
|
+
const plugin = composeAgentFromYaml(config, pluginRegistry, agentModel);
|
|
48
|
+
agents.push({
|
|
49
|
+
name: config.name,
|
|
50
|
+
description: config.description,
|
|
51
|
+
plugin,
|
|
52
|
+
});
|
|
53
|
+
console.log(`[agents] Loaded: ${config.name} — ${config.description}${config.model ? ` (model: ${config.model})` : ""}`);
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
// Invalid or missing agent.yaml — silently skip
|
|
57
|
+
console.warn(`[agents] Error loading "${entry.name}/agent.yaml":`, err);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// Agents directory doesn't exist or can't be read — that's fine
|
|
63
|
+
}
|
|
64
|
+
return agents;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Compose a Melony plugin from a YAML agent configuration.
|
|
68
|
+
*
|
|
69
|
+
* Resolves each plugin name against the registry, collects their tool definitions,
|
|
70
|
+
* and wires them with an agent-scoped LLM plugin.
|
|
71
|
+
*/
|
|
72
|
+
function composeAgentFromYaml(config, pluginRegistry, model) {
|
|
73
|
+
return (builder) => {
|
|
74
|
+
const allToolDefinitions = {};
|
|
75
|
+
for (const pluginName of config.plugins) {
|
|
76
|
+
const entry = pluginRegistry.get(pluginName);
|
|
77
|
+
if (!entry) {
|
|
78
|
+
console.warn(`[agents] "${config.name}": plugin "${pluginName}" not found in registry — skipping`);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
// Register the plugin's event handlers
|
|
82
|
+
builder.use(entry.factory());
|
|
83
|
+
// Register UI plugin if available
|
|
84
|
+
if (entry.uiFactory) {
|
|
85
|
+
builder.use(entry.uiFactory());
|
|
86
|
+
}
|
|
87
|
+
// Collect tool definitions for the LLM
|
|
88
|
+
Object.assign(allToolDefinitions, entry.toolDefinitions);
|
|
89
|
+
}
|
|
90
|
+
// Wire up the LLM with agent-scoped event channels
|
|
91
|
+
builder.use(llmPlugin({
|
|
92
|
+
model,
|
|
93
|
+
system: config.systemPrompt,
|
|
94
|
+
toolDefinitions: allToolDefinitions,
|
|
95
|
+
promptInputType: `agent:${config.name}:input`,
|
|
96
|
+
actionResultInputType: `agent:${config.name}:result`,
|
|
97
|
+
completionEventType: `agent:${config.name}:output`,
|
|
98
|
+
}));
|
|
99
|
+
};
|
|
100
|
+
}
|
package/dist/server.js
CHANGED
|
@@ -1,25 +1,16 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
1
|
import "dotenv/config";
|
|
3
2
|
import express from "express";
|
|
4
3
|
import cors from "cors";
|
|
5
|
-
import { Command } from "commander";
|
|
6
4
|
import { generateId } from "melony";
|
|
7
|
-
import { createOpenBot } from "./
|
|
5
|
+
import { createOpenBot } from "./open-bot.js";
|
|
8
6
|
import { loadConfig } from "./config.js";
|
|
9
7
|
import { loadSession, saveSession, logEvent, loadEvents } from "./session.js";
|
|
10
|
-
|
|
11
|
-
program
|
|
12
|
-
.name("openbot-server")
|
|
13
|
-
.description("OpenBot server")
|
|
14
|
-
.option("-p, --port <number>", "Port to listen on")
|
|
15
|
-
.option("--openai-api-key <key>", "OpenAI API Key")
|
|
16
|
-
.option("--anthropic-api-key <key>", "Anthropic API Key")
|
|
17
|
-
.action(async (options) => {
|
|
8
|
+
export async function startServer(options = {}) {
|
|
18
9
|
const config = loadConfig();
|
|
19
10
|
const PORT = Number(options.port ?? config.port ?? process.env.PORT ?? 4001);
|
|
20
11
|
const app = express();
|
|
21
|
-
// Initialize the
|
|
22
|
-
const
|
|
12
|
+
// Initialize the agent instance once at startup
|
|
13
|
+
const openBotAgent = await createOpenBot({
|
|
23
14
|
openaiApiKey: options.openaiApiKey,
|
|
24
15
|
anthropicApiKey: options.anthropicApiKey,
|
|
25
16
|
});
|
|
@@ -32,24 +23,32 @@ program
|
|
|
32
23
|
stream: "Server-Sent Events (SSE)",
|
|
33
24
|
});
|
|
34
25
|
});
|
|
35
|
-
//
|
|
36
|
-
app.get("/api/
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
const
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
26
|
+
// View endpoint (GET version of chat, returns JSON instead of SSE)
|
|
27
|
+
app.get("/api/view", async (req, res) => {
|
|
28
|
+
const { type = "init", data: dataStr } = req.query;
|
|
29
|
+
// Parse the structured data
|
|
30
|
+
const eventData = dataStr ? JSON.parse(dataStr) : {};
|
|
31
|
+
const { sessionId = "default", history = false } = eventData;
|
|
32
|
+
const state = (await loadSession(sessionId)) ?? {};
|
|
33
|
+
state.sessionId = sessionId;
|
|
34
|
+
const response = await openBotAgent.jsonResponse({
|
|
35
|
+
type: type,
|
|
36
|
+
data: eventData,
|
|
44
37
|
}, {
|
|
45
|
-
state
|
|
46
|
-
runId: generateId()
|
|
38
|
+
state,
|
|
39
|
+
runId: generateId(),
|
|
47
40
|
});
|
|
48
41
|
const result = await response.json();
|
|
49
|
-
const
|
|
42
|
+
const uiEvents = result.events.filter((event) => event.type === "ui");
|
|
43
|
+
const layout = uiEvents.find((e) => e.meta?.type === "layout")?.data;
|
|
44
|
+
const sidebar = uiEvents.find((e) => e.meta?.type === "sidebar")?.data;
|
|
45
|
+
const content = uiEvents.find((e) => e.meta?.type === "content")?.data;
|
|
46
|
+
const initialEvents = history ? await loadEvents(sessionId) : undefined;
|
|
50
47
|
res.json({
|
|
51
|
-
|
|
52
|
-
|
|
48
|
+
ui: layout,
|
|
49
|
+
sidebar,
|
|
50
|
+
content,
|
|
51
|
+
initialEvents,
|
|
53
52
|
});
|
|
54
53
|
});
|
|
55
54
|
// Chat endpoint
|
|
@@ -66,10 +65,11 @@ program
|
|
|
66
65
|
Connection: "keep-alive",
|
|
67
66
|
});
|
|
68
67
|
res.flushHeaders?.();
|
|
69
|
-
const runtime =
|
|
68
|
+
const runtime = openBotAgent.build();
|
|
70
69
|
const sessionId = body.sessionId ?? "default";
|
|
71
70
|
const runId = body.runId ?? `run_${generateId()}`;
|
|
72
|
-
const state = await loadSession(sessionId) ?? {};
|
|
71
|
+
const state = (await loadSession(sessionId)) ?? {};
|
|
72
|
+
state.sessionId = sessionId;
|
|
73
73
|
// Log the incoming event
|
|
74
74
|
await logEvent(sessionId, runId, body.event);
|
|
75
75
|
const iterator = runtime.run(body.event, {
|
|
@@ -116,5 +116,4 @@ program
|
|
|
116
116
|
if (options.anthropicApiKey)
|
|
117
117
|
console.log(" - Using Anthropic API Key from CLI");
|
|
118
118
|
});
|
|
119
|
-
}
|
|
120
|
-
program.parse();
|
|
119
|
+
}
|
package/dist/ui/header.js
CHANGED
|
@@ -6,15 +6,12 @@ export const headerUI = (tab) => ui.box({
|
|
|
6
6
|
}, [
|
|
7
7
|
ui.row({ justify: "between", align: "center", width: "full" }, [
|
|
8
8
|
ui.row({ gap: "xs", align: "center" }, [
|
|
9
|
-
ui.
|
|
10
|
-
label: "OpenBot",
|
|
11
|
-
variant: "ghost",
|
|
12
|
-
size: "sm",
|
|
9
|
+
ui.listItem({
|
|
13
10
|
onClickAction: {
|
|
14
11
|
type: "client:navigate",
|
|
15
|
-
data: { path: "/" }
|
|
16
|
-
}
|
|
17
|
-
}),
|
|
12
|
+
data: { path: "/" }
|
|
13
|
+
}
|
|
14
|
+
}, [ui.text("OpenBot")]),
|
|
18
15
|
ui.divider({ orientation: "vertical", margin: "xs" }),
|
|
19
16
|
ui.button({
|
|
20
17
|
label: "Chat",
|
package/dist/ui/layout.js
CHANGED
|
@@ -9,13 +9,6 @@ const tabs = {
|
|
|
9
9
|
settings: settingsUI,
|
|
10
10
|
skills: skillsUI,
|
|
11
11
|
};
|
|
12
|
-
// export const layoutUI = (tab: string) =>
|
|
13
|
-
// ui.col({ height: "full", width: "full", gap: "none" }, [
|
|
14
|
-
// headerUI(tab),
|
|
15
|
-
// ui.box({ flex: 1, width: "full", overflow: "auto" }, [
|
|
16
|
-
// tabs[tab as keyof typeof tabs],
|
|
17
|
-
// ]),
|
|
18
|
-
// ]);
|
|
19
12
|
export const layoutUI = async ({ tab, sessionId }) => {
|
|
20
13
|
const sessions = await listSessions();
|
|
21
14
|
return ui.row({ height: "full" }, [
|
|
@@ -23,3 +16,10 @@ export const layoutUI = async ({ tab, sessionId }) => {
|
|
|
23
16
|
tabs[tab]
|
|
24
17
|
]);
|
|
25
18
|
};
|
|
19
|
+
export const sidebarOnlyUI = async ({ sessionId }) => {
|
|
20
|
+
const sessions = await listSessions();
|
|
21
|
+
return sidebarUI({ sessions, sessionId });
|
|
22
|
+
};
|
|
23
|
+
export const tabOnlyUI = async ({ tab }) => {
|
|
24
|
+
return tabs[tab];
|
|
25
|
+
};
|
package/dist/ui/navigation.js
CHANGED
|
@@ -8,6 +8,8 @@ const listItemProps = (path) => ({
|
|
|
8
8
|
export const navigationUI = ui.box({ width: "full" }, [ui.list({
|
|
9
9
|
gap: "none",
|
|
10
10
|
}, [
|
|
11
|
-
ui.listItem(listItemProps("/"), [
|
|
12
|
-
|
|
11
|
+
ui.listItem(listItemProps("/"), [
|
|
12
|
+
ui.icon("PlusIcon", { size: "sm" }),
|
|
13
|
+
ui.text("New chat", { size: "sm" })
|
|
14
|
+
])
|
|
13
15
|
])]);
|