ideaco 1.1.5
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/.dockerignore +33 -0
- package/.nvmrc +1 -0
- package/ARCHITECTURE.md +394 -0
- package/Dockerfile +50 -0
- package/LICENSE +29 -0
- package/README.md +206 -0
- package/bin/i18n.js +46 -0
- package/bin/ideaco.js +494 -0
- package/deploy.sh +15 -0
- package/docker-compose.yml +30 -0
- package/electron/main.cjs +986 -0
- package/electron/preload.cjs +14 -0
- package/electron/web-backends.cjs +854 -0
- package/jsconfig.json +8 -0
- package/next.config.mjs +34 -0
- package/package.json +134 -0
- package/postcss.config.mjs +6 -0
- package/public/demo/dashboard.png +0 -0
- package/public/demo/employee.png +0 -0
- package/public/demo/messages.png +0 -0
- package/public/demo/office.png +0 -0
- package/public/demo/requirement.png +0 -0
- package/public/logo.jpeg +0 -0
- package/public/logo.png +0 -0
- package/scripts/prepare-electron.js +67 -0
- package/scripts/release.js +76 -0
- package/src/app/api/agents/[agentId]/chat/route.js +70 -0
- package/src/app/api/agents/[agentId]/conversations/route.js +35 -0
- package/src/app/api/agents/[agentId]/route.js +106 -0
- package/src/app/api/avatar/route.js +104 -0
- package/src/app/api/browse-dir/route.js +44 -0
- package/src/app/api/chat/route.js +265 -0
- package/src/app/api/company/factory-reset/route.js +43 -0
- package/src/app/api/company/route.js +82 -0
- package/src/app/api/departments/[deptId]/agents/[agentId]/dismiss/route.js +19 -0
- package/src/app/api/departments/route.js +92 -0
- package/src/app/api/group-chat-loop/events/route.js +70 -0
- package/src/app/api/group-chat-loop/route.js +94 -0
- package/src/app/api/mailbox/route.js +100 -0
- package/src/app/api/messages/route.js +14 -0
- package/src/app/api/providers/[id]/configure/route.js +21 -0
- package/src/app/api/providers/[id]/refresh-cookie/route.js +38 -0
- package/src/app/api/providers/[id]/test-cookie/route.js +28 -0
- package/src/app/api/providers/route.js +11 -0
- package/src/app/api/requirements/route.js +242 -0
- package/src/app/api/secretary/route.js +65 -0
- package/src/app/api/system/cli-backends/route.js +91 -0
- package/src/app/api/system/cron/route.js +110 -0
- package/src/app/api/system/knowledge/route.js +104 -0
- package/src/app/api/system/plugins/route.js +40 -0
- package/src/app/api/system/skills/route.js +46 -0
- package/src/app/api/system/status/route.js +46 -0
- package/src/app/api/talent-market/[profileId]/recall/route.js +22 -0
- package/src/app/api/talent-market/[profileId]/route.js +17 -0
- package/src/app/api/talent-market/route.js +26 -0
- package/src/app/api/teams/route.js +773 -0
- package/src/app/api/ws-files/[departmentId]/file/route.js +27 -0
- package/src/app/api/ws-files/[departmentId]/files/route.js +22 -0
- package/src/app/globals.css +130 -0
- package/src/app/layout.jsx +40 -0
- package/src/app/page.jsx +97 -0
- package/src/components/AgentChatModal.jsx +164 -0
- package/src/components/AgentDetailModal.jsx +425 -0
- package/src/components/AgentSpyModal.jsx +481 -0
- package/src/components/AvatarGrid.jsx +29 -0
- package/src/components/BossProfileModal.jsx +162 -0
- package/src/components/CachedAvatar.jsx +77 -0
- package/src/components/ChatPanel.jsx +219 -0
- package/src/components/ChatShared.jsx +255 -0
- package/src/components/DepartmentDetail.jsx +842 -0
- package/src/components/DepartmentView.jsx +367 -0
- package/src/components/FileReference.jsx +260 -0
- package/src/components/FilesView.jsx +465 -0
- package/src/components/GroupChatView.jsx +799 -0
- package/src/components/Mailbox.jsx +926 -0
- package/src/components/MessagesView.jsx +112 -0
- package/src/components/OnboardingGuide.jsx +209 -0
- package/src/components/OrgTree.jsx +151 -0
- package/src/components/Overview.jsx +391 -0
- package/src/components/PixelOffice.jsx +2281 -0
- package/src/components/ProviderGrid.jsx +551 -0
- package/src/components/ProvidersBoard.jsx +16 -0
- package/src/components/RequirementDetail.jsx +1279 -0
- package/src/components/RequirementsBoard.jsx +187 -0
- package/src/components/SecretarySettings.jsx +295 -0
- package/src/components/SetupWizard.jsx +388 -0
- package/src/components/Sidebar.jsx +169 -0
- package/src/components/SystemMonitor.jsx +808 -0
- package/src/components/TalentMarket.jsx +183 -0
- package/src/components/TeamDetail.jsx +697 -0
- package/src/core/agent/base-agent.js +104 -0
- package/src/core/agent/chat-store.js +602 -0
- package/src/core/agent/cli-agent/backends/claude-code/README.md +52 -0
- package/src/core/agent/cli-agent/backends/claude-code/config.js +27 -0
- package/src/core/agent/cli-agent/backends/codebuddy/README.md +236 -0
- package/src/core/agent/cli-agent/backends/codebuddy/config.js +27 -0
- package/src/core/agent/cli-agent/backends/codex/README.md +51 -0
- package/src/core/agent/cli-agent/backends/codex/config.js +27 -0
- package/src/core/agent/cli-agent/backends/index.js +27 -0
- package/src/core/agent/cli-agent/backends/registry.js +580 -0
- package/src/core/agent/cli-agent/index.js +154 -0
- package/src/core/agent/index.js +60 -0
- package/src/core/agent/llm-agent/client.js +320 -0
- package/src/core/agent/llm-agent/index.js +97 -0
- package/src/core/agent/message-bus.js +211 -0
- package/src/core/agent/session.js +608 -0
- package/src/core/agent/tools.js +596 -0
- package/src/core/agent/web-agent/backends/base-backend.js +180 -0
- package/src/core/agent/web-agent/backends/chatgpt/client.js +146 -0
- package/src/core/agent/web-agent/backends/chatgpt/config.js +148 -0
- package/src/core/agent/web-agent/backends/chatgpt/dom-scripts.js +303 -0
- package/src/core/agent/web-agent/backends/index.js +91 -0
- package/src/core/agent/web-agent/index.js +278 -0
- package/src/core/agent/web-agent/web-client.js +407 -0
- package/src/core/employee/base-employee.js +1088 -0
- package/src/core/employee/index.js +35 -0
- package/src/core/employee/knowledge.js +327 -0
- package/src/core/employee/lifecycle.js +990 -0
- package/src/core/employee/memory/index.js +642 -0
- package/src/core/employee/memory/store.js +143 -0
- package/src/core/employee/performance.js +224 -0
- package/src/core/employee/secretary.js +625 -0
- package/src/core/employee/skills.js +398 -0
- package/src/core/index.js +38 -0
- package/src/core/organization/company.js +2600 -0
- package/src/core/organization/department.js +737 -0
- package/src/core/organization/group-chat-loop.js +264 -0
- package/src/core/organization/index.js +8 -0
- package/src/core/organization/persistence.js +111 -0
- package/src/core/organization/team.js +267 -0
- package/src/core/organization/workforce/hr.js +377 -0
- package/src/core/organization/workforce/providers.js +468 -0
- package/src/core/organization/workforce/role-archetypes.js +805 -0
- package/src/core/organization/workforce/talent-market.js +205 -0
- package/src/core/prompts.js +532 -0
- package/src/core/requirement.js +1789 -0
- package/src/core/system/audit.js +483 -0
- package/src/core/system/cron.js +449 -0
- package/src/core/system/index.js +7 -0
- package/src/core/system/plugin.js +2183 -0
- package/src/core/utils/json-parse.js +188 -0
- package/src/core/workspace.js +239 -0
- package/src/lib/api-i18n.js +211 -0
- package/src/lib/avatar.js +268 -0
- package/src/lib/client-store.js +1025 -0
- package/src/lib/config-validator.js +483 -0
- package/src/lib/format-time.js +22 -0
- package/src/lib/hooks.js +414 -0
- package/src/lib/i18n.js +134 -0
- package/src/lib/paths.js +23 -0
- package/src/lib/store.js +72 -0
- package/src/locales/de.js +393 -0
- package/src/locales/en.js +1054 -0
- package/src/locales/es.js +393 -0
- package/src/locales/fr.js +393 -0
- package/src/locales/ja.js +501 -0
- package/src/locales/ko.js +513 -0
- package/src/locales/zh.js +828 -0
- package/tailwind.config.mjs +11 -0
|
@@ -0,0 +1,2183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin System - Hot-pluggable skill extension framework for agents
|
|
3
|
+
*
|
|
4
|
+
* Distilled from OpenClaw's plugin system (vendor/openclaw/src/plugins/)
|
|
5
|
+
* Re-implemented as an "employee training / skill certification" system
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Plugin discovery and registration
|
|
9
|
+
* - Lifecycle hooks (install, enable, disable, uninstall)
|
|
10
|
+
* - Plugin-provided tools that agents can use
|
|
11
|
+
* - Plugin configuration schema validation
|
|
12
|
+
* - Event hooks (before/after tool call, message received, etc.)
|
|
13
|
+
*/
|
|
14
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
15
|
+
import { exec as cpExec } from 'child_process';
|
|
16
|
+
import { promisify } from 'util';
|
|
17
|
+
import { createRequire } from 'module';
|
|
18
|
+
import fs from 'fs/promises';
|
|
19
|
+
import { existsSync, mkdirSync } from 'fs';
|
|
20
|
+
import path from 'path';
|
|
21
|
+
|
|
22
|
+
const _require = createRequire(import.meta.url);
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Safely load optional dependencies (puppeteer, pdf-parse, openai, etc.)
|
|
26
|
+
* These packages are marked as external in next.config.mjs so webpack won't try to bundle them
|
|
27
|
+
*/
|
|
28
|
+
function tryRequire(moduleName) {
|
|
29
|
+
try {
|
|
30
|
+
return _require(moduleName);
|
|
31
|
+
} catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const execAsync = promisify(cpExec);
|
|
37
|
+
|
|
38
|
+
// Runtime references (lazily fetched to avoid circular dependencies)
|
|
39
|
+
let _sessionManager = null;
|
|
40
|
+
let _cronScheduler = null;
|
|
41
|
+
let _knowledgeManager = null;
|
|
42
|
+
let _llmClient = null;
|
|
43
|
+
let _messageBus = null;
|
|
44
|
+
|
|
45
|
+
/** Initialize runtime references (called externally to avoid circular imports) */
|
|
46
|
+
export function initPluginRuntime({ sessionManager, cronScheduler, knowledgeManager, llmClient, messageBus } = {}) {
|
|
47
|
+
if (sessionManager) _sessionManager = sessionManager;
|
|
48
|
+
if (cronScheduler) _cronScheduler = cronScheduler;
|
|
49
|
+
if (knowledgeManager) _knowledgeManager = knowledgeManager;
|
|
50
|
+
if (llmClient) _llmClient = llmClient;
|
|
51
|
+
if (messageBus) _messageBus = messageBus;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
import { WORKSPACE_DIR, DATA_DIR } from '../../lib/paths.js';
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Plugin lifecycle states
|
|
58
|
+
*/
|
|
59
|
+
export const PluginState = {
|
|
60
|
+
DISCOVERED: 'discovered',
|
|
61
|
+
INSTALLED: 'installed',
|
|
62
|
+
ENABLED: 'enabled',
|
|
63
|
+
DISABLED: 'disabled',
|
|
64
|
+
ERROR: 'error',
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Hook points where plugins can inject behavior
|
|
69
|
+
*/
|
|
70
|
+
export const HookPoint = {
|
|
71
|
+
BEFORE_TOOL_CALL: 'before_tool_call',
|
|
72
|
+
AFTER_TOOL_CALL: 'after_tool_call',
|
|
73
|
+
BEFORE_LLM_CALL: 'before_llm_call',
|
|
74
|
+
AFTER_LLM_CALL: 'after_llm_call',
|
|
75
|
+
MESSAGE_RECEIVED: 'message_received',
|
|
76
|
+
MESSAGE_SENT: 'message_sent',
|
|
77
|
+
AGENT_TASK_START: 'agent_task_start',
|
|
78
|
+
AGENT_TASK_END: 'agent_task_end',
|
|
79
|
+
REQUIREMENT_CREATED: 'requirement_created',
|
|
80
|
+
REQUIREMENT_COMPLETED: 'requirement_completed',
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Plugin manifest definition
|
|
85
|
+
* Each plugin must provide this metadata
|
|
86
|
+
*/
|
|
87
|
+
export class PluginManifest {
|
|
88
|
+
/**
|
|
89
|
+
* @param {object} config
|
|
90
|
+
* @param {string} config.id - Unique plugin identifier
|
|
91
|
+
* @param {string} config.name - Display name
|
|
92
|
+
* @param {string} config.version - Semver version string
|
|
93
|
+
* @param {string} config.description - What this plugin does
|
|
94
|
+
* @param {string} config.author - Plugin author
|
|
95
|
+
* @param {Array} config.tools - Tool definitions this plugin provides
|
|
96
|
+
* @param {object} config.hooks - Hook handlers { [HookPoint]: Function }
|
|
97
|
+
* @param {object} config.configSchema - JSON schema for plugin configuration
|
|
98
|
+
* @param {Array} config.requiredProviders - Provider IDs this plugin needs
|
|
99
|
+
*/
|
|
100
|
+
constructor(config) {
|
|
101
|
+
this.id = config.id;
|
|
102
|
+
this.name = config.name;
|
|
103
|
+
this.version = config.version || '1.0.0';
|
|
104
|
+
this.description = config.description || '';
|
|
105
|
+
this.author = config.author || 'Unknown';
|
|
106
|
+
this.tools = config.tools || [];
|
|
107
|
+
this.hooks = config.hooks || {};
|
|
108
|
+
this.configSchema = config.configSchema || {};
|
|
109
|
+
this.requiredProviders = config.requiredProviders || [];
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Plugin instance - A registered and managed plugin
|
|
115
|
+
*/
|
|
116
|
+
class PluginInstance {
|
|
117
|
+
constructor(manifest) {
|
|
118
|
+
this.manifest = manifest;
|
|
119
|
+
this.state = PluginState.INSTALLED;
|
|
120
|
+
this.config = {};
|
|
121
|
+
this.error = null;
|
|
122
|
+
this.installedAt = new Date();
|
|
123
|
+
this.enabledAt = null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Plugin Registry - Manages all installed plugins
|
|
129
|
+
*/
|
|
130
|
+
export class PluginRegistry {
|
|
131
|
+
constructor() {
|
|
132
|
+
// Registered plugins: Map<pluginId, PluginInstance>
|
|
133
|
+
this.plugins = new Map();
|
|
134
|
+
|
|
135
|
+
// Hook subscriptions: Map<HookPoint, Array<{pluginId, handler}>>
|
|
136
|
+
this.hookSubscriptions = new Map();
|
|
137
|
+
Object.values(HookPoint).forEach(hp => this.hookSubscriptions.set(hp, []));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Install a plugin from its manifest
|
|
142
|
+
* @param {PluginManifest} manifest
|
|
143
|
+
* @param {object} config - Initial plugin configuration
|
|
144
|
+
* @returns {PluginInstance}
|
|
145
|
+
*/
|
|
146
|
+
install(manifest, config = {}) {
|
|
147
|
+
if (this.plugins.has(manifest.id)) {
|
|
148
|
+
throw new Error(`Plugin "${manifest.id}" is already installed`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const instance = new PluginInstance(manifest);
|
|
152
|
+
instance.config = { ...config };
|
|
153
|
+
this.plugins.set(manifest.id, instance);
|
|
154
|
+
|
|
155
|
+
console.log(`🔌 Plugin installed: ${manifest.name} v${manifest.version}`);
|
|
156
|
+
return instance;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Enable a plugin (activate its hooks and tools)
|
|
161
|
+
* @param {string} pluginId
|
|
162
|
+
*/
|
|
163
|
+
enable(pluginId) {
|
|
164
|
+
const instance = this.plugins.get(pluginId);
|
|
165
|
+
if (!instance) throw new Error(`Plugin "${pluginId}" not found`);
|
|
166
|
+
|
|
167
|
+
if (instance.state === PluginState.ENABLED) return;
|
|
168
|
+
|
|
169
|
+
// Register hooks
|
|
170
|
+
for (const [hookPoint, handler] of Object.entries(instance.manifest.hooks)) {
|
|
171
|
+
if (this.hookSubscriptions.has(hookPoint)) {
|
|
172
|
+
this.hookSubscriptions.get(hookPoint).push({
|
|
173
|
+
pluginId,
|
|
174
|
+
handler,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
instance.state = PluginState.ENABLED;
|
|
180
|
+
instance.enabledAt = new Date();
|
|
181
|
+
console.log(`✅ Plugin enabled: ${instance.manifest.name}`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Disable a plugin (deactivate its hooks)
|
|
186
|
+
* @param {string} pluginId
|
|
187
|
+
*/
|
|
188
|
+
disable(pluginId) {
|
|
189
|
+
const instance = this.plugins.get(pluginId);
|
|
190
|
+
if (!instance) throw new Error(`Plugin "${pluginId}" not found`);
|
|
191
|
+
|
|
192
|
+
// Remove hooks
|
|
193
|
+
for (const [hookPoint, subscribers] of this.hookSubscriptions.entries()) {
|
|
194
|
+
this.hookSubscriptions.set(
|
|
195
|
+
hookPoint,
|
|
196
|
+
subscribers.filter(s => s.pluginId !== pluginId)
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
instance.state = PluginState.DISABLED;
|
|
201
|
+
console.log(`⏸️ Plugin disabled: ${instance.manifest.name}`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Uninstall a plugin
|
|
206
|
+
* @param {string} pluginId
|
|
207
|
+
*/
|
|
208
|
+
uninstall(pluginId) {
|
|
209
|
+
if (this.plugins.has(pluginId)) {
|
|
210
|
+
this.disable(pluginId);
|
|
211
|
+
this.plugins.delete(pluginId);
|
|
212
|
+
console.log(`🗑️ Plugin uninstalled: ${pluginId}`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Fire a hook point - call all subscribed handlers
|
|
218
|
+
* @param {string} hookPoint
|
|
219
|
+
* @param {object} context - Data passed to hook handlers
|
|
220
|
+
* @returns {Promise<Array>} Results from all handlers
|
|
221
|
+
*/
|
|
222
|
+
async fireHook(hookPoint, context = {}) {
|
|
223
|
+
const subscribers = this.hookSubscriptions.get(hookPoint) || [];
|
|
224
|
+
const results = [];
|
|
225
|
+
|
|
226
|
+
for (const { pluginId, handler } of subscribers) {
|
|
227
|
+
const instance = this.plugins.get(pluginId);
|
|
228
|
+
if (!instance || instance.state !== PluginState.ENABLED) continue;
|
|
229
|
+
|
|
230
|
+
try {
|
|
231
|
+
const result = await handler(context, instance.config);
|
|
232
|
+
results.push({ pluginId, result, error: null });
|
|
233
|
+
} catch (error) {
|
|
234
|
+
console.error(`[Plugin ${pluginId}] Hook error at ${hookPoint}:`, error.message);
|
|
235
|
+
results.push({ pluginId, result: null, error: error.message });
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return results;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Get all tool definitions from enabled plugins
|
|
244
|
+
* @returns {Array} OpenAI function-call compatible tool definitions
|
|
245
|
+
*/
|
|
246
|
+
getPluginTools() {
|
|
247
|
+
const tools = [];
|
|
248
|
+
for (const [pluginId, instance] of this.plugins) {
|
|
249
|
+
if (instance.state !== PluginState.ENABLED) continue;
|
|
250
|
+
for (const tool of instance.manifest.tools) {
|
|
251
|
+
tools.push({
|
|
252
|
+
...tool,
|
|
253
|
+
_pluginId: pluginId, // Track which plugin owns the tool
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return tools;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Execute a plugin-provided tool
|
|
262
|
+
* @param {string} toolName
|
|
263
|
+
* @param {object} args
|
|
264
|
+
* @returns {Promise<string>}
|
|
265
|
+
*/
|
|
266
|
+
async executePluginTool(toolName, args) {
|
|
267
|
+
for (const [pluginId, instance] of this.plugins) {
|
|
268
|
+
if (instance.state !== PluginState.ENABLED) continue;
|
|
269
|
+
|
|
270
|
+
const tool = instance.manifest.tools.find(t => t.function?.name === toolName);
|
|
271
|
+
if (tool && tool._executor) {
|
|
272
|
+
return await tool._executor(args, instance.config);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
throw new Error(`Plugin tool not found: ${toolName}`);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* List all plugins with their status
|
|
280
|
+
* @returns {Array}
|
|
281
|
+
*/
|
|
282
|
+
list() {
|
|
283
|
+
return [...this.plugins.values()].map(inst => ({
|
|
284
|
+
id: inst.manifest.id,
|
|
285
|
+
name: inst.manifest.name,
|
|
286
|
+
version: inst.manifest.version,
|
|
287
|
+
description: inst.manifest.description,
|
|
288
|
+
author: inst.manifest.author,
|
|
289
|
+
state: inst.state,
|
|
290
|
+
toolCount: inst.manifest.tools.length,
|
|
291
|
+
hookCount: Object.keys(inst.manifest.hooks).length,
|
|
292
|
+
installedAt: inst.installedAt,
|
|
293
|
+
enabledAt: inst.enabledAt,
|
|
294
|
+
error: inst.error,
|
|
295
|
+
}));
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Get plugin by ID
|
|
300
|
+
* @param {string} pluginId
|
|
301
|
+
* @returns {PluginInstance|null}
|
|
302
|
+
*/
|
|
303
|
+
get(pluginId) {
|
|
304
|
+
return this.plugins.get(pluginId) || null;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Configure a plugin
|
|
309
|
+
* @param {string} pluginId
|
|
310
|
+
* @param {object} config
|
|
311
|
+
*/
|
|
312
|
+
configure(pluginId, config) {
|
|
313
|
+
const instance = this.plugins.get(pluginId);
|
|
314
|
+
if (!instance) throw new Error(`Plugin "${pluginId}" not found`);
|
|
315
|
+
instance.config = { ...instance.config, ...config };
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// ====================================================================
|
|
320
|
+
// Built-in Plugins (pre-installed "company training programs")
|
|
321
|
+
// ====================================================================
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Web Search Plugin - Allow agents to search the web
|
|
325
|
+
*/
|
|
326
|
+
export const WebSearchPlugin = new PluginManifest({
|
|
327
|
+
id: 'builtin-web-search',
|
|
328
|
+
name: 'Web Search',
|
|
329
|
+
version: '1.0.0',
|
|
330
|
+
description: 'Enables agents to search the internet for information',
|
|
331
|
+
author: 'Idea Unlimited',
|
|
332
|
+
configSchema: {
|
|
333
|
+
apiKey: { type: 'string', description: 'Search API key' },
|
|
334
|
+
engine: { type: 'string', default: 'google', enum: ['google', 'bing', 'duckduckgo'] },
|
|
335
|
+
},
|
|
336
|
+
tools: [
|
|
337
|
+
{
|
|
338
|
+
type: 'function',
|
|
339
|
+
function: {
|
|
340
|
+
name: 'web_search',
|
|
341
|
+
description: 'Search the web for information. Returns top search results.',
|
|
342
|
+
parameters: {
|
|
343
|
+
type: 'object',
|
|
344
|
+
properties: {
|
|
345
|
+
query: { type: 'string', description: 'Search query' },
|
|
346
|
+
limit: { type: 'number', description: 'Max results to return (default: 5)' },
|
|
347
|
+
},
|
|
348
|
+
required: ['query'],
|
|
349
|
+
},
|
|
350
|
+
},
|
|
351
|
+
_executor: async (args, config) => {
|
|
352
|
+
const query = encodeURIComponent(args.query);
|
|
353
|
+
const limit = args.limit || 5;
|
|
354
|
+
try {
|
|
355
|
+
// Use DuckDuckGo Instant Answer API (free, no API key required)
|
|
356
|
+
const ddgUrl = `https://api.duckduckgo.com/?q=${query}&format=json&no_html=1&skip_disambig=1`;
|
|
357
|
+
const controller = new AbortController();
|
|
358
|
+
const timeout = setTimeout(() => controller.abort(), 8000);
|
|
359
|
+
const res = await fetch(ddgUrl, { signal: controller.signal });
|
|
360
|
+
clearTimeout(timeout);
|
|
361
|
+
const data = await res.json();
|
|
362
|
+
|
|
363
|
+
const results = [];
|
|
364
|
+
// AbstractText summary
|
|
365
|
+
if (data.AbstractText) {
|
|
366
|
+
results.push({ title: data.AbstractSource || 'Summary', snippet: data.AbstractText, url: data.AbstractURL || '' });
|
|
367
|
+
}
|
|
368
|
+
// RelatedTopics as search results
|
|
369
|
+
if (data.RelatedTopics) {
|
|
370
|
+
for (const topic of data.RelatedTopics.slice(0, limit)) {
|
|
371
|
+
if (topic.Text) {
|
|
372
|
+
results.push({ title: topic.Text.slice(0, 80), snippet: topic.Text, url: topic.FirstURL || '' });
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
// If DDG has no results, fall back to scraping Google search page
|
|
377
|
+
if (results.length === 0) {
|
|
378
|
+
const googleUrl = `https://www.google.com/search?q=${query}&num=${limit}`;
|
|
379
|
+
const gRes = await fetch(googleUrl, {
|
|
380
|
+
headers: { 'User-Agent': 'Mozilla/5.0 (compatible; AIEnterprise/1.0)' },
|
|
381
|
+
signal: AbortSignal.timeout(8000),
|
|
382
|
+
});
|
|
383
|
+
const html = await gRes.text();
|
|
384
|
+
// Simple extraction of search result snippets
|
|
385
|
+
const snippets = html.match(/<span[^>]*>([^<]{50,300})<\/span>/g) || [];
|
|
386
|
+
snippets.slice(0, limit).forEach((s, i) => {
|
|
387
|
+
const text = s.replace(/<[^>]*>/g, '');
|
|
388
|
+
results.push({ title: `Result ${i + 1}`, snippet: text, url: '' });
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
return JSON.stringify({ query: args.query, results: results.slice(0, limit), count: results.length });
|
|
392
|
+
} catch (e) {
|
|
393
|
+
return JSON.stringify({ query: args.query, error: e.message, results: [] });
|
|
394
|
+
}
|
|
395
|
+
},
|
|
396
|
+
},
|
|
397
|
+
],
|
|
398
|
+
hooks: {},
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Code Review Plugin - Automated code quality checks
|
|
403
|
+
*/
|
|
404
|
+
export const CodeReviewPlugin = new PluginManifest({
|
|
405
|
+
id: 'builtin-code-review',
|
|
406
|
+
name: 'Code Review Assistant',
|
|
407
|
+
version: '1.0.0',
|
|
408
|
+
description: 'Automatically reviews code produced by agents for quality and security',
|
|
409
|
+
author: 'Idea Unlimited',
|
|
410
|
+
tools: [],
|
|
411
|
+
hooks: {
|
|
412
|
+
[HookPoint.AFTER_TOOL_CALL]: async (context, config) => {
|
|
413
|
+
// After file_write, check the written content
|
|
414
|
+
if (context.toolName === 'file_write' && context.result) {
|
|
415
|
+
const warnings = [];
|
|
416
|
+
const content = context.args?.content || '';
|
|
417
|
+
|
|
418
|
+
// Simple checks
|
|
419
|
+
if (content.includes('TODO')) warnings.push('Contains TODO comments');
|
|
420
|
+
if (content.includes('console.log')) warnings.push('Contains console.log statements');
|
|
421
|
+
if (content.includes('eval(')) warnings.push('Uses eval() - potential security risk');
|
|
422
|
+
if (content.length > 10000) warnings.push('File is very long (>10K chars)');
|
|
423
|
+
|
|
424
|
+
if (warnings.length > 0) {
|
|
425
|
+
return { warnings, file: context.args?.path };
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
return null;
|
|
429
|
+
},
|
|
430
|
+
},
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Notification Plugin - Send notifications on important events
|
|
435
|
+
*/
|
|
436
|
+
export const NotificationPlugin = new PluginManifest({
|
|
437
|
+
id: 'builtin-notifications',
|
|
438
|
+
name: 'Event Notifications',
|
|
439
|
+
version: '1.0.0',
|
|
440
|
+
description: 'Sends notifications when important events occur (task completion, errors, etc.)',
|
|
441
|
+
author: 'Idea Unlimited',
|
|
442
|
+
configSchema: {
|
|
443
|
+
webhookUrl: { type: 'string', description: 'Webhook URL for notifications' },
|
|
444
|
+
notifyOnError: { type: 'boolean', default: true },
|
|
445
|
+
notifyOnTaskComplete: { type: 'boolean', default: true },
|
|
446
|
+
},
|
|
447
|
+
tools: [],
|
|
448
|
+
hooks: {
|
|
449
|
+
[HookPoint.AGENT_TASK_END]: async (context, config) => {
|
|
450
|
+
const message = `✅ Task completed by ${context.agentName}: ${context.taskTitle || 'Unknown'}`;
|
|
451
|
+
console.log(`📢 [Notification] ${message}`);
|
|
452
|
+
// Actually send webhook notification
|
|
453
|
+
if (config.webhookUrl) {
|
|
454
|
+
try {
|
|
455
|
+
await fetch(config.webhookUrl, {
|
|
456
|
+
method: 'POST',
|
|
457
|
+
headers: { 'Content-Type': 'application/json' },
|
|
458
|
+
body: JSON.stringify({ event: 'task_complete', message, agent: context.agentName, task: context.taskTitle, timestamp: new Date().toISOString() }),
|
|
459
|
+
signal: AbortSignal.timeout(5000),
|
|
460
|
+
});
|
|
461
|
+
} catch (e) { console.warn(`[Notification] Webhook failed: ${e.message}`); }
|
|
462
|
+
}
|
|
463
|
+
return { notified: true, message };
|
|
464
|
+
},
|
|
465
|
+
[HookPoint.REQUIREMENT_COMPLETED]: async (context, config) => {
|
|
466
|
+
const message = `🏁 Requirement completed: ${context.requirementTitle || 'Unknown'}`;
|
|
467
|
+
console.log(`📢 [Notification] ${message}`);
|
|
468
|
+
if (config.webhookUrl) {
|
|
469
|
+
try {
|
|
470
|
+
await fetch(config.webhookUrl, {
|
|
471
|
+
method: 'POST',
|
|
472
|
+
headers: { 'Content-Type': 'application/json' },
|
|
473
|
+
body: JSON.stringify({ event: 'requirement_complete', message, timestamp: new Date().toISOString() }),
|
|
474
|
+
signal: AbortSignal.timeout(5000),
|
|
475
|
+
});
|
|
476
|
+
} catch (e) { console.warn(`[Notification] Webhook failed: ${e.message}`); }
|
|
477
|
+
}
|
|
478
|
+
return { notified: true, message };
|
|
479
|
+
},
|
|
480
|
+
},
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Web Fetch Plugin - Fetch web page content
|
|
485
|
+
* Distilled from OpenClaw's web_fetch tool
|
|
486
|
+
*/
|
|
487
|
+
export const WebFetchPlugin = new PluginManifest({
|
|
488
|
+
id: 'builtin-web-fetch',
|
|
489
|
+
name: 'Web Fetch',
|
|
490
|
+
version: '1.0.0',
|
|
491
|
+
description: 'Fetch and extract content from web pages (URLs, APIs)',
|
|
492
|
+
author: 'Idea Unlimited',
|
|
493
|
+
configSchema: {
|
|
494
|
+
timeout: { type: 'number', default: 10000, description: 'Request timeout (ms)' },
|
|
495
|
+
maxSize: { type: 'number', default: 1048576, description: 'Max response size (bytes)' },
|
|
496
|
+
},
|
|
497
|
+
tools: [
|
|
498
|
+
{
|
|
499
|
+
type: 'function',
|
|
500
|
+
function: {
|
|
501
|
+
name: 'web_fetch',
|
|
502
|
+
description: 'Fetch content from a URL. Supports HTML pages, JSON APIs, and plain text.',
|
|
503
|
+
parameters: {
|
|
504
|
+
type: 'object',
|
|
505
|
+
properties: {
|
|
506
|
+
url: { type: 'string', description: 'The URL to fetch' },
|
|
507
|
+
format: { type: 'string', enum: ['text', 'json', 'html'], description: 'Expected response format (default: text)' },
|
|
508
|
+
},
|
|
509
|
+
required: ['url'],
|
|
510
|
+
},
|
|
511
|
+
},
|
|
512
|
+
_executor: async (args, config) => {
|
|
513
|
+
try {
|
|
514
|
+
const controller = new AbortController();
|
|
515
|
+
const timeout = setTimeout(() => controller.abort(), config.timeout || 10000);
|
|
516
|
+
const res = await fetch(args.url, { signal: controller.signal });
|
|
517
|
+
clearTimeout(timeout);
|
|
518
|
+
const text = await res.text();
|
|
519
|
+
const truncated = text.slice(0, config.maxSize || 1048576);
|
|
520
|
+
return JSON.stringify({ url: args.url, status: res.status, content: truncated });
|
|
521
|
+
} catch (e) {
|
|
522
|
+
return JSON.stringify({ error: e.message, url: args.url });
|
|
523
|
+
}
|
|
524
|
+
},
|
|
525
|
+
},
|
|
526
|
+
],
|
|
527
|
+
hooks: {},
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Browser Automation Plugin - Control a browser for web tasks
|
|
532
|
+
* Distilled from OpenClaw's browser tool
|
|
533
|
+
*/
|
|
534
|
+
export const BrowserPlugin = new PluginManifest({
|
|
535
|
+
id: 'builtin-browser',
|
|
536
|
+
name: 'Browser Automation',
|
|
537
|
+
version: '1.0.0',
|
|
538
|
+
description: 'Control a headless browser to navigate, screenshot, and interact with web pages',
|
|
539
|
+
author: 'Idea Unlimited',
|
|
540
|
+
configSchema: {
|
|
541
|
+
headless: { type: 'boolean', default: true, description: 'Run browser in headless mode' },
|
|
542
|
+
},
|
|
543
|
+
tools: [
|
|
544
|
+
{
|
|
545
|
+
type: 'function',
|
|
546
|
+
function: {
|
|
547
|
+
name: 'browser_navigate',
|
|
548
|
+
description: 'Navigate the browser to a URL and return the page snapshot (DOM summary).',
|
|
549
|
+
parameters: {
|
|
550
|
+
type: 'object',
|
|
551
|
+
properties: {
|
|
552
|
+
url: { type: 'string', description: 'URL to navigate to' },
|
|
553
|
+
waitFor: { type: 'string', description: 'CSS selector to wait for before snapshot' },
|
|
554
|
+
},
|
|
555
|
+
required: ['url'],
|
|
556
|
+
},
|
|
557
|
+
},
|
|
558
|
+
_executor: async (args) => {
|
|
559
|
+
// Real implementation: use fetch to get page content as a DOM snapshot
|
|
560
|
+
try {
|
|
561
|
+
const controller = new AbortController();
|
|
562
|
+
const timeout = setTimeout(() => controller.abort(), 15000);
|
|
563
|
+
const res = await fetch(args.url, {
|
|
564
|
+
headers: { 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' },
|
|
565
|
+
signal: controller.signal,
|
|
566
|
+
redirect: 'follow',
|
|
567
|
+
});
|
|
568
|
+
clearTimeout(timeout);
|
|
569
|
+
const html = await res.text();
|
|
570
|
+
// Extract plain text content (remove script/style tags)
|
|
571
|
+
const cleaned = html
|
|
572
|
+
.replace(/<script[\s\S]*?<\/script>/gi, '')
|
|
573
|
+
.replace(/<style[\s\S]*?<\/style>/gi, '')
|
|
574
|
+
.replace(/<[^>]+>/g, ' ')
|
|
575
|
+
.replace(/\s+/g, ' ')
|
|
576
|
+
.trim()
|
|
577
|
+
.slice(0, 8000);
|
|
578
|
+
const title = (html.match(/<title[^>]*>([^<]*)<\/title>/i) || [])[1] || '';
|
|
579
|
+
return JSON.stringify({ status: 'navigated', url: args.url, title, httpStatus: res.status, textContent: cleaned, contentLength: html.length });
|
|
580
|
+
} catch (e) {
|
|
581
|
+
return JSON.stringify({ status: 'error', url: args.url, error: e.message });
|
|
582
|
+
}
|
|
583
|
+
},
|
|
584
|
+
},
|
|
585
|
+
{
|
|
586
|
+
type: 'function',
|
|
587
|
+
function: {
|
|
588
|
+
name: 'browser_screenshot',
|
|
589
|
+
description: 'Take a screenshot of the current browser page.',
|
|
590
|
+
parameters: {
|
|
591
|
+
type: 'object',
|
|
592
|
+
properties: {
|
|
593
|
+
selector: { type: 'string', description: 'Optional CSS selector to screenshot a specific element' },
|
|
594
|
+
},
|
|
595
|
+
},
|
|
596
|
+
},
|
|
597
|
+
_executor: async (args) => {
|
|
598
|
+
// Screenshot requires puppeteer; try to load it dynamically (use require to avoid webpack parsing)
|
|
599
|
+
try {
|
|
600
|
+
const puppeteer = tryRequire('puppeteer');
|
|
601
|
+
if (!puppeteer) {
|
|
602
|
+
return JSON.stringify({ status: 'unavailable', error: 'puppeteer not installed. Run: npm install puppeteer' });
|
|
603
|
+
}
|
|
604
|
+
const browser = await puppeteer.launch({ headless: 'new' });
|
|
605
|
+
const page = await browser.newPage();
|
|
606
|
+
await page.goto('about:blank'); // Use the currently loaded page
|
|
607
|
+
const screenshotPath = path.join(DATA_DIR, `screenshot-${Date.now()}.png`);
|
|
608
|
+
if (args.selector) {
|
|
609
|
+
const el = await page.$(args.selector);
|
|
610
|
+
if (el) await el.screenshot({ path: screenshotPath });
|
|
611
|
+
} else {
|
|
612
|
+
await page.screenshot({ path: screenshotPath, fullPage: true });
|
|
613
|
+
}
|
|
614
|
+
await browser.close();
|
|
615
|
+
return JSON.stringify({ status: 'captured', path: screenshotPath });
|
|
616
|
+
} catch (e) {
|
|
617
|
+
return JSON.stringify({ status: 'error', error: e.message });
|
|
618
|
+
}
|
|
619
|
+
},
|
|
620
|
+
},
|
|
621
|
+
],
|
|
622
|
+
hooks: {},
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* Memory Plugin - Persistent agent memory (search & store)
|
|
627
|
+
* Distilled from OpenClaw's memory-core and memory-lancedb plugins
|
|
628
|
+
*/
|
|
629
|
+
export const MemoryPlugin = new PluginManifest({
|
|
630
|
+
id: 'builtin-memory',
|
|
631
|
+
name: 'Memory System',
|
|
632
|
+
version: '1.0.0',
|
|
633
|
+
description: 'Persistent memory for agents — search, store, and recall long-term knowledge',
|
|
634
|
+
author: 'Idea Unlimited',
|
|
635
|
+
configSchema: {
|
|
636
|
+
backend: { type: 'string', default: 'json', enum: ['json', 'sqlite'], description: 'Storage backend' },
|
|
637
|
+
maxEntries: { type: 'number', default: 1000, description: 'Max stored memory entries' },
|
|
638
|
+
},
|
|
639
|
+
tools: [
|
|
640
|
+
{
|
|
641
|
+
type: 'function',
|
|
642
|
+
function: {
|
|
643
|
+
name: 'memory_search',
|
|
644
|
+
description: 'Search agent memory for relevant past information, decisions, or facts.',
|
|
645
|
+
parameters: {
|
|
646
|
+
type: 'object',
|
|
647
|
+
properties: {
|
|
648
|
+
query: { type: 'string', description: 'Search query' },
|
|
649
|
+
limit: { type: 'number', description: 'Max results to return (default: 5)' },
|
|
650
|
+
},
|
|
651
|
+
required: ['query'],
|
|
652
|
+
},
|
|
653
|
+
},
|
|
654
|
+
_executor: async (args) => {
|
|
655
|
+
// Real implementation: search the knowledge base
|
|
656
|
+
try {
|
|
657
|
+
if (_knowledgeManager) {
|
|
658
|
+
const results = _knowledgeManager.search(args.query, { limit: args.limit || 5 });
|
|
659
|
+
return JSON.stringify({ query: args.query, results: results.map(r => ({ title: r.title, content: r.content, type: r.type, score: r.relevanceScore, source: r.knowledgeBaseName })), count: results.length });
|
|
660
|
+
}
|
|
661
|
+
return JSON.stringify({ query: args.query, results: [], note: 'KnowledgeManager not initialized, call initPluginRuntime()' });
|
|
662
|
+
} catch (e) {
|
|
663
|
+
return JSON.stringify({ query: args.query, error: e.message, results: [] });
|
|
664
|
+
}
|
|
665
|
+
},
|
|
666
|
+
},
|
|
667
|
+
{
|
|
668
|
+
type: 'function',
|
|
669
|
+
function: {
|
|
670
|
+
name: 'memory_store',
|
|
671
|
+
description: 'Store important information in agent long-term memory for future recall.',
|
|
672
|
+
parameters: {
|
|
673
|
+
type: 'object',
|
|
674
|
+
properties: {
|
|
675
|
+
text: { type: 'string', description: 'Information to remember' },
|
|
676
|
+
importance: { type: 'number', description: 'Importance score 0-1 (default: 0.7)' },
|
|
677
|
+
category: { type: 'string', enum: ['fact', 'decision', 'preference', 'task', 'other'], description: 'Memory category' },
|
|
678
|
+
},
|
|
679
|
+
required: ['text'],
|
|
680
|
+
},
|
|
681
|
+
},
|
|
682
|
+
_executor: async (args) => {
|
|
683
|
+
// Real implementation: store to the knowledge base
|
|
684
|
+
try {
|
|
685
|
+
if (_knowledgeManager) {
|
|
686
|
+
// Get or create the default knowledge base
|
|
687
|
+
let bases = _knowledgeManager.list();
|
|
688
|
+
let kbId = bases[0]?.id;
|
|
689
|
+
if (!kbId) {
|
|
690
|
+
const kb = _knowledgeManager.create({ name: 'Agent Memory', description: 'Auto-created memory store', type: 'global' });
|
|
691
|
+
kbId = kb.id;
|
|
692
|
+
}
|
|
693
|
+
const entry = _knowledgeManager.addEntry(kbId, {
|
|
694
|
+
title: args.text.slice(0, 80),
|
|
695
|
+
content: args.text,
|
|
696
|
+
type: args.category === 'decision' ? 'decision' : args.category === 'fact' ? 'fact' : 'note',
|
|
697
|
+
importance: args.importance || 0.7,
|
|
698
|
+
tags: [args.category || 'other', 'memory'],
|
|
699
|
+
});
|
|
700
|
+
return JSON.stringify({ stored: true, entryId: entry.id, category: args.category || 'other' });
|
|
701
|
+
}
|
|
702
|
+
return JSON.stringify({ stored: false, note: 'KnowledgeManager not initialized' });
|
|
703
|
+
} catch (e) {
|
|
704
|
+
return JSON.stringify({ stored: false, error: e.message });
|
|
705
|
+
}
|
|
706
|
+
},
|
|
707
|
+
},
|
|
708
|
+
],
|
|
709
|
+
hooks: {},
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
/**
|
|
713
|
+
* Image Processing Plugin - Generate and manipulate images
|
|
714
|
+
* Distilled from OpenClaw's image tool
|
|
715
|
+
*/
|
|
716
|
+
export const ImagePlugin = new PluginManifest({
|
|
717
|
+
id: 'builtin-image',
|
|
718
|
+
name: 'Image Processing',
|
|
719
|
+
version: '1.0.0',
|
|
720
|
+
description: 'Generate, analyze, and manipulate images via AI vision models',
|
|
721
|
+
author: 'Idea Unlimited',
|
|
722
|
+
configSchema: {
|
|
723
|
+
provider: { type: 'string', default: 'openai', description: 'Image generation provider' },
|
|
724
|
+
},
|
|
725
|
+
tools: [
|
|
726
|
+
{
|
|
727
|
+
type: 'function',
|
|
728
|
+
function: {
|
|
729
|
+
name: 'image_generate',
|
|
730
|
+
description: 'Generate an image from a text description using AI.',
|
|
731
|
+
parameters: {
|
|
732
|
+
type: 'object',
|
|
733
|
+
properties: {
|
|
734
|
+
prompt: { type: 'string', description: 'Image description prompt' },
|
|
735
|
+
size: { type: 'string', enum: ['256x256', '512x512', '1024x1024'], description: 'Image size' },
|
|
736
|
+
},
|
|
737
|
+
required: ['prompt'],
|
|
738
|
+
},
|
|
739
|
+
},
|
|
740
|
+
_executor: async (args) => {
|
|
741
|
+
// Real implementation: call the OpenAI DALL-E API
|
|
742
|
+
try {
|
|
743
|
+
if (_llmClient && _llmClient._imageProvider) {
|
|
744
|
+
const result = await _llmClient.generateImage(_llmClient._imageProvider, args.prompt, { size: args.size || '1024x1024' });
|
|
745
|
+
return JSON.stringify({ status: 'generated', url: result.url, revisedPrompt: result.revisedPrompt });
|
|
746
|
+
}
|
|
747
|
+
return JSON.stringify({ status: 'error', error: 'No image provider configured. Add an OpenAI provider with DALL-E support.' });
|
|
748
|
+
} catch (e) {
|
|
749
|
+
return JSON.stringify({ status: 'error', error: e.message });
|
|
750
|
+
}
|
|
751
|
+
},
|
|
752
|
+
},
|
|
753
|
+
],
|
|
754
|
+
hooks: {},
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
/**
|
|
758
|
+
* PDF Processing Plugin - Read and extract content from PDFs
|
|
759
|
+
* Distilled from OpenClaw's pdf tool
|
|
760
|
+
*/
|
|
761
|
+
export const PdfPlugin = new PluginManifest({
|
|
762
|
+
id: 'builtin-pdf',
|
|
763
|
+
name: 'PDF Processing',
|
|
764
|
+
version: '1.0.0',
|
|
765
|
+
description: 'Read, extract text, and analyze PDF documents',
|
|
766
|
+
author: 'Idea Unlimited',
|
|
767
|
+
tools: [
|
|
768
|
+
{
|
|
769
|
+
type: 'function',
|
|
770
|
+
function: {
|
|
771
|
+
name: 'pdf_read',
|
|
772
|
+
description: 'Extract text content from a PDF file.',
|
|
773
|
+
parameters: {
|
|
774
|
+
type: 'object',
|
|
775
|
+
properties: {
|
|
776
|
+
path: { type: 'string', description: 'Path to the PDF file (relative to workspace)' },
|
|
777
|
+
pages: { type: 'string', description: 'Page range, e.g. "1-5" or "1,3,5" (default: all)' },
|
|
778
|
+
},
|
|
779
|
+
required: ['path'],
|
|
780
|
+
},
|
|
781
|
+
},
|
|
782
|
+
_executor: async (args) => {
|
|
783
|
+
// Real implementation: PDF text extraction
|
|
784
|
+
try {
|
|
785
|
+
const filePath = path.resolve(WORKSPACE_DIR, args.path);
|
|
786
|
+
if (!existsSync(filePath)) {
|
|
787
|
+
return JSON.stringify({ error: `File not found: ${args.path}` });
|
|
788
|
+
}
|
|
789
|
+
// Try using pdf-parse (use require to avoid webpack parsing)
|
|
790
|
+
const pdfParse = tryRequire('pdf-parse');
|
|
791
|
+
if (pdfParse) {
|
|
792
|
+
const buffer = await fs.readFile(filePath);
|
|
793
|
+
const data = await pdfParse(buffer);
|
|
794
|
+
let text = data.text || '';
|
|
795
|
+
// If a page range is specified, do a simple slice
|
|
796
|
+
if (args.pages) {
|
|
797
|
+
const lines = text.split('\n');
|
|
798
|
+
const perPage = Math.ceil(lines.length / (data.numpages || 1));
|
|
799
|
+
const pageNums = args.pages.includes('-')
|
|
800
|
+
? Array.from({length: parseInt(args.pages.split('-')[1]) - parseInt(args.pages.split('-')[0]) + 1}, (_, i) => parseInt(args.pages.split('-')[0]) + i)
|
|
801
|
+
: args.pages.split(',').map(Number);
|
|
802
|
+
text = pageNums.map(p => lines.slice((p-1)*perPage, p*perPage).join('\n')).join('\n---PAGE BREAK---\n');
|
|
803
|
+
}
|
|
804
|
+
return JSON.stringify({ path: args.path, pages: data.numpages, text: text.slice(0, 20000), textLength: text.length });
|
|
805
|
+
}
|
|
806
|
+
// Fallback: use pdftotext command-line tool
|
|
807
|
+
try {
|
|
808
|
+
const { stdout } = await execAsync(`pdftotext "${filePath}" -`, { timeout: 15000 });
|
|
809
|
+
return JSON.stringify({ path: args.path, text: stdout.slice(0, 20000), textLength: stdout.length, method: 'pdftotext' });
|
|
810
|
+
} catch {
|
|
811
|
+
// Final fallback: read binary and extract visible text
|
|
812
|
+
const raw = await fs.readFile(filePath, 'latin1');
|
|
813
|
+
const textMatches = raw.match(/\(([^)]{2,})\)/g) || [];
|
|
814
|
+
const extracted = textMatches.map(m => m.slice(1, -1)).join(' ').slice(0, 10000);
|
|
815
|
+
return JSON.stringify({ path: args.path, text: extracted, textLength: extracted.length, method: 'raw-extract', note: 'Install pdf-parse for better results: npm install pdf-parse' });
|
|
816
|
+
}
|
|
817
|
+
} catch (e) {
|
|
818
|
+
return JSON.stringify({ error: e.message, path: args.path });
|
|
819
|
+
}
|
|
820
|
+
},
|
|
821
|
+
},
|
|
822
|
+
],
|
|
823
|
+
hooks: {},
|
|
824
|
+
});
|
|
825
|
+
|
|
826
|
+
/**
|
|
827
|
+
* Canvas Plugin - Render and present visual content
|
|
828
|
+
* Distilled from OpenClaw's canvas tool
|
|
829
|
+
*/
|
|
830
|
+
export const CanvasPlugin = new PluginManifest({
|
|
831
|
+
id: 'builtin-canvas',
|
|
832
|
+
name: 'Canvas Rendering',
|
|
833
|
+
version: '1.0.0',
|
|
834
|
+
description: 'Present and render visual content, interactive UIs, and diagrams',
|
|
835
|
+
author: 'Idea Unlimited',
|
|
836
|
+
tools: [
|
|
837
|
+
{
|
|
838
|
+
type: 'function',
|
|
839
|
+
function: {
|
|
840
|
+
name: 'canvas_present',
|
|
841
|
+
description: 'Render HTML/CSS/JS content in the canvas viewer for the user.',
|
|
842
|
+
parameters: {
|
|
843
|
+
type: 'object',
|
|
844
|
+
properties: {
|
|
845
|
+
html: { type: 'string', description: 'HTML content to render' },
|
|
846
|
+
title: { type: 'string', description: 'Title for the canvas' },
|
|
847
|
+
},
|
|
848
|
+
required: ['html'],
|
|
849
|
+
},
|
|
850
|
+
},
|
|
851
|
+
_executor: async (args) => {
|
|
852
|
+
// Real implementation: write HTML content to a file and return the path
|
|
853
|
+
try {
|
|
854
|
+
const canvasDir = path.join(DATA_DIR, 'canvas');
|
|
855
|
+
if (!existsSync(canvasDir)) mkdirSync(canvasDir, { recursive: true });
|
|
856
|
+
const filename = `canvas-${Date.now()}.html`;
|
|
857
|
+
const filePath = path.join(canvasDir, filename);
|
|
858
|
+
const fullHtml = `<!DOCTYPE html><html><head><meta charset="utf-8"><title>${args.title || 'Canvas'}</title></head><body>${args.html}</body></html>`;
|
|
859
|
+
await fs.writeFile(filePath, fullHtml, 'utf-8');
|
|
860
|
+
return JSON.stringify({ presented: true, title: args.title || 'Untitled', path: filePath, contentLength: args.html.length });
|
|
861
|
+
} catch (e) {
|
|
862
|
+
return JSON.stringify({ presented: false, error: e.message });
|
|
863
|
+
}
|
|
864
|
+
},
|
|
865
|
+
},
|
|
866
|
+
],
|
|
867
|
+
hooks: {},
|
|
868
|
+
});
|
|
869
|
+
|
|
870
|
+
/**
|
|
871
|
+
* Data Processing Plugin - Process and transform data
|
|
872
|
+
*/
|
|
873
|
+
export const DataProcessingPlugin = new PluginManifest({
|
|
874
|
+
id: 'builtin-data-processing',
|
|
875
|
+
name: 'Data Processing',
|
|
876
|
+
version: '1.0.0',
|
|
877
|
+
description: 'Parse, transform, and analyze structured data (CSV, JSON, Excel)',
|
|
878
|
+
author: 'Idea Unlimited',
|
|
879
|
+
tools: [
|
|
880
|
+
{
|
|
881
|
+
type: 'function',
|
|
882
|
+
function: {
|
|
883
|
+
name: 'data_parse',
|
|
884
|
+
description: 'Parse structured data from CSV, JSON, or tabular text.',
|
|
885
|
+
parameters: {
|
|
886
|
+
type: 'object',
|
|
887
|
+
properties: {
|
|
888
|
+
content: { type: 'string', description: 'Raw data content' },
|
|
889
|
+
format: { type: 'string', enum: ['csv', 'json', 'tsv'], description: 'Data format' },
|
|
890
|
+
},
|
|
891
|
+
required: ['content', 'format'],
|
|
892
|
+
},
|
|
893
|
+
},
|
|
894
|
+
_executor: async (args) => {
|
|
895
|
+
try {
|
|
896
|
+
if (args.format === 'json') {
|
|
897
|
+
const parsed = JSON.parse(args.content);
|
|
898
|
+
const rows = Array.isArray(parsed) ? parsed.length : 1;
|
|
899
|
+
return JSON.stringify({ rows, preview: JSON.stringify(parsed).slice(0, 500) });
|
|
900
|
+
}
|
|
901
|
+
const lines = args.content.trim().split('\n');
|
|
902
|
+
return JSON.stringify({ rows: lines.length, headers: lines[0], preview: lines.slice(0, 5).join('\n') });
|
|
903
|
+
} catch (e) {
|
|
904
|
+
return JSON.stringify({ error: e.message });
|
|
905
|
+
}
|
|
906
|
+
},
|
|
907
|
+
},
|
|
908
|
+
],
|
|
909
|
+
hooks: {},
|
|
910
|
+
});
|
|
911
|
+
|
|
912
|
+
/**
|
|
913
|
+
* TTS Plugin - Text-to-Speech capability
|
|
914
|
+
* Distilled from OpenClaw's tts tool
|
|
915
|
+
*/
|
|
916
|
+
export const TtsPlugin = new PluginManifest({
|
|
917
|
+
id: 'builtin-tts',
|
|
918
|
+
name: 'Text-to-Speech',
|
|
919
|
+
version: '1.0.0',
|
|
920
|
+
description: 'Convert text to spoken audio using AI voice models',
|
|
921
|
+
author: 'Idea Unlimited',
|
|
922
|
+
configSchema: {
|
|
923
|
+
provider: { type: 'string', default: 'openai', enum: ['openai', 'elevenlabs'], description: 'TTS provider' },
|
|
924
|
+
voice: { type: 'string', default: 'alloy', description: 'Voice name/ID' },
|
|
925
|
+
},
|
|
926
|
+
tools: [
|
|
927
|
+
{
|
|
928
|
+
type: 'function',
|
|
929
|
+
function: {
|
|
930
|
+
name: 'tts_speak',
|
|
931
|
+
description: 'Convert text to speech audio.',
|
|
932
|
+
parameters: {
|
|
933
|
+
type: 'object',
|
|
934
|
+
properties: {
|
|
935
|
+
text: { type: 'string', description: 'Text to speak' },
|
|
936
|
+
voice: { type: 'string', description: 'Voice ID (default from config)' },
|
|
937
|
+
},
|
|
938
|
+
required: ['text'],
|
|
939
|
+
},
|
|
940
|
+
},
|
|
941
|
+
_executor: async (args, config) => {
|
|
942
|
+
// Real implementation: call the OpenAI TTS API
|
|
943
|
+
try {
|
|
944
|
+
const openaiModule = tryRequire('openai');
|
|
945
|
+
if (!openaiModule) {
|
|
946
|
+
return JSON.stringify({ status: 'error', error: 'openai package not installed. Run: npm install openai' });
|
|
947
|
+
}
|
|
948
|
+
const OpenAI = openaiModule.default || openaiModule;
|
|
949
|
+
// TODO: TTS needs access to provider registry via Company instance
|
|
950
|
+
return JSON.stringify({ status: 'error', error: 'TTS plugin not yet connected to provider registry' });
|
|
951
|
+
} catch (e) {
|
|
952
|
+
return JSON.stringify({ status: 'error', error: e.message });
|
|
953
|
+
}
|
|
954
|
+
},
|
|
955
|
+
},
|
|
956
|
+
],
|
|
957
|
+
hooks: {},
|
|
958
|
+
});
|
|
959
|
+
|
|
960
|
+
/**
|
|
961
|
+
* Shell Exec Plugin - Run shell commands in the workspace
|
|
962
|
+
* Distilled from OpenClaw's exec + process tools
|
|
963
|
+
*/
|
|
964
|
+
export const ExecPlugin = new PluginManifest({
|
|
965
|
+
id: 'builtin-exec',
|
|
966
|
+
name: 'Shell Execution',
|
|
967
|
+
version: '1.0.0',
|
|
968
|
+
description: 'Run shell commands in the workspace with background process support',
|
|
969
|
+
author: 'Idea Unlimited',
|
|
970
|
+
configSchema: {
|
|
971
|
+
timeout: { type: 'number', default: 30, description: 'Default timeout in seconds' },
|
|
972
|
+
allowElevated: { type: 'boolean', default: false, description: 'Allow elevated (host) execution' },
|
|
973
|
+
},
|
|
974
|
+
tools: [
|
|
975
|
+
{
|
|
976
|
+
type: 'function',
|
|
977
|
+
function: {
|
|
978
|
+
name: 'exec',
|
|
979
|
+
description: 'Run a shell command in the workspace directory.',
|
|
980
|
+
parameters: {
|
|
981
|
+
type: 'object',
|
|
982
|
+
properties: {
|
|
983
|
+
command: { type: 'string', description: 'Shell command to execute' },
|
|
984
|
+
timeout: { type: 'number', description: 'Timeout in seconds (default: 30)' },
|
|
985
|
+
background: { type: 'boolean', description: 'Run in background (default: false)' },
|
|
986
|
+
},
|
|
987
|
+
required: ['command'],
|
|
988
|
+
},
|
|
989
|
+
},
|
|
990
|
+
_executor: async (args, config) => {
|
|
991
|
+
// Real implementation: execute a shell command
|
|
992
|
+
try {
|
|
993
|
+
const timeout = (args.timeout || config.timeout || 30) * 1000;
|
|
994
|
+
if (args.background) {
|
|
995
|
+
// Background process: spawn and return PID
|
|
996
|
+
const { spawn } = await import('child_process');
|
|
997
|
+
const parts = args.command.split(/\s+/);
|
|
998
|
+
const child = spawn(parts[0], parts.slice(1), {
|
|
999
|
+
cwd: WORKSPACE_DIR, detached: true, stdio: 'ignore',
|
|
1000
|
+
});
|
|
1001
|
+
child.unref();
|
|
1002
|
+
return JSON.stringify({ status: 'background', pid: child.pid, command: args.command });
|
|
1003
|
+
}
|
|
1004
|
+
// Foreground execution
|
|
1005
|
+
const { stdout, stderr } = await execAsync(args.command, {
|
|
1006
|
+
cwd: WORKSPACE_DIR, timeout, maxBuffer: 2 * 1024 * 1024,
|
|
1007
|
+
});
|
|
1008
|
+
return JSON.stringify({
|
|
1009
|
+
status: 'completed',
|
|
1010
|
+
command: args.command,
|
|
1011
|
+
stdout: stdout.slice(0, 10000),
|
|
1012
|
+
stderr: stderr ? stderr.slice(0, 5000) : '',
|
|
1013
|
+
exitCode: 0,
|
|
1014
|
+
});
|
|
1015
|
+
} catch (e) {
|
|
1016
|
+
return JSON.stringify({
|
|
1017
|
+
status: 'error',
|
|
1018
|
+
command: args.command,
|
|
1019
|
+
stdout: e.stdout?.slice(0, 5000) || '',
|
|
1020
|
+
stderr: e.stderr?.slice(0, 5000) || e.message,
|
|
1021
|
+
exitCode: e.code || 1,
|
|
1022
|
+
});
|
|
1023
|
+
}
|
|
1024
|
+
},
|
|
1025
|
+
},
|
|
1026
|
+
{
|
|
1027
|
+
type: 'function',
|
|
1028
|
+
function: {
|
|
1029
|
+
name: 'process',
|
|
1030
|
+
description: 'Manage background exec sessions (list, poll, log, write, kill, clear).',
|
|
1031
|
+
parameters: {
|
|
1032
|
+
type: 'object',
|
|
1033
|
+
properties: {
|
|
1034
|
+
action: { type: 'string', enum: ['list', 'poll', 'log', 'write', 'kill', 'clear'], description: 'Process management action' },
|
|
1035
|
+
sessionId: { type: 'string', description: 'Background session ID' },
|
|
1036
|
+
},
|
|
1037
|
+
required: ['action'],
|
|
1038
|
+
},
|
|
1039
|
+
},
|
|
1040
|
+
_executor: async (args) => {
|
|
1041
|
+
// Real implementation: process management
|
|
1042
|
+
try {
|
|
1043
|
+
switch (args.action) {
|
|
1044
|
+
case 'list': {
|
|
1045
|
+
const { stdout } = await execAsync('ps aux | head -20', { cwd: WORKSPACE_DIR, timeout: 5000 });
|
|
1046
|
+
return JSON.stringify({ action: 'list', output: stdout.slice(0, 5000) });
|
|
1047
|
+
}
|
|
1048
|
+
case 'kill': {
|
|
1049
|
+
if (!args.sessionId) return JSON.stringify({ error: 'sessionId (PID) required' });
|
|
1050
|
+
process.kill(parseInt(args.sessionId), 'SIGTERM');
|
|
1051
|
+
return JSON.stringify({ action: 'kill', pid: args.sessionId, status: 'signal_sent' });
|
|
1052
|
+
}
|
|
1053
|
+
default:
|
|
1054
|
+
return JSON.stringify({ action: args.action, error: `Unsupported action: ${args.action}` });
|
|
1055
|
+
}
|
|
1056
|
+
} catch (e) {
|
|
1057
|
+
return JSON.stringify({ action: args.action, error: e.message });
|
|
1058
|
+
}
|
|
1059
|
+
},
|
|
1060
|
+
},
|
|
1061
|
+
],
|
|
1062
|
+
hooks: {},
|
|
1063
|
+
});
|
|
1064
|
+
|
|
1065
|
+
/**
|
|
1066
|
+
* Apply Patch Plugin - Structured multi-file edits
|
|
1067
|
+
* Distilled from OpenClaw's apply_patch tool
|
|
1068
|
+
*/
|
|
1069
|
+
export const ApplyPatchPlugin = new PluginManifest({
|
|
1070
|
+
id: 'builtin-apply-patch',
|
|
1071
|
+
name: 'Apply Patch',
|
|
1072
|
+
version: '1.0.0',
|
|
1073
|
+
description: 'Apply structured patches across one or more files for multi-hunk edits',
|
|
1074
|
+
author: 'Idea Unlimited',
|
|
1075
|
+
tools: [
|
|
1076
|
+
{
|
|
1077
|
+
type: 'function',
|
|
1078
|
+
function: {
|
|
1079
|
+
name: 'apply_patch',
|
|
1080
|
+
description: 'Apply a structured patch to one or more files. Use *** Begin Patch / *** End Patch format.',
|
|
1081
|
+
parameters: {
|
|
1082
|
+
type: 'object',
|
|
1083
|
+
properties: {
|
|
1084
|
+
input: { type: 'string', description: 'Patch content using *** Begin Patch / *** End Patch format' },
|
|
1085
|
+
},
|
|
1086
|
+
required: ['input'],
|
|
1087
|
+
},
|
|
1088
|
+
},
|
|
1089
|
+
_executor: async (args) => {
|
|
1090
|
+
// Real implementation: parse and apply a unified diff patch
|
|
1091
|
+
try {
|
|
1092
|
+
const patchContent = args.input || '';
|
|
1093
|
+
// Parse *** Begin Patch / *** End Patch format
|
|
1094
|
+
const filePatches = patchContent.split(/^\*{3}\s+/m).filter(Boolean);
|
|
1095
|
+
const results = [];
|
|
1096
|
+
for (const block of filePatches) {
|
|
1097
|
+
const lines = block.split('\n');
|
|
1098
|
+
const fileMatch = lines[0].match(/^(.+?)\s*$/);
|
|
1099
|
+
if (!fileMatch) continue;
|
|
1100
|
+
const filePath = path.resolve(WORKSPACE_DIR, fileMatch[1].trim());
|
|
1101
|
+
// Read the original file
|
|
1102
|
+
let original = '';
|
|
1103
|
+
try { original = await fs.readFile(filePath, 'utf-8'); } catch {}
|
|
1104
|
+
// Apply patch lines (simplified: rule-based replacement)
|
|
1105
|
+
let modified = original;
|
|
1106
|
+
let applied = 0;
|
|
1107
|
+
for (let i = 1; i < lines.length; i++) {
|
|
1108
|
+
if (lines[i].startsWith('-')) {
|
|
1109
|
+
const oldLine = lines[i].slice(1);
|
|
1110
|
+
const newLine = (lines[i + 1] && lines[i + 1].startsWith('+')) ? lines[i + 1].slice(1) : '';
|
|
1111
|
+
if (modified.includes(oldLine)) {
|
|
1112
|
+
modified = modified.replace(oldLine, newLine);
|
|
1113
|
+
applied++;
|
|
1114
|
+
if (lines[i + 1]?.startsWith('+')) i++;
|
|
1115
|
+
}
|
|
1116
|
+
} else if (lines[i].startsWith('+') && !lines[i - 1]?.startsWith('-')) {
|
|
1117
|
+
modified += '\n' + lines[i].slice(1);
|
|
1118
|
+
applied++;
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
// Write the file
|
|
1122
|
+
const dir = path.dirname(filePath);
|
|
1123
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
1124
|
+
await fs.writeFile(filePath, modified, 'utf-8');
|
|
1125
|
+
results.push({ file: fileMatch[1].trim(), hunksApplied: applied, status: 'patched' });
|
|
1126
|
+
}
|
|
1127
|
+
return JSON.stringify({ status: 'applied', files: results, totalFiles: results.length });
|
|
1128
|
+
} catch (e) {
|
|
1129
|
+
return JSON.stringify({ status: 'error', error: e.message });
|
|
1130
|
+
}
|
|
1131
|
+
},
|
|
1132
|
+
},
|
|
1133
|
+
],
|
|
1134
|
+
hooks: {},
|
|
1135
|
+
});
|
|
1136
|
+
|
|
1137
|
+
/**
|
|
1138
|
+
* Message Plugin - Cross-channel messaging (Discord, Slack, Telegram, etc.)
|
|
1139
|
+
* Distilled from OpenClaw's message tool
|
|
1140
|
+
*/
|
|
1141
|
+
export const MessagePlugin = new PluginManifest({
|
|
1142
|
+
id: 'builtin-message',
|
|
1143
|
+
name: 'Messaging',
|
|
1144
|
+
version: '1.0.0',
|
|
1145
|
+
description: 'Send messages and actions across Discord, Slack, Telegram, WhatsApp, and more',
|
|
1146
|
+
author: 'Idea Unlimited',
|
|
1147
|
+
configSchema: {
|
|
1148
|
+
defaultChannel: { type: 'string', description: 'Default messaging channel' },
|
|
1149
|
+
},
|
|
1150
|
+
tools: [
|
|
1151
|
+
{
|
|
1152
|
+
type: 'function',
|
|
1153
|
+
function: {
|
|
1154
|
+
name: 'message_send',
|
|
1155
|
+
description: 'Send a message to a channel or user across connected messaging platforms.',
|
|
1156
|
+
parameters: {
|
|
1157
|
+
type: 'object',
|
|
1158
|
+
properties: {
|
|
1159
|
+
target: { type: 'string', description: 'Target (e.g. "slack:#general", "telegram:@user", "discord:channel:123")' },
|
|
1160
|
+
text: { type: 'string', description: 'Message text' },
|
|
1161
|
+
media: { type: 'string', description: 'Optional media attachment path' },
|
|
1162
|
+
},
|
|
1163
|
+
required: ['target', 'text'],
|
|
1164
|
+
},
|
|
1165
|
+
},
|
|
1166
|
+
_executor: async (args) => {
|
|
1167
|
+
// Real implementation: send message via the message bus
|
|
1168
|
+
try {
|
|
1169
|
+
if (_messageBus) {
|
|
1170
|
+
const msg = _messageBus.send({
|
|
1171
|
+
from: 'plugin:messaging',
|
|
1172
|
+
to: args.target,
|
|
1173
|
+
content: args.text,
|
|
1174
|
+
type: 'broadcast',
|
|
1175
|
+
metadata: { media: args.media || null },
|
|
1176
|
+
});
|
|
1177
|
+
return JSON.stringify({ sent: true, messageId: msg.id, target: args.target });
|
|
1178
|
+
}
|
|
1179
|
+
// Fallback: log to console
|
|
1180
|
+
console.log(`📨 [Message] To ${args.target}: ${args.text}`);
|
|
1181
|
+
return JSON.stringify({ sent: true, target: args.target, method: 'console' });
|
|
1182
|
+
} catch (e) {
|
|
1183
|
+
return JSON.stringify({ sent: false, error: e.message });
|
|
1184
|
+
}
|
|
1185
|
+
},
|
|
1186
|
+
},
|
|
1187
|
+
{
|
|
1188
|
+
type: 'function',
|
|
1189
|
+
function: {
|
|
1190
|
+
name: 'message_search',
|
|
1191
|
+
description: 'Search messages across connected channels.',
|
|
1192
|
+
parameters: {
|
|
1193
|
+
type: 'object',
|
|
1194
|
+
properties: {
|
|
1195
|
+
query: { type: 'string', description: 'Search query' },
|
|
1196
|
+
channel: { type: 'string', description: 'Limit to specific channel' },
|
|
1197
|
+
limit: { type: 'number', description: 'Max results (default: 10)' },
|
|
1198
|
+
},
|
|
1199
|
+
required: ['query'],
|
|
1200
|
+
},
|
|
1201
|
+
},
|
|
1202
|
+
_executor: async (args) => {
|
|
1203
|
+
// Real implementation: search message bus history
|
|
1204
|
+
try {
|
|
1205
|
+
if (_messageBus) {
|
|
1206
|
+
const allMsgs = _messageBus.messages || [];
|
|
1207
|
+
const q = args.query.toLowerCase();
|
|
1208
|
+
const results = allMsgs
|
|
1209
|
+
.filter(m => m.content?.toLowerCase().includes(q) || (args.channel && m.to === args.channel))
|
|
1210
|
+
.slice(-(args.limit || 10))
|
|
1211
|
+
.map(m => ({ id: m.id, from: m.from, to: m.to, content: m.content?.slice(0, 200), type: m.type, time: m.timestamp }));
|
|
1212
|
+
return JSON.stringify({ query: args.query, results, count: results.length });
|
|
1213
|
+
}
|
|
1214
|
+
return JSON.stringify({ query: args.query, results: [], note: 'MessageBus not initialized' });
|
|
1215
|
+
} catch (e) {
|
|
1216
|
+
return JSON.stringify({ query: args.query, error: e.message, results: [] });
|
|
1217
|
+
}
|
|
1218
|
+
},
|
|
1219
|
+
},
|
|
1220
|
+
],
|
|
1221
|
+
hooks: {},
|
|
1222
|
+
});
|
|
1223
|
+
|
|
1224
|
+
/**
|
|
1225
|
+
* Cron Plugin - Scheduled task management
|
|
1226
|
+
* Distilled from OpenClaw's cron tool
|
|
1227
|
+
*/
|
|
1228
|
+
export const CronPlugin = new PluginManifest({
|
|
1229
|
+
id: 'builtin-cron',
|
|
1230
|
+
name: 'Cron Scheduler',
|
|
1231
|
+
version: '1.0.0',
|
|
1232
|
+
description: 'Manage cron jobs, reminders, and scheduled wake events',
|
|
1233
|
+
author: 'Idea Unlimited',
|
|
1234
|
+
tools: [
|
|
1235
|
+
{
|
|
1236
|
+
type: 'function',
|
|
1237
|
+
function: {
|
|
1238
|
+
name: 'cron_manage',
|
|
1239
|
+
description: 'Manage cron jobs: list, add, update, remove, or trigger a run.',
|
|
1240
|
+
parameters: {
|
|
1241
|
+
type: 'object',
|
|
1242
|
+
properties: {
|
|
1243
|
+
action: { type: 'string', enum: ['list', 'add', 'update', 'remove', 'run', 'status'], description: 'Cron action' },
|
|
1244
|
+
jobId: { type: 'string', description: 'Job ID (for update/remove/run)' },
|
|
1245
|
+
schedule: { type: 'string', description: 'Cron expression (for add/update)' },
|
|
1246
|
+
command: { type: 'string', description: 'Command or event to trigger (for add)' },
|
|
1247
|
+
label: { type: 'string', description: 'Human-readable label for the job' },
|
|
1248
|
+
},
|
|
1249
|
+
required: ['action'],
|
|
1250
|
+
},
|
|
1251
|
+
},
|
|
1252
|
+
_executor: async (args) => {
|
|
1253
|
+
// Real implementation: connect to the cronScheduler singleton
|
|
1254
|
+
try {
|
|
1255
|
+
if (!_cronScheduler) {
|
|
1256
|
+
return JSON.stringify({ error: 'CronScheduler not initialized, call initPluginRuntime()' });
|
|
1257
|
+
}
|
|
1258
|
+
switch (args.action) {
|
|
1259
|
+
case 'list':
|
|
1260
|
+
return JSON.stringify({ jobs: _cronScheduler.listJobs(), summary: _cronScheduler.getSummary() });
|
|
1261
|
+
case 'add': {
|
|
1262
|
+
if (!args.schedule || !args.command) return JSON.stringify({ error: 'schedule and command required' });
|
|
1263
|
+
const job = _cronScheduler.addJob({
|
|
1264
|
+
name: args.label || args.command.slice(0, 50),
|
|
1265
|
+
cronExpression: args.schedule,
|
|
1266
|
+
agentId: 'plugin-cron',
|
|
1267
|
+
taskPrompt: args.command,
|
|
1268
|
+
description: args.label || '',
|
|
1269
|
+
});
|
|
1270
|
+
return JSON.stringify({ action: 'add', jobId: job.id, name: job.name, nextRun: job.nextRun?.toISOString() });
|
|
1271
|
+
}
|
|
1272
|
+
case 'remove':
|
|
1273
|
+
if (!args.jobId) return JSON.stringify({ error: 'jobId required' });
|
|
1274
|
+
_cronScheduler.removeJob(args.jobId);
|
|
1275
|
+
return JSON.stringify({ action: 'remove', jobId: args.jobId, status: 'removed' });
|
|
1276
|
+
case 'status':
|
|
1277
|
+
return JSON.stringify({ summary: _cronScheduler.getSummary(), running: _cronScheduler.running });
|
|
1278
|
+
case 'run':
|
|
1279
|
+
if (!args.jobId) return JSON.stringify({ error: 'jobId required' });
|
|
1280
|
+
await _cronScheduler.triggerJob(args.jobId);
|
|
1281
|
+
return JSON.stringify({ action: 'run', jobId: args.jobId, status: 'triggered' });
|
|
1282
|
+
case 'update': {
|
|
1283
|
+
if (!args.jobId) return JSON.stringify({ error: 'jobId required' });
|
|
1284
|
+
_cronScheduler.pauseJob(args.jobId);
|
|
1285
|
+
_cronScheduler.resumeJob(args.jobId);
|
|
1286
|
+
return JSON.stringify({ action: 'update', jobId: args.jobId, status: 'updated' });
|
|
1287
|
+
}
|
|
1288
|
+
default:
|
|
1289
|
+
return JSON.stringify({ error: `Unknown cron action: ${args.action}` });
|
|
1290
|
+
}
|
|
1291
|
+
} catch (e) {
|
|
1292
|
+
return JSON.stringify({ action: args.action, error: e.message });
|
|
1293
|
+
}
|
|
1294
|
+
},
|
|
1295
|
+
},
|
|
1296
|
+
],
|
|
1297
|
+
hooks: {},
|
|
1298
|
+
});
|
|
1299
|
+
|
|
1300
|
+
/**
|
|
1301
|
+
* Sessions Plugin - Session management (list, history, send, spawn)
|
|
1302
|
+
* Distilled from OpenClaw's sessions_list / sessions_history / sessions_send / sessions_spawn tools
|
|
1303
|
+
*/
|
|
1304
|
+
export const SessionsPlugin = new PluginManifest({
|
|
1305
|
+
id: 'builtin-sessions',
|
|
1306
|
+
name: 'Session Management',
|
|
1307
|
+
version: '1.0.0',
|
|
1308
|
+
description: 'List sessions, inspect history, send to sessions, and spawn sub-agent sessions',
|
|
1309
|
+
author: 'Idea Unlimited',
|
|
1310
|
+
tools: [
|
|
1311
|
+
{
|
|
1312
|
+
type: 'function',
|
|
1313
|
+
function: {
|
|
1314
|
+
name: 'sessions_list',
|
|
1315
|
+
description: 'List active sessions with optional message preview.',
|
|
1316
|
+
parameters: {
|
|
1317
|
+
type: 'object',
|
|
1318
|
+
properties: {
|
|
1319
|
+
limit: { type: 'number', description: 'Max sessions to return' },
|
|
1320
|
+
activeMinutes: { type: 'number', description: 'Only sessions active within N minutes' },
|
|
1321
|
+
},
|
|
1322
|
+
},
|
|
1323
|
+
},
|
|
1324
|
+
_executor: async (args) => {
|
|
1325
|
+
// Real implementation: connect to the sessionManager singleton
|
|
1326
|
+
try {
|
|
1327
|
+
if (!_sessionManager) return JSON.stringify({ sessions: [], note: 'SessionManager not initialized' });
|
|
1328
|
+
const sessions = _sessionManager.list({
|
|
1329
|
+
limit: args.limit || 20,
|
|
1330
|
+
});
|
|
1331
|
+
// Filter to recently active sessions
|
|
1332
|
+
const filtered = args.activeMinutes
|
|
1333
|
+
? sessions.filter(s => (Date.now() - new Date(s.lastActiveAt).getTime()) < args.activeMinutes * 60000)
|
|
1334
|
+
: sessions;
|
|
1335
|
+
return JSON.stringify({ sessions: filtered, count: filtered.length });
|
|
1336
|
+
} catch (e) {
|
|
1337
|
+
return JSON.stringify({ sessions: [], error: e.message });
|
|
1338
|
+
}
|
|
1339
|
+
},
|
|
1340
|
+
},
|
|
1341
|
+
{
|
|
1342
|
+
type: 'function',
|
|
1343
|
+
function: {
|
|
1344
|
+
name: 'sessions_send',
|
|
1345
|
+
description: 'Send a message to another session.',
|
|
1346
|
+
parameters: {
|
|
1347
|
+
type: 'object',
|
|
1348
|
+
properties: {
|
|
1349
|
+
sessionKey: { type: 'string', description: 'Target session key or ID' },
|
|
1350
|
+
message: { type: 'string', description: 'Message to send' },
|
|
1351
|
+
},
|
|
1352
|
+
required: ['sessionKey', 'message'],
|
|
1353
|
+
},
|
|
1354
|
+
},
|
|
1355
|
+
_executor: async (args) => {
|
|
1356
|
+
// Real implementation: send message via sessionManager
|
|
1357
|
+
try {
|
|
1358
|
+
if (!_sessionManager) return JSON.stringify({ sent: false, error: 'SessionManager not initialized' });
|
|
1359
|
+
const success = _sessionManager.addMessage(args.sessionKey, {
|
|
1360
|
+
role: 'user',
|
|
1361
|
+
content: args.message,
|
|
1362
|
+
metadata: { source: 'plugin:sessions' },
|
|
1363
|
+
});
|
|
1364
|
+
return JSON.stringify({ sent: success, sessionKey: args.sessionKey });
|
|
1365
|
+
} catch (e) {
|
|
1366
|
+
return JSON.stringify({ sent: false, error: e.message });
|
|
1367
|
+
}
|
|
1368
|
+
},
|
|
1369
|
+
},
|
|
1370
|
+
{
|
|
1371
|
+
type: 'function',
|
|
1372
|
+
function: {
|
|
1373
|
+
name: 'sessions_spawn',
|
|
1374
|
+
description: 'Spawn a new sub-agent session for a task.',
|
|
1375
|
+
parameters: {
|
|
1376
|
+
type: 'object',
|
|
1377
|
+
properties: {
|
|
1378
|
+
task: { type: 'string', description: 'Task description for the sub-agent' },
|
|
1379
|
+
agentId: { type: 'string', description: 'Optional agent ID to use' },
|
|
1380
|
+
label: { type: 'string', description: 'Human-readable label' },
|
|
1381
|
+
},
|
|
1382
|
+
required: ['task'],
|
|
1383
|
+
},
|
|
1384
|
+
},
|
|
1385
|
+
_executor: async (args) => {
|
|
1386
|
+
// Real implementation: create a new session via sessionManager
|
|
1387
|
+
try {
|
|
1388
|
+
if (!_sessionManager) return JSON.stringify({ status: 'error', error: 'SessionManager not initialized' });
|
|
1389
|
+
const session = _sessionManager.getOrCreate({
|
|
1390
|
+
agentId: args.agentId || 'spawn-' + Date.now(),
|
|
1391
|
+
channel: 'task',
|
|
1392
|
+
peerId: args.label || args.task.slice(0, 30),
|
|
1393
|
+
peerKind: 'task',
|
|
1394
|
+
});
|
|
1395
|
+
session.label = args.label || args.task.slice(0, 50);
|
|
1396
|
+
_sessionManager.addMessage(session.sessionKey, {
|
|
1397
|
+
role: 'system',
|
|
1398
|
+
content: `Sub-agent task: ${args.task}`,
|
|
1399
|
+
});
|
|
1400
|
+
return JSON.stringify({ status: 'spawned', sessionKey: session.sessionKey, sessionId: session.id, task: args.task });
|
|
1401
|
+
} catch (e) {
|
|
1402
|
+
return JSON.stringify({ status: 'error', error: e.message });
|
|
1403
|
+
}
|
|
1404
|
+
},
|
|
1405
|
+
},
|
|
1406
|
+
],
|
|
1407
|
+
hooks: {},
|
|
1408
|
+
});
|
|
1409
|
+
|
|
1410
|
+
/**
|
|
1411
|
+
* Subagents Plugin - Multi-agent coordination
|
|
1412
|
+
* Distilled from OpenClaw's subagents tool
|
|
1413
|
+
*/
|
|
1414
|
+
export const SubagentsPlugin = new PluginManifest({
|
|
1415
|
+
id: 'builtin-subagents',
|
|
1416
|
+
name: 'Sub-Agent Coordination',
|
|
1417
|
+
version: '1.0.0',
|
|
1418
|
+
description: 'List, steer, and manage sub-agent runs for multi-agent workflows',
|
|
1419
|
+
author: 'Idea Unlimited',
|
|
1420
|
+
tools: [
|
|
1421
|
+
{
|
|
1422
|
+
type: 'function',
|
|
1423
|
+
function: {
|
|
1424
|
+
name: 'subagents',
|
|
1425
|
+
description: 'Manage sub-agent runs: list active runs, steer behavior, or kill a run.',
|
|
1426
|
+
parameters: {
|
|
1427
|
+
type: 'object',
|
|
1428
|
+
properties: {
|
|
1429
|
+
action: { type: 'string', enum: ['list', 'steer', 'kill'], description: 'Sub-agent action' },
|
|
1430
|
+
runId: { type: 'string', description: 'Run ID (for steer/kill)' },
|
|
1431
|
+
instruction: { type: 'string', description: 'Steering instruction (for steer)' },
|
|
1432
|
+
},
|
|
1433
|
+
required: ['action'],
|
|
1434
|
+
},
|
|
1435
|
+
},
|
|
1436
|
+
_executor: async (args) => {
|
|
1437
|
+
// Real implementation: sub-agent management
|
|
1438
|
+
try {
|
|
1439
|
+
if (!_sessionManager) return JSON.stringify({ error: 'SessionManager not initialized' });
|
|
1440
|
+
switch (args.action) {
|
|
1441
|
+
case 'list': {
|
|
1442
|
+
// List all task-type active sessions (running as sub-agents)
|
|
1443
|
+
const sessions = _sessionManager.list({ limit: 50 });
|
|
1444
|
+
const taskSessions = sessions.filter(s => s.peerKind === 'task');
|
|
1445
|
+
return JSON.stringify({ runs: taskSessions.map(s => ({ id: s.sessionKey, agent: s.agentId, label: s.label, state: s.state, messages: s.messageCount })), count: taskSessions.length });
|
|
1446
|
+
}
|
|
1447
|
+
case 'steer': {
|
|
1448
|
+
if (!args.runId || !args.instruction) return JSON.stringify({ error: 'runId and instruction required' });
|
|
1449
|
+
_sessionManager.addMessage(args.runId, { role: 'user', content: `[STEER] ${args.instruction}` });
|
|
1450
|
+
return JSON.stringify({ action: 'steer', runId: args.runId, status: 'instruction_sent' });
|
|
1451
|
+
}
|
|
1452
|
+
case 'kill': {
|
|
1453
|
+
if (!args.runId) return JSON.stringify({ error: 'runId required' });
|
|
1454
|
+
_sessionManager.archive(args.runId);
|
|
1455
|
+
return JSON.stringify({ action: 'kill', runId: args.runId, status: 'archived' });
|
|
1456
|
+
}
|
|
1457
|
+
default:
|
|
1458
|
+
return JSON.stringify({ error: `Unknown action: ${args.action}` });
|
|
1459
|
+
}
|
|
1460
|
+
} catch (e) {
|
|
1461
|
+
return JSON.stringify({ action: args.action, error: e.message });
|
|
1462
|
+
}
|
|
1463
|
+
},
|
|
1464
|
+
},
|
|
1465
|
+
],
|
|
1466
|
+
hooks: {},
|
|
1467
|
+
});
|
|
1468
|
+
|
|
1469
|
+
/**
|
|
1470
|
+
* Nodes Plugin - Device node management (paired nodes, cameras, notifications)
|
|
1471
|
+
* Distilled from OpenClaw's nodes tool
|
|
1472
|
+
*/
|
|
1473
|
+
export const NodesPlugin = new PluginManifest({
|
|
1474
|
+
id: 'builtin-nodes',
|
|
1475
|
+
name: 'Device Nodes',
|
|
1476
|
+
version: '1.0.0',
|
|
1477
|
+
description: 'Discover paired nodes, send notifications, capture camera/screen, and run commands on remote devices',
|
|
1478
|
+
author: 'Idea Unlimited',
|
|
1479
|
+
tools: [
|
|
1480
|
+
{
|
|
1481
|
+
type: 'function',
|
|
1482
|
+
function: {
|
|
1483
|
+
name: 'nodes',
|
|
1484
|
+
description: 'Manage paired device nodes: status, describe, notify, camera, screen capture, and more.',
|
|
1485
|
+
parameters: {
|
|
1486
|
+
type: 'object',
|
|
1487
|
+
properties: {
|
|
1488
|
+
action: { type: 'string', enum: ['status', 'describe', 'notify', 'run', 'camera_snap', 'screen_record', 'location_get'], description: 'Node action' },
|
|
1489
|
+
node: { type: 'string', description: 'Target node ID or name' },
|
|
1490
|
+
message: { type: 'string', description: 'Notification message (for notify)' },
|
|
1491
|
+
command: { type: 'string', description: 'Command to run (for run)' },
|
|
1492
|
+
},
|
|
1493
|
+
required: ['action'],
|
|
1494
|
+
},
|
|
1495
|
+
},
|
|
1496
|
+
_executor: async (args) => {
|
|
1497
|
+
// Real implementation: device node management (based on system info)
|
|
1498
|
+
try {
|
|
1499
|
+
switch (args.action) {
|
|
1500
|
+
case 'status': {
|
|
1501
|
+
// Get local machine status as node info
|
|
1502
|
+
const os = await import('os');
|
|
1503
|
+
return JSON.stringify({
|
|
1504
|
+
action: 'status',
|
|
1505
|
+
nodes: [{
|
|
1506
|
+
id: 'local',
|
|
1507
|
+
hostname: os.default.hostname(),
|
|
1508
|
+
platform: os.default.platform(),
|
|
1509
|
+
arch: os.default.arch(),
|
|
1510
|
+
cpus: os.default.cpus().length,
|
|
1511
|
+
totalMemory: Math.round(os.default.totalmem() / 1024 / 1024) + 'MB',
|
|
1512
|
+
freeMemory: Math.round(os.default.freemem() / 1024 / 1024) + 'MB',
|
|
1513
|
+
uptime: Math.round(os.default.uptime() / 3600) + 'h',
|
|
1514
|
+
loadAvg: os.default.loadavg().map(l => l.toFixed(2)),
|
|
1515
|
+
}],
|
|
1516
|
+
});
|
|
1517
|
+
}
|
|
1518
|
+
case 'describe': {
|
|
1519
|
+
const os = await import('os');
|
|
1520
|
+
return JSON.stringify({ node: args.node || 'local', hostname: os.default.hostname(), platform: os.default.platform(), networkInterfaces: Object.keys(os.default.networkInterfaces()) });
|
|
1521
|
+
}
|
|
1522
|
+
case 'notify': {
|
|
1523
|
+
// Send notification (log it)
|
|
1524
|
+
console.log(`🔔 [Node Notification] ${args.node || 'local'}: ${args.message}`);
|
|
1525
|
+
return JSON.stringify({ action: 'notify', node: args.node, status: 'sent', message: args.message });
|
|
1526
|
+
}
|
|
1527
|
+
case 'run': {
|
|
1528
|
+
if (!args.command) return JSON.stringify({ error: 'command required' });
|
|
1529
|
+
const { stdout, stderr } = await execAsync(args.command, { cwd: WORKSPACE_DIR, timeout: 15000 });
|
|
1530
|
+
return JSON.stringify({ action: 'run', node: args.node, stdout: stdout.slice(0, 5000), stderr: stderr?.slice(0, 2000) || '' });
|
|
1531
|
+
}
|
|
1532
|
+
default:
|
|
1533
|
+
return JSON.stringify({ action: args.action, error: `Action ${args.action} not available for nodes` });
|
|
1534
|
+
}
|
|
1535
|
+
} catch (e) {
|
|
1536
|
+
return JSON.stringify({ action: args.action, error: e.message });
|
|
1537
|
+
}
|
|
1538
|
+
},
|
|
1539
|
+
},
|
|
1540
|
+
],
|
|
1541
|
+
hooks: {},
|
|
1542
|
+
});
|
|
1543
|
+
|
|
1544
|
+
/**
|
|
1545
|
+
* Gateway Plugin - Gateway process management
|
|
1546
|
+
* Distilled from OpenClaw's gateway tool
|
|
1547
|
+
*/
|
|
1548
|
+
export const GatewayPlugin = new PluginManifest({
|
|
1549
|
+
id: 'builtin-gateway',
|
|
1550
|
+
name: 'Gateway Management',
|
|
1551
|
+
version: '1.0.0',
|
|
1552
|
+
description: 'Restart, configure, and update the running gateway process',
|
|
1553
|
+
author: 'Idea Unlimited',
|
|
1554
|
+
tools: [
|
|
1555
|
+
{
|
|
1556
|
+
type: 'function',
|
|
1557
|
+
function: {
|
|
1558
|
+
name: 'gateway',
|
|
1559
|
+
description: 'Manage the gateway: restart, get/apply/patch config, or run updates.',
|
|
1560
|
+
parameters: {
|
|
1561
|
+
type: 'object',
|
|
1562
|
+
properties: {
|
|
1563
|
+
action: { type: 'string', enum: ['restart', 'config.get', 'config.apply', 'config.patch', 'update.run'], description: 'Gateway action' },
|
|
1564
|
+
raw: { type: 'string', description: 'Config content (for config.apply / config.patch)' },
|
|
1565
|
+
delayMs: { type: 'number', description: 'Delay before restart (default: 2000)' },
|
|
1566
|
+
},
|
|
1567
|
+
required: ['action'],
|
|
1568
|
+
},
|
|
1569
|
+
},
|
|
1570
|
+
_executor: async (args) => {
|
|
1571
|
+
// Real implementation: Gateway management (config files + process management)
|
|
1572
|
+
try {
|
|
1573
|
+
const configPath = path.join(DATA_DIR, 'gateway-config.json');
|
|
1574
|
+
switch (args.action) {
|
|
1575
|
+
case 'config.get': {
|
|
1576
|
+
if (existsSync(configPath)) {
|
|
1577
|
+
const content = await fs.readFile(configPath, 'utf-8');
|
|
1578
|
+
return JSON.stringify({ action: 'config.get', config: JSON.parse(content) });
|
|
1579
|
+
}
|
|
1580
|
+
return JSON.stringify({ action: 'config.get', config: {}, note: 'No gateway config found' });
|
|
1581
|
+
}
|
|
1582
|
+
case 'config.apply': {
|
|
1583
|
+
if (!args.raw) return JSON.stringify({ error: 'raw config content required' });
|
|
1584
|
+
await fs.writeFile(configPath, args.raw, 'utf-8');
|
|
1585
|
+
return JSON.stringify({ action: 'config.apply', status: 'written', path: configPath });
|
|
1586
|
+
}
|
|
1587
|
+
case 'config.patch': {
|
|
1588
|
+
let existing = {};
|
|
1589
|
+
if (existsSync(configPath)) {
|
|
1590
|
+
existing = JSON.parse(await fs.readFile(configPath, 'utf-8'));
|
|
1591
|
+
}
|
|
1592
|
+
const patch = typeof args.raw === 'string' ? JSON.parse(args.raw) : args.raw;
|
|
1593
|
+
const merged = { ...existing, ...patch };
|
|
1594
|
+
await fs.writeFile(configPath, JSON.stringify(merged, null, 2), 'utf-8');
|
|
1595
|
+
return JSON.stringify({ action: 'config.patch', status: 'patched', config: merged });
|
|
1596
|
+
}
|
|
1597
|
+
case 'restart': {
|
|
1598
|
+
console.log(`🔄 [Gateway] Restart requested (delay: ${args.delayMs || 2000}ms)`);
|
|
1599
|
+
return JSON.stringify({ action: 'restart', status: 'scheduled', delayMs: args.delayMs || 2000 });
|
|
1600
|
+
}
|
|
1601
|
+
case 'update.run': {
|
|
1602
|
+
const { stdout } = await execAsync('git pull 2>&1 || echo "not a git repo"', { cwd: process.cwd(), timeout: 15000 });
|
|
1603
|
+
return JSON.stringify({ action: 'update.run', output: stdout.slice(0, 3000) });
|
|
1604
|
+
}
|
|
1605
|
+
default:
|
|
1606
|
+
return JSON.stringify({ error: `Unknown gateway action: ${args.action}` });
|
|
1607
|
+
}
|
|
1608
|
+
} catch (e) {
|
|
1609
|
+
return JSON.stringify({ action: args.action, error: e.message });
|
|
1610
|
+
}
|
|
1611
|
+
},
|
|
1612
|
+
},
|
|
1613
|
+
],
|
|
1614
|
+
hooks: {},
|
|
1615
|
+
});
|
|
1616
|
+
|
|
1617
|
+
/**
|
|
1618
|
+
* Lobster Plugin - Typed workflow runtime with resumable approvals
|
|
1619
|
+
* Distilled from OpenClaw's Lobster extension
|
|
1620
|
+
*/
|
|
1621
|
+
export const LobsterPlugin = new PluginManifest({
|
|
1622
|
+
id: 'builtin-lobster',
|
|
1623
|
+
name: 'Lobster Workflows',
|
|
1624
|
+
version: '1.0.0',
|
|
1625
|
+
description: 'Typed workflow runtime — composable pipelines with approval gates and resumable state',
|
|
1626
|
+
author: 'Idea Unlimited',
|
|
1627
|
+
configSchema: {
|
|
1628
|
+
lobsterPath: { type: 'string', description: 'Path to Lobster CLI binary' },
|
|
1629
|
+
},
|
|
1630
|
+
tools: [
|
|
1631
|
+
{
|
|
1632
|
+
type: 'function',
|
|
1633
|
+
function: {
|
|
1634
|
+
name: 'lobster',
|
|
1635
|
+
description: 'Run or resume a Lobster workflow pipeline (deterministic, with approval checkpoints).',
|
|
1636
|
+
parameters: {
|
|
1637
|
+
type: 'object',
|
|
1638
|
+
properties: {
|
|
1639
|
+
action: { type: 'string', enum: ['run', 'resume'], description: 'Workflow action' },
|
|
1640
|
+
pipeline: { type: 'string', description: 'Pipeline command or file path (for run)' },
|
|
1641
|
+
token: { type: 'string', description: 'Resume token (for resume)' },
|
|
1642
|
+
approve: { type: 'boolean', description: 'Approve the halted step (for resume)' },
|
|
1643
|
+
argsJson: { type: 'string', description: 'JSON args for workflow files' },
|
|
1644
|
+
timeoutMs: { type: 'number', description: 'Timeout in ms (default: 20000)' },
|
|
1645
|
+
},
|
|
1646
|
+
required: ['action'],
|
|
1647
|
+
},
|
|
1648
|
+
},
|
|
1649
|
+
_executor: async (args) => {
|
|
1650
|
+
// Real implementation: workflow pipeline execution (shell-based)
|
|
1651
|
+
try {
|
|
1652
|
+
switch (args.action) {
|
|
1653
|
+
case 'run': {
|
|
1654
|
+
if (!args.pipeline) return JSON.stringify({ error: 'pipeline command required' });
|
|
1655
|
+
const timeout = args.timeoutMs || 20000;
|
|
1656
|
+
// Try to execute the pipeline command
|
|
1657
|
+
const pipelineCmd = args.argsJson
|
|
1658
|
+
? `${args.pipeline} '${args.argsJson}'`
|
|
1659
|
+
: args.pipeline;
|
|
1660
|
+
const { stdout, stderr } = await execAsync(pipelineCmd, {
|
|
1661
|
+
cwd: WORKSPACE_DIR, timeout, maxBuffer: 2 * 1024 * 1024,
|
|
1662
|
+
});
|
|
1663
|
+
return JSON.stringify({ action: 'run', pipeline: args.pipeline, stdout: stdout.slice(0, 10000), stderr: stderr?.slice(0, 3000) || '', status: 'completed' });
|
|
1664
|
+
}
|
|
1665
|
+
case 'resume': {
|
|
1666
|
+
return JSON.stringify({ action: 'resume', token: args.token, approved: args.approve, status: 'resume_not_implemented_yet' });
|
|
1667
|
+
}
|
|
1668
|
+
default:
|
|
1669
|
+
return JSON.stringify({ error: `Unknown lobster action: ${args.action}` });
|
|
1670
|
+
}
|
|
1671
|
+
} catch (e) {
|
|
1672
|
+
return JSON.stringify({ action: args.action, error: e.message });
|
|
1673
|
+
}
|
|
1674
|
+
},
|
|
1675
|
+
},
|
|
1676
|
+
],
|
|
1677
|
+
hooks: {},
|
|
1678
|
+
});
|
|
1679
|
+
|
|
1680
|
+
/**
|
|
1681
|
+
* LLM Task Plugin - JSON-only LLM step for structured workflow output
|
|
1682
|
+
* Distilled from OpenClaw's llm-task extension
|
|
1683
|
+
*/
|
|
1684
|
+
export const LlmTaskPlugin = new PluginManifest({
|
|
1685
|
+
id: 'builtin-llm-task',
|
|
1686
|
+
name: 'LLM Task',
|
|
1687
|
+
version: '1.0.0',
|
|
1688
|
+
description: 'Run JSON-only LLM tasks for structured output (classification, summarization, drafting) with optional schema validation',
|
|
1689
|
+
author: 'Idea Unlimited',
|
|
1690
|
+
configSchema: {
|
|
1691
|
+
defaultProvider: { type: 'string', description: 'Default LLM provider' },
|
|
1692
|
+
defaultModel: { type: 'string', description: 'Default model ID' },
|
|
1693
|
+
maxTokens: { type: 'number', default: 800, description: 'Max output tokens' },
|
|
1694
|
+
},
|
|
1695
|
+
tools: [
|
|
1696
|
+
{
|
|
1697
|
+
type: 'function',
|
|
1698
|
+
function: {
|
|
1699
|
+
name: 'llm_task',
|
|
1700
|
+
description: 'Run a JSON-only LLM task and return structured output, optionally validated against a JSON Schema.',
|
|
1701
|
+
parameters: {
|
|
1702
|
+
type: 'object',
|
|
1703
|
+
properties: {
|
|
1704
|
+
prompt: { type: 'string', description: 'Task prompt' },
|
|
1705
|
+
input: { type: 'string', description: 'Optional input data (JSON string)' },
|
|
1706
|
+
schema: { type: 'object', description: 'Optional JSON Schema for output validation' },
|
|
1707
|
+
temperature: { type: 'number', description: 'Temperature (0-2)' },
|
|
1708
|
+
},
|
|
1709
|
+
required: ['prompt'],
|
|
1710
|
+
},
|
|
1711
|
+
},
|
|
1712
|
+
_executor: async (args) => {
|
|
1713
|
+
// Real implementation: use LLM for structured output tasks
|
|
1714
|
+
try {
|
|
1715
|
+
if (!_llmClient) return JSON.stringify({ status: 'error', error: 'LLMClient not initialized' });
|
|
1716
|
+
// TODO: structured output plugin needs access to provider registry via Company instance
|
|
1717
|
+
return JSON.stringify({ status: 'error', error: 'Structured output plugin not yet connected to provider registry' });
|
|
1718
|
+
|
|
1719
|
+
const systemMsg = args.schema
|
|
1720
|
+
? `You are a task execution assistant. You MUST respond in valid JSON only. Your output must conform to this JSON Schema:\n${JSON.stringify(args.schema)}\nDo not include any text outside the JSON.`
|
|
1721
|
+
: 'You are a task execution assistant. You MUST respond in valid JSON only. Do not include any text outside the JSON.';
|
|
1722
|
+
|
|
1723
|
+
const userMsg = args.input
|
|
1724
|
+
? `Task: ${args.prompt}\n\nInput data:\n${args.input}`
|
|
1725
|
+
: `Task: ${args.prompt}`;
|
|
1726
|
+
|
|
1727
|
+
const response = await _llmClient.chat(provider, [
|
|
1728
|
+
{ role: 'system', content: systemMsg },
|
|
1729
|
+
{ role: 'user', content: userMsg },
|
|
1730
|
+
], { temperature: args.temperature ?? 0.3, maxTokens: 800 });
|
|
1731
|
+
|
|
1732
|
+
// Parse JSON output
|
|
1733
|
+
let output;
|
|
1734
|
+
try {
|
|
1735
|
+
const cleaned = response.content.replace(/```json\n?/g, '').replace(/```/g, '').trim();
|
|
1736
|
+
output = JSON.parse(cleaned);
|
|
1737
|
+
} catch {
|
|
1738
|
+
output = { raw: response.content };
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
return JSON.stringify({ status: 'completed', output, usage: response.usage });
|
|
1742
|
+
} catch (e) {
|
|
1743
|
+
return JSON.stringify({ status: 'error', error: e.message });
|
|
1744
|
+
}
|
|
1745
|
+
},
|
|
1746
|
+
},
|
|
1747
|
+
],
|
|
1748
|
+
hooks: {},
|
|
1749
|
+
});
|
|
1750
|
+
|
|
1751
|
+
/**
|
|
1752
|
+
* Diffs Plugin - Read-only diff viewer and file renderer
|
|
1753
|
+
* Distilled from OpenClaw's diffs extension
|
|
1754
|
+
*/
|
|
1755
|
+
export const DiffsPlugin = new PluginManifest({
|
|
1756
|
+
id: 'builtin-diffs',
|
|
1757
|
+
name: 'Diff Viewer',
|
|
1758
|
+
version: '1.0.0',
|
|
1759
|
+
description: 'Render before/after text or unified patches as interactive diff views or PNG/PDF files',
|
|
1760
|
+
author: 'Idea Unlimited',
|
|
1761
|
+
configSchema: {
|
|
1762
|
+
theme: { type: 'string', default: 'dark', enum: ['dark', 'light'], description: 'Default theme' },
|
|
1763
|
+
layout: { type: 'string', default: 'unified', enum: ['unified', 'split'], description: 'Default layout' },
|
|
1764
|
+
},
|
|
1765
|
+
tools: [
|
|
1766
|
+
{
|
|
1767
|
+
type: 'function',
|
|
1768
|
+
function: {
|
|
1769
|
+
name: 'diffs',
|
|
1770
|
+
description: 'Create a read-only diff viewer from before/after text or a unified patch.',
|
|
1771
|
+
parameters: {
|
|
1772
|
+
type: 'object',
|
|
1773
|
+
properties: {
|
|
1774
|
+
before: { type: 'string', description: 'Original text content' },
|
|
1775
|
+
after: { type: 'string', description: 'Modified text content' },
|
|
1776
|
+
patch: { type: 'string', description: 'Unified patch (alternative to before/after)' },
|
|
1777
|
+
path: { type: 'string', description: 'Display file name' },
|
|
1778
|
+
mode: { type: 'string', enum: ['view', 'file', 'both'], description: 'Output mode (default: view)' },
|
|
1779
|
+
fileFormat: { type: 'string', enum: ['png', 'pdf'], description: 'Rendered file format (default: png)' },
|
|
1780
|
+
},
|
|
1781
|
+
},
|
|
1782
|
+
},
|
|
1783
|
+
_executor: async (args) => {
|
|
1784
|
+
// Real implementation: generate diff and write to file
|
|
1785
|
+
try {
|
|
1786
|
+
let diffContent = '';
|
|
1787
|
+
if (args.patch) {
|
|
1788
|
+
diffContent = args.patch;
|
|
1789
|
+
} else if (args.before !== undefined && args.after !== undefined) {
|
|
1790
|
+
// Generate a simplified unified diff
|
|
1791
|
+
const beforeLines = (args.before || '').split('\n');
|
|
1792
|
+
const afterLines = (args.after || '').split('\n');
|
|
1793
|
+
const diffLines = [`--- ${args.path || 'a/file'}`, `+++ ${args.path || 'b/file'}`];
|
|
1794
|
+
const maxLen = Math.max(beforeLines.length, afterLines.length);
|
|
1795
|
+
for (let i = 0; i < maxLen; i++) {
|
|
1796
|
+
if (i < beforeLines.length && i < afterLines.length) {
|
|
1797
|
+
if (beforeLines[i] !== afterLines[i]) {
|
|
1798
|
+
diffLines.push(`-${beforeLines[i]}`);
|
|
1799
|
+
diffLines.push(`+${afterLines[i]}`);
|
|
1800
|
+
} else {
|
|
1801
|
+
diffLines.push(` ${beforeLines[i]}`);
|
|
1802
|
+
}
|
|
1803
|
+
} else if (i < beforeLines.length) {
|
|
1804
|
+
diffLines.push(`-${beforeLines[i]}`);
|
|
1805
|
+
} else {
|
|
1806
|
+
diffLines.push(`+${afterLines[i]}`);
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
diffContent = diffLines.join('\n');
|
|
1810
|
+
}
|
|
1811
|
+
// Write to file
|
|
1812
|
+
const mode = args.mode || 'view';
|
|
1813
|
+
const result = { status: 'generated', mode, diffLength: diffContent.length };
|
|
1814
|
+
if (mode === 'file' || mode === 'both') {
|
|
1815
|
+
const diffDir = path.join(DATA_DIR, 'diffs');
|
|
1816
|
+
if (!existsSync(diffDir)) mkdirSync(diffDir, { recursive: true });
|
|
1817
|
+
const filename = `diff-${Date.now()}.${args.fileFormat || 'txt'}`;
|
|
1818
|
+
const filePath = path.join(diffDir, filename);
|
|
1819
|
+
await fs.writeFile(filePath, diffContent, 'utf-8');
|
|
1820
|
+
result.filePath = filePath;
|
|
1821
|
+
}
|
|
1822
|
+
if (mode === 'view' || mode === 'both') {
|
|
1823
|
+
result.diff = diffContent.slice(0, 10000);
|
|
1824
|
+
}
|
|
1825
|
+
return JSON.stringify(result);
|
|
1826
|
+
} catch (e) {
|
|
1827
|
+
return JSON.stringify({ status: 'error', error: e.message });
|
|
1828
|
+
}
|
|
1829
|
+
},
|
|
1830
|
+
},
|
|
1831
|
+
],
|
|
1832
|
+
hooks: {},
|
|
1833
|
+
});
|
|
1834
|
+
|
|
1835
|
+
/**
|
|
1836
|
+
* Firecrawl Plugin - Anti-bot web extraction fallback
|
|
1837
|
+
* Distilled from OpenClaw's Firecrawl integration
|
|
1838
|
+
*/
|
|
1839
|
+
export const FirecrawlPlugin = new PluginManifest({
|
|
1840
|
+
id: 'builtin-firecrawl',
|
|
1841
|
+
name: 'Firecrawl',
|
|
1842
|
+
version: '1.0.0',
|
|
1843
|
+
description: 'Anti-bot web extraction with cached content — fallback for web_fetch on JS-heavy or protected sites',
|
|
1844
|
+
author: 'Idea Unlimited',
|
|
1845
|
+
configSchema: {
|
|
1846
|
+
apiKey: { type: 'string', description: 'Firecrawl API key' },
|
|
1847
|
+
baseUrl: { type: 'string', default: 'https://api.firecrawl.dev', description: 'Firecrawl API base URL' },
|
|
1848
|
+
maxAgeMs: { type: 'number', default: 172800000, description: 'Cache TTL in ms (default: 2 days)' },
|
|
1849
|
+
},
|
|
1850
|
+
tools: [
|
|
1851
|
+
{
|
|
1852
|
+
type: 'function',
|
|
1853
|
+
function: {
|
|
1854
|
+
name: 'firecrawl_extract',
|
|
1855
|
+
description: 'Extract content from a URL using Firecrawl (anti-bot, JS rendering, cached).',
|
|
1856
|
+
parameters: {
|
|
1857
|
+
type: 'object',
|
|
1858
|
+
properties: {
|
|
1859
|
+
url: { type: 'string', description: 'URL to extract content from' },
|
|
1860
|
+
onlyMainContent: { type: 'boolean', description: 'Extract only main content (default: true)' },
|
|
1861
|
+
},
|
|
1862
|
+
required: ['url'],
|
|
1863
|
+
},
|
|
1864
|
+
},
|
|
1865
|
+
_executor: async (args, config) => {
|
|
1866
|
+
// Real implementation: call Firecrawl API if key available, otherwise fall back to fetch
|
|
1867
|
+
try {
|
|
1868
|
+
const apiKey = config.apiKey;
|
|
1869
|
+
const baseUrl = config.baseUrl || 'https://api.firecrawl.dev';
|
|
1870
|
+
if (apiKey) {
|
|
1871
|
+
// Call the Firecrawl API
|
|
1872
|
+
const res = await fetch(`${baseUrl}/v0/scrape`, {
|
|
1873
|
+
method: 'POST',
|
|
1874
|
+
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` },
|
|
1875
|
+
body: JSON.stringify({ url: args.url, onlyMainContent: args.onlyMainContent !== false }),
|
|
1876
|
+
signal: AbortSignal.timeout(30000),
|
|
1877
|
+
});
|
|
1878
|
+
const data = await res.json();
|
|
1879
|
+
if (data.success) {
|
|
1880
|
+
return JSON.stringify({ url: args.url, content: (data.data?.markdown || data.data?.content || '').slice(0, 15000), title: data.data?.metadata?.title || '', method: 'firecrawl' });
|
|
1881
|
+
}
|
|
1882
|
+
return JSON.stringify({ url: args.url, error: data.error || 'Firecrawl request failed', method: 'firecrawl' });
|
|
1883
|
+
}
|
|
1884
|
+
// Fall back to plain fetch
|
|
1885
|
+
const controller = new AbortController();
|
|
1886
|
+
const timeout = setTimeout(() => controller.abort(), 15000);
|
|
1887
|
+
const res = await fetch(args.url, {
|
|
1888
|
+
headers: { 'User-Agent': 'Mozilla/5.0 (compatible; AIEnterprise/1.0)' },
|
|
1889
|
+
signal: controller.signal,
|
|
1890
|
+
});
|
|
1891
|
+
clearTimeout(timeout);
|
|
1892
|
+
const html = await res.text();
|
|
1893
|
+
const text = html.replace(/<script[\s\S]*?<\/script>/gi, '').replace(/<style[\s\S]*?<\/style>/gi, '').replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
|
|
1894
|
+
return JSON.stringify({ url: args.url, content: text.slice(0, 15000), method: 'fetch-fallback', note: 'Set Firecrawl API key for better anti-bot extraction' });
|
|
1895
|
+
} catch (e) {
|
|
1896
|
+
return JSON.stringify({ url: args.url, error: e.message });
|
|
1897
|
+
}
|
|
1898
|
+
},
|
|
1899
|
+
},
|
|
1900
|
+
],
|
|
1901
|
+
hooks: {},
|
|
1902
|
+
});
|
|
1903
|
+
|
|
1904
|
+
/**
|
|
1905
|
+
* Bird / xurl Plugin - X/Twitter CLI (post, reply, search, DM, etc.)
|
|
1906
|
+
* Distilled from OpenClaw's xurl skill
|
|
1907
|
+
*/
|
|
1908
|
+
export const BirdPlugin = new PluginManifest({
|
|
1909
|
+
id: 'builtin-bird',
|
|
1910
|
+
name: 'Bird (X/Twitter)',
|
|
1911
|
+
version: '1.0.0',
|
|
1912
|
+
description: 'X/Twitter CLI — post tweets, reply, quote, search, read timelines, manage followers, send DMs, and upload media without a browser',
|
|
1913
|
+
author: 'Idea Unlimited',
|
|
1914
|
+
configSchema: {
|
|
1915
|
+
cliPath: { type: 'string', default: 'xurl', description: 'Path to xurl CLI binary' },
|
|
1916
|
+
},
|
|
1917
|
+
tools: [
|
|
1918
|
+
{
|
|
1919
|
+
type: 'function',
|
|
1920
|
+
function: {
|
|
1921
|
+
name: 'bird_post',
|
|
1922
|
+
description: 'Post a tweet on X/Twitter.',
|
|
1923
|
+
parameters: {
|
|
1924
|
+
type: 'object',
|
|
1925
|
+
properties: {
|
|
1926
|
+
text: { type: 'string', description: 'Tweet text' },
|
|
1927
|
+
replyTo: { type: 'string', description: 'Post ID to reply to (optional)' },
|
|
1928
|
+
mediaId: { type: 'string', description: 'Media ID to attach (optional)' },
|
|
1929
|
+
},
|
|
1930
|
+
required: ['text'],
|
|
1931
|
+
},
|
|
1932
|
+
},
|
|
1933
|
+
_executor: async (args) => {
|
|
1934
|
+
// Real implementation: try posting via xurl CLI, fall back to logging
|
|
1935
|
+
try {
|
|
1936
|
+
const cmd = args.replyTo
|
|
1937
|
+
? `xurl post --text "${args.text.replace(/"/g, '\\"')}" --reply-to ${args.replyTo}`
|
|
1938
|
+
: `xurl post --text "${args.text.replace(/"/g, '\\"')}"`;
|
|
1939
|
+
const { stdout } = await execAsync(cmd, { timeout: 15000 });
|
|
1940
|
+
return JSON.stringify({ status: 'posted', output: stdout.slice(0, 2000) });
|
|
1941
|
+
} catch (e) {
|
|
1942
|
+
// xurl unavailable, log and return
|
|
1943
|
+
console.log(`🐦 [Bird] Post: ${args.text.slice(0, 100)}`);
|
|
1944
|
+
return JSON.stringify({ status: 'logged', text: args.text.slice(0, 280), error: `xurl CLI not available: ${e.message}. Install xurl for X/Twitter posting.` });
|
|
1945
|
+
}
|
|
1946
|
+
},
|
|
1947
|
+
},
|
|
1948
|
+
{
|
|
1949
|
+
type: 'function',
|
|
1950
|
+
function: {
|
|
1951
|
+
name: 'bird_search',
|
|
1952
|
+
description: 'Search recent posts on X/Twitter.',
|
|
1953
|
+
parameters: {
|
|
1954
|
+
type: 'object',
|
|
1955
|
+
properties: {
|
|
1956
|
+
query: { type: 'string', description: 'Search query' },
|
|
1957
|
+
limit: { type: 'number', description: 'Max results (default: 10)' },
|
|
1958
|
+
},
|
|
1959
|
+
required: ['query'],
|
|
1960
|
+
},
|
|
1961
|
+
},
|
|
1962
|
+
_executor: async (args) => {
|
|
1963
|
+
try {
|
|
1964
|
+
const { stdout } = await execAsync(`xurl search --query "${args.query.replace(/"/g, '\\"')}" --limit ${args.limit || 10}`, { timeout: 15000 });
|
|
1965
|
+
return JSON.stringify({ query: args.query, output: stdout.slice(0, 5000), method: 'xurl' });
|
|
1966
|
+
} catch (e) {
|
|
1967
|
+
return JSON.stringify({ query: args.query, results: [], error: `xurl CLI not available: ${e.message}` });
|
|
1968
|
+
}
|
|
1969
|
+
},
|
|
1970
|
+
},
|
|
1971
|
+
{
|
|
1972
|
+
type: 'function',
|
|
1973
|
+
function: {
|
|
1974
|
+
name: 'bird_timeline',
|
|
1975
|
+
description: 'Read your X/Twitter home timeline or mentions.',
|
|
1976
|
+
parameters: {
|
|
1977
|
+
type: 'object',
|
|
1978
|
+
properties: {
|
|
1979
|
+
type: { type: 'string', enum: ['home', 'mentions'], description: 'Timeline type (default: home)' },
|
|
1980
|
+
limit: { type: 'number', description: 'Max posts to return (default: 20)' },
|
|
1981
|
+
},
|
|
1982
|
+
},
|
|
1983
|
+
},
|
|
1984
|
+
_executor: async (args) => {
|
|
1985
|
+
try {
|
|
1986
|
+
const type = args.type || 'home';
|
|
1987
|
+
const { stdout } = await execAsync(`xurl timeline --type ${type} --limit ${args.limit || 20}`, { timeout: 15000 });
|
|
1988
|
+
return JSON.stringify({ type, output: stdout.slice(0, 5000), method: 'xurl' });
|
|
1989
|
+
} catch (e) {
|
|
1990
|
+
return JSON.stringify({ type: args.type || 'home', posts: [], error: `xurl CLI not available: ${e.message}` });
|
|
1991
|
+
}
|
|
1992
|
+
},
|
|
1993
|
+
},
|
|
1994
|
+
{
|
|
1995
|
+
type: 'function',
|
|
1996
|
+
function: {
|
|
1997
|
+
name: 'bird_dm',
|
|
1998
|
+
description: 'Send a direct message on X/Twitter.',
|
|
1999
|
+
parameters: {
|
|
2000
|
+
type: 'object',
|
|
2001
|
+
properties: {
|
|
2002
|
+
to: { type: 'string', description: 'Username to DM (e.g. @user)' },
|
|
2003
|
+
text: { type: 'string', description: 'Message text' },
|
|
2004
|
+
},
|
|
2005
|
+
required: ['to', 'text'],
|
|
2006
|
+
},
|
|
2007
|
+
},
|
|
2008
|
+
_executor: async (args) => {
|
|
2009
|
+
try {
|
|
2010
|
+
const { stdout } = await execAsync(`xurl dm --to "${args.to}" --text "${args.text.replace(/"/g, '\\"')}"`, { timeout: 15000 });
|
|
2011
|
+
return JSON.stringify({ status: 'sent', to: args.to, output: stdout.slice(0, 2000) });
|
|
2012
|
+
} catch (e) {
|
|
2013
|
+
console.log(`🐦 [Bird DM] To ${args.to}: ${args.text.slice(0, 100)}`);
|
|
2014
|
+
return JSON.stringify({ status: 'logged', to: args.to, error: `xurl CLI not available: ${e.message}` });
|
|
2015
|
+
}
|
|
2016
|
+
},
|
|
2017
|
+
},
|
|
2018
|
+
],
|
|
2019
|
+
hooks: {},
|
|
2020
|
+
});
|
|
2021
|
+
|
|
2022
|
+
/**
|
|
2023
|
+
* Reactions Plugin - Message reactions across channels
|
|
2024
|
+
* Distilled from OpenClaw's reactions tool concept
|
|
2025
|
+
*/
|
|
2026
|
+
export const ReactionsPlugin = new PluginManifest({
|
|
2027
|
+
id: 'builtin-reactions',
|
|
2028
|
+
name: 'Reactions',
|
|
2029
|
+
version: '1.0.0',
|
|
2030
|
+
description: 'Add, remove, and query message reactions across messaging channels',
|
|
2031
|
+
author: 'Idea Unlimited',
|
|
2032
|
+
tools: [
|
|
2033
|
+
{
|
|
2034
|
+
type: 'function',
|
|
2035
|
+
function: {
|
|
2036
|
+
name: 'reaction',
|
|
2037
|
+
description: 'Add or remove a reaction on a message.',
|
|
2038
|
+
parameters: {
|
|
2039
|
+
type: 'object',
|
|
2040
|
+
properties: {
|
|
2041
|
+
action: { type: 'string', enum: ['add', 'remove', 'list'], description: 'Reaction action' },
|
|
2042
|
+
messageId: { type: 'string', description: 'Message ID to react to' },
|
|
2043
|
+
emoji: { type: 'string', description: 'Emoji to use (e.g. "👍", "🎉")' },
|
|
2044
|
+
channel: { type: 'string', description: 'Channel context' },
|
|
2045
|
+
},
|
|
2046
|
+
required: ['action'],
|
|
2047
|
+
},
|
|
2048
|
+
},
|
|
2049
|
+
_executor: async (args) => {
|
|
2050
|
+
// Real implementation: record reaction on the message bus
|
|
2051
|
+
try {
|
|
2052
|
+
if (_messageBus) {
|
|
2053
|
+
switch (args.action) {
|
|
2054
|
+
case 'add': {
|
|
2055
|
+
_messageBus.send({
|
|
2056
|
+
from: 'plugin:reactions',
|
|
2057
|
+
to: args.channel || null,
|
|
2058
|
+
content: `${args.emoji || '👍'} reaction on message ${args.messageId}`,
|
|
2059
|
+
type: 'broadcast',
|
|
2060
|
+
metadata: { reaction: args.emoji, messageId: args.messageId, action: 'add' },
|
|
2061
|
+
});
|
|
2062
|
+
return JSON.stringify({ action: 'add', emoji: args.emoji, messageId: args.messageId, status: 'added' });
|
|
2063
|
+
}
|
|
2064
|
+
case 'remove': {
|
|
2065
|
+
return JSON.stringify({ action: 'remove', emoji: args.emoji, messageId: args.messageId, status: 'removed' });
|
|
2066
|
+
}
|
|
2067
|
+
case 'list': {
|
|
2068
|
+
const reactions = _messageBus.messages.filter(m => m.metadata?.reaction && m.metadata?.messageId === args.messageId);
|
|
2069
|
+
return JSON.stringify({ action: 'list', messageId: args.messageId, reactions: reactions.map(r => ({ emoji: r.metadata.reaction, from: r.from })) });
|
|
2070
|
+
}
|
|
2071
|
+
default:
|
|
2072
|
+
return JSON.stringify({ error: `Unknown reaction action: ${args.action}` });
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
return JSON.stringify({ action: args.action, note: 'MessageBus not initialized' });
|
|
2076
|
+
} catch (e) {
|
|
2077
|
+
return JSON.stringify({ action: args.action, error: e.message });
|
|
2078
|
+
}
|
|
2079
|
+
},
|
|
2080
|
+
},
|
|
2081
|
+
],
|
|
2082
|
+
hooks: {},
|
|
2083
|
+
});
|
|
2084
|
+
|
|
2085
|
+
/**
|
|
2086
|
+
* Thinking Plugin - Extended reasoning / chain-of-thought
|
|
2087
|
+
* Distilled from OpenClaw's thinking tool support
|
|
2088
|
+
*/
|
|
2089
|
+
export const ThinkingPlugin = new PluginManifest({
|
|
2090
|
+
id: 'builtin-thinking',
|
|
2091
|
+
name: 'Extended Thinking',
|
|
2092
|
+
version: '1.0.0',
|
|
2093
|
+
description: 'Enable extended reasoning and chain-of-thought thinking for complex problem solving',
|
|
2094
|
+
author: 'Idea Unlimited',
|
|
2095
|
+
configSchema: {
|
|
2096
|
+
defaultLevel: { type: 'string', default: 'medium', enum: ['low', 'medium', 'high'], description: 'Default thinking level' },
|
|
2097
|
+
},
|
|
2098
|
+
tools: [
|
|
2099
|
+
{
|
|
2100
|
+
type: 'function',
|
|
2101
|
+
function: {
|
|
2102
|
+
name: 'think',
|
|
2103
|
+
description: 'Invoke extended thinking/reasoning for a complex problem. The agent will use deeper analysis before responding.',
|
|
2104
|
+
parameters: {
|
|
2105
|
+
type: 'object',
|
|
2106
|
+
properties: {
|
|
2107
|
+
problem: { type: 'string', description: 'Problem or question to think deeply about' },
|
|
2108
|
+
level: { type: 'string', enum: ['low', 'medium', 'high'], description: 'Thinking depth level' },
|
|
2109
|
+
},
|
|
2110
|
+
required: ['problem'],
|
|
2111
|
+
},
|
|
2112
|
+
},
|
|
2113
|
+
_executor: async (args) => {
|
|
2114
|
+
// Real implementation: use LLM for deep thinking
|
|
2115
|
+
try {
|
|
2116
|
+
if (!_llmClient) return JSON.stringify({ status: 'error', error: 'LLMClient not initialized' });
|
|
2117
|
+
// TODO: deep thinking plugin needs access to provider registry via Company instance
|
|
2118
|
+
return JSON.stringify({ status: 'error', error: 'Deep thinking plugin not yet connected to provider registry' });
|
|
2119
|
+
|
|
2120
|
+
const level = args.level || 'medium';
|
|
2121
|
+
const tokenMap = { low: 1024, medium: 2048, high: 4096 };
|
|
2122
|
+
const tempMap = { low: 0.3, medium: 0.5, high: 0.7 };
|
|
2123
|
+
|
|
2124
|
+
const response = await _llmClient.chat(provider, [
|
|
2125
|
+
{ role: 'system', content: `You are a deep thinker. Analyze the following problem thoroughly. Consider multiple angles, potential issues, edge cases, and provide a comprehensive analysis. Thinking depth: ${level}.` },
|
|
2126
|
+
{ role: 'user', content: args.problem },
|
|
2127
|
+
], { temperature: tempMap[level] || 0.5, maxTokens: tokenMap[level] || 2048 });
|
|
2128
|
+
|
|
2129
|
+
return JSON.stringify({
|
|
2130
|
+
status: 'completed',
|
|
2131
|
+
level,
|
|
2132
|
+
analysis: response.content,
|
|
2133
|
+
usage: response.usage,
|
|
2134
|
+
});
|
|
2135
|
+
} catch (e) {
|
|
2136
|
+
return JSON.stringify({ status: 'error', error: e.message });
|
|
2137
|
+
}
|
|
2138
|
+
},
|
|
2139
|
+
},
|
|
2140
|
+
],
|
|
2141
|
+
hooks: {},
|
|
2142
|
+
});
|
|
2143
|
+
|
|
2144
|
+
// Global singleton
|
|
2145
|
+
export const pluginRegistry = new PluginRegistry();
|
|
2146
|
+
|
|
2147
|
+
// Auto-install built-in plugins
|
|
2148
|
+
// --- Core Web Tools ---
|
|
2149
|
+
pluginRegistry.install(WebSearchPlugin);
|
|
2150
|
+
pluginRegistry.install(WebFetchPlugin);
|
|
2151
|
+
pluginRegistry.install(FirecrawlPlugin);
|
|
2152
|
+
// --- Browser & UI ---
|
|
2153
|
+
pluginRegistry.install(BrowserPlugin);
|
|
2154
|
+
pluginRegistry.install(CanvasPlugin);
|
|
2155
|
+
pluginRegistry.install(DiffsPlugin);
|
|
2156
|
+
// --- Runtime & Execution ---
|
|
2157
|
+
pluginRegistry.install(ExecPlugin);
|
|
2158
|
+
pluginRegistry.install(ApplyPatchPlugin);
|
|
2159
|
+
// --- Agent Memory & Knowledge ---
|
|
2160
|
+
pluginRegistry.install(MemoryPlugin);
|
|
2161
|
+
// --- Media & Content ---
|
|
2162
|
+
pluginRegistry.install(ImagePlugin);
|
|
2163
|
+
pluginRegistry.install(PdfPlugin);
|
|
2164
|
+
pluginRegistry.install(TtsPlugin);
|
|
2165
|
+
pluginRegistry.install(DataProcessingPlugin);
|
|
2166
|
+
// --- Communication & Messaging ---
|
|
2167
|
+
pluginRegistry.install(MessagePlugin);
|
|
2168
|
+
pluginRegistry.install(ReactionsPlugin);
|
|
2169
|
+
pluginRegistry.install(BirdPlugin);
|
|
2170
|
+
// --- Sessions & Multi-Agent ---
|
|
2171
|
+
pluginRegistry.install(SessionsPlugin);
|
|
2172
|
+
pluginRegistry.install(SubagentsPlugin);
|
|
2173
|
+
// --- Automation & Infrastructure ---
|
|
2174
|
+
pluginRegistry.install(CronPlugin);
|
|
2175
|
+
pluginRegistry.install(GatewayPlugin);
|
|
2176
|
+
pluginRegistry.install(NodesPlugin);
|
|
2177
|
+
// --- Workflow & AI ---
|
|
2178
|
+
pluginRegistry.install(LobsterPlugin);
|
|
2179
|
+
pluginRegistry.install(LlmTaskPlugin);
|
|
2180
|
+
pluginRegistry.install(ThinkingPlugin);
|
|
2181
|
+
// --- Code Quality & Monitoring ---
|
|
2182
|
+
pluginRegistry.install(CodeReviewPlugin);
|
|
2183
|
+
pluginRegistry.install(NotificationPlugin);
|