@openagents-org/agent-connector 0.1.10 → 0.2.0

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.
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Adapter registry — maps agent type names to adapter classes.
3
+ */
4
+
5
+ 'use strict';
6
+
7
+ const BaseAdapter = require('./base');
8
+ const OpenClawAdapter = require('./openclaw');
9
+ const ClaudeAdapter = require('./claude');
10
+ const CodexAdapter = require('./codex');
11
+
12
+ const ADAPTER_MAP = {
13
+ openclaw: OpenClawAdapter,
14
+ claude: ClaudeAdapter,
15
+ codex: CodexAdapter,
16
+ };
17
+
18
+ /**
19
+ * Create an adapter instance for the given agent type.
20
+ * @param {string} type - Agent type (openclaw, claude, codex)
21
+ * @param {object} opts - Adapter constructor options
22
+ * @returns {BaseAdapter}
23
+ */
24
+ function createAdapter(type, opts) {
25
+ const AdapterClass = ADAPTER_MAP[type];
26
+ if (!AdapterClass) {
27
+ throw new Error(`Unknown agent type: ${type}. Supported: ${Object.keys(ADAPTER_MAP).join(', ')}`);
28
+ }
29
+ return new AdapterClass(opts);
30
+ }
31
+
32
+ module.exports = {
33
+ BaseAdapter,
34
+ OpenClawAdapter,
35
+ ClaudeAdapter,
36
+ CodexAdapter,
37
+ createAdapter,
38
+ ADAPTER_MAP,
39
+ };
@@ -0,0 +1,259 @@
1
+ /**
2
+ * OpenClaw adapter for OpenAgents workspace.
3
+ *
4
+ * Bridges OpenClaw to an OpenAgents workspace via:
5
+ * - CLI mode: `openclaw agent --local --json` (preferred)
6
+ * - Workspace context injected via SKILL.md auto-discovery
7
+ *
8
+ * Direct port of Python: src/openagents/adapters/openclaw.py
9
+ * (CLI mode only — gateway WS and direct HTTP modes are not yet ported)
10
+ */
11
+
12
+ 'use strict';
13
+
14
+ const fs = require('fs');
15
+ const path = require('path');
16
+ const { spawn, execSync } = require('child_process');
17
+
18
+ const BaseAdapter = require('./base');
19
+ const { formatAttachmentsForPrompt } = require('./utils');
20
+ const { buildOpenclawSkillMd, buildOpenclawSystemPrompt } = require('./workspace-prompt');
21
+
22
+ const IS_WINDOWS = process.platform === 'win32';
23
+ const OPENCLAW_STATE_DIR = path.join(
24
+ IS_WINDOWS ? (process.env.USERPROFILE || '') : (process.env.HOME || ''),
25
+ '.openclaw'
26
+ );
27
+
28
+ class OpenClawAdapter extends BaseAdapter {
29
+ /**
30
+ * @param {object} opts - BaseAdapter opts plus:
31
+ * @param {string} [opts.openclawAgentId='main']
32
+ * @param {Set} [opts.disabledModules]
33
+ */
34
+ constructor(opts) {
35
+ super(opts);
36
+ this.openclawAgentId = opts.openclawAgentId || 'main';
37
+ this.disabledModules = opts.disabledModules || new Set();
38
+
39
+ // Conversation history for multi-turn context
40
+ this._conversationHistory = [];
41
+ this._maxHistory = 50;
42
+
43
+ // Find the openclaw binary
44
+ this._openclawBinary = this._findOpenclawBinary();
45
+ if (this._openclawBinary) {
46
+ this._log(`Using OpenClaw CLI mode (${this._openclawBinary})`);
47
+ } else {
48
+ this._log('OpenClaw binary not found — agent will not be able to process messages');
49
+ }
50
+
51
+ // Install workspace skill
52
+ this._installWorkspaceSkill();
53
+ }
54
+
55
+ // ------------------------------------------------------------------
56
+ // Binary resolution
57
+ // ------------------------------------------------------------------
58
+
59
+ _findOpenclawBinary() {
60
+ try {
61
+ const cmd = IS_WINDOWS ? 'where openclaw' : 'which openclaw';
62
+ const result = execSync(cmd, { encoding: 'utf-8', timeout: 5000 })
63
+ .split(/\r?\n/)[0].trim();
64
+ if (result) return result;
65
+ } catch {}
66
+
67
+ // Check common npm global directories
68
+ const dirs = [];
69
+ if (IS_WINDOWS) {
70
+ const appdata = process.env.APPDATA || '';
71
+ if (appdata) dirs.push(path.join(appdata, 'npm'));
72
+ } else {
73
+ const home = process.env.HOME || '';
74
+ dirs.push(path.join(home, '.npm-global', 'bin'), '/usr/local/bin');
75
+ }
76
+ for (const d of dirs) {
77
+ for (const name of ['openclaw.cmd', 'openclaw']) {
78
+ const candidate = path.join(d, name);
79
+ if (fs.existsSync(candidate)) return candidate;
80
+ }
81
+ }
82
+ return null;
83
+ }
84
+
85
+ // ------------------------------------------------------------------
86
+ // Workspace skill installation
87
+ // ------------------------------------------------------------------
88
+
89
+ _resolveOpenclawWorkspace() {
90
+ const agentId = this.openclawAgentId;
91
+ const wsDir = agentId && agentId !== 'main'
92
+ ? path.join(OPENCLAW_STATE_DIR, `workspace-${agentId}`)
93
+ : path.join(OPENCLAW_STATE_DIR, 'workspace');
94
+
95
+ if (fs.existsSync(wsDir)) return wsDir;
96
+
97
+ // Fall back to default workspace
98
+ const fallback = path.join(OPENCLAW_STATE_DIR, 'workspace');
99
+ if (fs.existsSync(fallback)) return fallback;
100
+
101
+ return null;
102
+ }
103
+
104
+ _installWorkspaceSkill() {
105
+ const wsDir = this._resolveOpenclawWorkspace();
106
+ if (!wsDir) {
107
+ this._log('OpenClaw workspace not found, skipping skill install');
108
+ return;
109
+ }
110
+
111
+ const skillName = `openagents-workspace-${this.agentName}`;
112
+ const skillDir = path.join(wsDir, 'skills', skillName);
113
+ fs.mkdirSync(skillDir, { recursive: true });
114
+
115
+ const content = buildOpenclawSkillMd({
116
+ endpoint: this.endpoint,
117
+ workspaceId: this.workspaceId,
118
+ token: this.token,
119
+ agentName: this.agentName,
120
+ channelName: this.channelName,
121
+ disabledModules: this.disabledModules,
122
+ });
123
+
124
+ const skillPath = path.join(skillDir, 'SKILL.md');
125
+ fs.writeFileSync(skillPath, content, 'utf-8');
126
+ this._log(`Installed workspace skill at ${skillPath}`);
127
+ }
128
+
129
+ // ------------------------------------------------------------------
130
+ // Message handling
131
+ // ------------------------------------------------------------------
132
+
133
+ async _handleMessage(msg) {
134
+ let content = (msg.content || '').trim();
135
+ const attachments = msg.attachments || [];
136
+
137
+ // Append attachment info
138
+ const attText = formatAttachmentsForPrompt(attachments);
139
+ if (attText) {
140
+ content = content ? content + attText : attText.trim();
141
+ }
142
+
143
+ if (!content) return;
144
+
145
+ const msgChannel = msg.sessionId || this.channelName;
146
+ const sender = msg.senderName || msg.senderType || 'user';
147
+ this._log(`Processing message from ${sender} in ${msgChannel}: ${content.slice(0, 80)}...`);
148
+
149
+ await this._autoTitleChannel(msgChannel, content);
150
+ await this.sendStatus(msgChannel, 'thinking...');
151
+
152
+ try {
153
+ const responseText = await this._runCliAgent(content, msgChannel);
154
+
155
+ if (responseText) {
156
+ this._conversationHistory.push({ role: 'user', content });
157
+ this._conversationHistory.push({ role: 'assistant', content: responseText });
158
+ if (this._conversationHistory.length > this._maxHistory * 2) {
159
+ this._conversationHistory = this._conversationHistory.slice(-this._maxHistory * 2);
160
+ }
161
+ await this.sendResponse(msgChannel, responseText);
162
+ } else {
163
+ await this.sendResponse(msgChannel, 'No response generated. Please try again.');
164
+ }
165
+ } catch (e) {
166
+ this._log(`Error handling message: ${e.message}`);
167
+ await this.sendError(msgChannel, `Error processing message: ${e.message}`);
168
+ }
169
+ }
170
+
171
+ // ------------------------------------------------------------------
172
+ // CLI mode (openclaw agent --local)
173
+ // ------------------------------------------------------------------
174
+
175
+ _runCliAgent(userMessage, channel) {
176
+ return new Promise((resolve, reject) => {
177
+ const binary = this._openclawBinary;
178
+ if (!binary) {
179
+ reject(new Error('OpenClaw binary not found'));
180
+ return;
181
+ }
182
+
183
+ const sessionKey = `openagents-${this.workspaceId.slice(0, 8)}-${channel.slice(-8)}`;
184
+
185
+ const args = [
186
+ 'agent', '--local',
187
+ '--agent', this.openclawAgentId,
188
+ '--session-id', sessionKey,
189
+ '--message', userMessage,
190
+ '--json',
191
+ ];
192
+
193
+ this._log(`CLI: ${binary} ${args.slice(0, 5).join(' ')} ...`);
194
+
195
+ const spawnEnv = { ...process.env };
196
+ if (IS_WINDOWS) {
197
+ const npmBin = path.join(process.env.APPDATA || '', 'npm');
198
+ if (npmBin && !(spawnEnv.PATH || '').includes(npmBin)) {
199
+ spawnEnv.PATH = npmBin + ';' + (spawnEnv.PATH || '');
200
+ }
201
+ }
202
+
203
+ let spawnBinary = binary;
204
+ let spawnArgs = args;
205
+ const spawnOpts = {
206
+ stdio: ['ignore', 'pipe', 'pipe'],
207
+ env: spawnEnv,
208
+ timeout: 600000,
209
+ };
210
+
211
+ if (IS_WINDOWS) {
212
+ spawnBinary = process.env.COMSPEC || 'cmd.exe';
213
+ const quotedArgs = args.map((a) => a.includes(' ') ? `"${a}"` : a);
214
+ spawnArgs = ['/C', binary, ...quotedArgs];
215
+ }
216
+
217
+ const proc = spawn(spawnBinary, spawnArgs, spawnOpts);
218
+ let stdout = '';
219
+ let stderr = '';
220
+
221
+ if (proc.stdout) proc.stdout.on('data', (d) => { stdout += d; });
222
+ if (proc.stderr) proc.stderr.on('data', (d) => { stderr += d; });
223
+
224
+ proc.on('error', (err) => reject(err));
225
+ proc.on('exit', (code) => {
226
+ if (code !== 0) {
227
+ reject(new Error(`CLI exited ${code}: ${stderr.slice(0, 300)}`));
228
+ return;
229
+ }
230
+
231
+ stdout = stdout.trim();
232
+ if (!stdout) { resolve(''); return; }
233
+
234
+ // Parse JSON output — find first '{'
235
+ const jsonStart = stdout.indexOf('{');
236
+ if (jsonStart < 0) { resolve(stdout); return; }
237
+
238
+ try {
239
+ const data = JSON.parse(stdout.slice(jsonStart));
240
+ // Extract response text from JSON
241
+ const payloads = data.payloads || [];
242
+ if (payloads.length > 0) {
243
+ const texts = payloads
244
+ .filter((p) => p.text)
245
+ .map((p) => p.text);
246
+ resolve(texts.join('\n\n'));
247
+ } else {
248
+ resolve('');
249
+ }
250
+ } catch {
251
+ // Failed to parse JSON — return raw output
252
+ resolve(stdout);
253
+ }
254
+ });
255
+ });
256
+ }
257
+ }
258
+
259
+ module.exports = OpenClawAdapter;
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Shared utilities for adapter implementations.
3
+ *
4
+ * Direct port of Python: src/openagents/adapters/utils.py
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ const SESSION_DEFAULT_RE = /^(Session \d+|session-[0-9a-f]+|channel-[0-9a-f]+)$/;
10
+
11
+ /**
12
+ * Generate a short session title from the first user message.
13
+ */
14
+ function generateSessionTitle(message, maxWords = 6) {
15
+ // Collapse whitespace, strip code blocks
16
+ let text = message.replace(/\s+/g, ' ').trim();
17
+ text = text.replace(/```[\s\S]*?```/g, '').trim();
18
+ text = text.replace(/`[^`]+`/g, '').trim();
19
+
20
+ if (!text) return '';
21
+
22
+ // Try to get first sentence
23
+ const sentenceMatch = text.match(/^(.+?[.!?])\s/);
24
+ if (sentenceMatch) {
25
+ text = sentenceMatch[1].replace(/[.!?]+$/, '').trim();
26
+ }
27
+
28
+ // Take first maxWords words
29
+ const words = text.split(/\s+/);
30
+ if (words.length > maxWords) {
31
+ text = words.slice(0, maxWords).join(' ');
32
+ }
33
+
34
+ // Strip common filler prefixes
35
+ text = text.replace(
36
+ /^(hey|hi|hello|please|can you|could you|i need you to|i want you to)\s+/i,
37
+ ''
38
+ ).trim();
39
+
40
+ // Capitalize first letter
41
+ if (text) {
42
+ text = text[0].toUpperCase() + text.slice(1);
43
+ }
44
+
45
+ // Hard cap at 50 characters
46
+ if (text.length > 50) {
47
+ text = text.slice(0, 47) + '...';
48
+ }
49
+
50
+ return text;
51
+ }
52
+
53
+ /**
54
+ * Format attachment metadata into text to append to an agent prompt.
55
+ */
56
+ function formatAttachmentsForPrompt(attachments) {
57
+ if (!attachments || attachments.length === 0) return null;
58
+
59
+ const lines = ['\n[Attached files]'];
60
+ for (const att of attachments) {
61
+ const filename = att.filename || 'unknown';
62
+ const fileId = att.fileId || '';
63
+ const contentType = att.contentType || '';
64
+ if (contentType.startsWith('image/')) {
65
+ lines.push(
66
+ `- Image: ${filename} (file_id: ${fileId}) — ` +
67
+ 'use workspace_read_file to view this image'
68
+ );
69
+ } else {
70
+ lines.push(
71
+ `- File: ${filename} (file_id: ${fileId}, type: ${contentType}) — ` +
72
+ 'use workspace_read_file to read this file'
73
+ );
74
+ }
75
+ }
76
+ return lines.join('\n');
77
+ }
78
+
79
+ module.exports = {
80
+ SESSION_DEFAULT_RE,
81
+ generateSessionTitle,
82
+ formatAttachmentsForPrompt,
83
+ };
@@ -0,0 +1,293 @@
1
+ /**
2
+ * Shared workspace prompt builder for all adapters.
3
+ *
4
+ * Generates system prompt sections that teach agents about:
5
+ * - Their identity and workspace context
6
+ * - Multi-agent collaboration (@mention delegation)
7
+ * - Workspace REST API skills (files, browser, tunnels)
8
+ *
9
+ * Direct port of Python: src/openagents/adapters/workspace_prompt.py
10
+ */
11
+
12
+ 'use strict';
13
+
14
+ /**
15
+ * Build the identity section common to all adapters.
16
+ */
17
+ function buildWorkspaceIdentity(agentName, workspaceId, channelName, mode = 'execute') {
18
+ return (
19
+ `You are agent '${agentName}' connected to an OpenAgents workspace.\n` +
20
+ 'Your text responses are automatically posted to the workspace chat ' +
21
+ '— just write your answer naturally.\n\n' +
22
+ '## Workspace Context\n' +
23
+ `- Workspace ID: ${workspaceId}\n` +
24
+ `- Channel: ${channelName}\n` +
25
+ `- Mode: ${mode}\n`
26
+ );
27
+ }
28
+
29
+ /**
30
+ * Build the multi-agent collaboration instructions.
31
+ */
32
+ function buildCollaborationPrompt() {
33
+ return (
34
+ '\n## Multi-Agent Collaboration\n' +
35
+ 'To delegate work to another agent, @mention them in your response. ' +
36
+ 'Only @mentioned agents will receive the message.\n\n' +
37
+ 'IMPORTANT: Do NOT @mention an agent just to say thanks or acknowledge ' +
38
+ '— that wakes them up for nothing. Only @mention when you need them ' +
39
+ 'to do work. When the task is complete, report results to the user ' +
40
+ 'without @mentioning other agents.\n\n' +
41
+ 'To discover available agents, use the workspace discover endpoint ' +
42
+ 'or the workspace_get_agents tool (if available).\n'
43
+ );
44
+ }
45
+
46
+ /**
47
+ * Build mode-specific instructions.
48
+ */
49
+ function buildModePrompt(mode) {
50
+ if (mode === 'plan') {
51
+ return (
52
+ '\n## Mode: PLAN\n' +
53
+ 'You are in PLAN mode. Only read, analyze, and propose.\n' +
54
+ '- Do NOT write code, make changes, or execute actions.\n' +
55
+ '- Outline your plan step by step.\n' +
56
+ '- Describe what changes you would make and why.\n' +
57
+ '- Ask clarifying questions if needed.\n' +
58
+ '- When the user is satisfied, they can switch you to Execute mode.\n'
59
+ );
60
+ }
61
+ return (
62
+ '\n## Mode: EXECUTE\n' +
63
+ 'You are in EXECUTE mode. You can write code, make changes, ' +
64
+ 'and take actions.\n' +
65
+ 'Be helpful, concise, and direct. Use markdown formatting.\n'
66
+ );
67
+ }
68
+
69
+ /**
70
+ * Build REST API skill instructions for non-MCP agents.
71
+ *
72
+ * These teach the agent how to interact with workspace resources
73
+ * (files, browser, tunnels) by calling HTTP endpoints directly.
74
+ *
75
+ * In plan mode, only read-only operations are documented.
76
+ */
77
+ function buildApiSkillsPrompt({ endpoint, workspaceId, token, agentName, channelName, disabledModules, mode = 'execute' }) {
78
+ const disabled = disabledModules || new Set();
79
+ const baseUrl = endpoint.replace(/\/+$/, '');
80
+ const isPlan = mode === 'plan';
81
+ const h = `X-Workspace-Token: ${token}`;
82
+
83
+ const sections = [];
84
+
85
+ // Capabilities preamble
86
+ const caps = [];
87
+ if (!disabled.has('files')) caps.push('share and read files with other agents and users');
88
+ if (!disabled.has('browser')) caps.push('browse websites in a shared browser');
89
+ caps.push('discover other agents in the workspace');
90
+
91
+ sections.push(
92
+ '## Workspace Tools (MANDATORY)\n\n' +
93
+ 'You can ' + caps.join(', ') + '.\n' +
94
+ 'These are WORKSPACE tools shared with all agents and users. ' +
95
+ 'They are different from your native tools.\n\n' +
96
+ '**HOW TO USE:** Call your `exec` tool to run the `curl` commands below. ' +
97
+ 'Do NOT output curl commands as text — EXECUTE them with `exec`.\n\n' +
98
+ '**IMPORTANT — tool priority:**\n' +
99
+ '- ALWAYS use `exec` + `curl` (documented below) for workspace operations.\n' +
100
+ '- Do NOT use `workspace_browser_*` native tools — they are not configured ' +
101
+ 'and will fail.\n' +
102
+ '- Do NOT use `web_fetch`, `browser`, or any native browsing tool ' +
103
+ 'when the user asks to use the workspace browser — use `exec` + `curl` instead.\n' +
104
+ '- The workspace browser is a *shared* browser visible to all users and agents.\n\n' +
105
+ '**Auth header** (include on every request):\n' +
106
+ `\`X-Workspace-Token: ${token}\`\n`
107
+ );
108
+
109
+ // Files
110
+ if (!disabled.has('files')) {
111
+ let s = '\n### Shared Files\n\n';
112
+
113
+ if (!isPlan) {
114
+ s += (
115
+ '**To upload a file**, exec this (replace filename/content):\n' +
116
+ `CONTENT=$(echo -n 'YOUR_CONTENT' | base64) && ` +
117
+ `curl -s -X POST ${baseUrl}/v1/files/base64 ` +
118
+ `-H "${h}" ` +
119
+ '-H "Content-Type: application/json" ' +
120
+ `-d '{"filename":"report.md",` +
121
+ `"content_base64":"'"$CONTENT"'",` +
122
+ `"content_type":"text/markdown",` +
123
+ `"network":"${workspaceId}",` +
124
+ `"source":"openagents:${agentName}",` +
125
+ `"channel_name":"${channelName}"}'\n\n`
126
+ );
127
+ }
128
+
129
+ s += (
130
+ '**List files:**\n' +
131
+ `\`curl -s -H "${h}" ${baseUrl}/v1/files?network=${workspaceId}\`\n\n` +
132
+ '**Download file:**\n' +
133
+ `\`curl -s -H "${h}" ${baseUrl}/v1/files/{file_id}\`\n\n` +
134
+ '**File info (metadata):**\n' +
135
+ `\`curl -s -H "${h}" ${baseUrl}/v1/files/{file_id}/info\`\n`
136
+ );
137
+
138
+ if (!isPlan) {
139
+ s += (
140
+ '\n**Delete file:**\n' +
141
+ `\`curl -s -X DELETE -H "${h}" ${baseUrl}/v1/files/{file_id}\`\n`
142
+ );
143
+ }
144
+
145
+ sections.push(s);
146
+ }
147
+
148
+ // Browser
149
+ if (!disabled.has('browser')) {
150
+ let s = '\n### Shared Browser\n\n';
151
+
152
+ if (!isPlan) {
153
+ s += (
154
+ '**To browse a website**, exec these steps (use exec for each):\n' +
155
+ `Step 1 — open tab: ` +
156
+ `curl -s -X POST ${baseUrl}/v1/browser/tabs ` +
157
+ `-H "${h}" -H "Content-Type: application/json" ` +
158
+ `-d '{"url":"https://example.com","network":"${workspaceId}",` +
159
+ `"source":"openagents:${agentName}"}'\n` +
160
+ `Step 2 — read content: ` +
161
+ `curl -s -H "${h}" ${baseUrl}/v1/browser/tabs/TAB_ID/snapshot\n` +
162
+ `Step 3 — close tab: ` +
163
+ `curl -s -X DELETE -H "${h}" ${baseUrl}/v1/browser/tabs/TAB_ID\n` +
164
+ '(Replace TAB_ID with the id from step 1 response)\n\n'
165
+ );
166
+ }
167
+
168
+ s += (
169
+ '**List open tabs:**\n' +
170
+ `\`curl -s -H "${h}" ${baseUrl}/v1/browser/tabs?network=${workspaceId}\`\n\n` +
171
+ '**Get page content (text):**\n' +
172
+ `\`curl -s -H "${h}" ${baseUrl}/v1/browser/tabs/{tab_id}/snapshot\`\n\n` +
173
+ '**Get screenshot (PNG):**\n' +
174
+ `\`curl -s -H "${h}" ${baseUrl}/v1/browser/tabs/{tab_id}/screenshot\`\n`
175
+ );
176
+
177
+ if (!isPlan) {
178
+ s += (
179
+ '\n**Open tab:**\n' +
180
+ `\`curl -s -X POST -H "${h}" -H "Content-Type: application/json"` +
181
+ ` ${baseUrl}/v1/browser/tabs` +
182
+ ` -d '{"url":"URL","network":"${workspaceId}",` +
183
+ `"source":"openagents:${agentName}"}'\`\n\n` +
184
+ '**Navigate:**\n' +
185
+ `\`curl -s -X POST -H "${h}" -H "Content-Type: application/json"` +
186
+ ` ${baseUrl}/v1/browser/tabs/{tab_id}/navigate` +
187
+ ` -d '{"url":"URL"}'\`\n\n` +
188
+ '**Click element:**\n' +
189
+ `\`curl -s -X POST -H "${h}" -H "Content-Type: application/json"` +
190
+ ` ${baseUrl}/v1/browser/tabs/{tab_id}/click` +
191
+ ` -d '{"selector":"CSS_SELECTOR"}'\`\n\n` +
192
+ '**Type text:**\n' +
193
+ `\`curl -s -X POST -H "${h}" -H "Content-Type: application/json"` +
194
+ ` ${baseUrl}/v1/browser/tabs/{tab_id}/type` +
195
+ ` -d '{"selector":"CSS_SELECTOR","text":"TEXT"}'\`\n\n` +
196
+ '**Close tab:**\n' +
197
+ `\`curl -s -X DELETE -H "${h}" ${baseUrl}/v1/browser/tabs/{tab_id}\`\n`
198
+ );
199
+ }
200
+
201
+ sections.push(s);
202
+ }
203
+
204
+ // Discovery
205
+ sections.push(
206
+ '\n### Discover Agents\n' +
207
+ `\`curl -s -H "${h}" ${baseUrl}/v1/discover?network=${workspaceId}\`\n`
208
+ );
209
+
210
+ return sections.join('\n');
211
+ }
212
+
213
+ /**
214
+ * Build the system prompt for Claude adapter (MCP-based).
215
+ * Claude gets identity + collaboration instructions but NOT API skills.
216
+ */
217
+ function buildClaudeSystemPrompt({ agentName, workspaceId, channelName, mode = 'execute' }) {
218
+ const parts = [];
219
+ parts.push(buildWorkspaceIdentity(agentName, workspaceId, channelName, mode));
220
+ parts.push(
221
+ 'Use workspace_get_history to read previous messages.\n' +
222
+ 'Use workspace_get_agents to see other agents.\n'
223
+ );
224
+ parts.push(buildCollaborationPrompt());
225
+
226
+ if (mode === 'plan') {
227
+ parts.push(
228
+ '\nYou are in PLAN mode. Only read, analyze, and propose ' +
229
+ 'changes. Do not make edits.\n'
230
+ );
231
+ }
232
+
233
+ parts.push(
234
+ '\nIMPORTANT: Never use AskUserQuestion. ' +
235
+ 'AskUserQuestion blocks the subprocess and will hang the thread. ' +
236
+ 'If you need to ask the user something, just write the question ' +
237
+ 'as your text response.\n'
238
+ );
239
+
240
+ return parts.join('\n');
241
+ }
242
+
243
+ /**
244
+ * Build the full system prompt for OpenClaw/non-MCP agents.
245
+ */
246
+ function buildOpenclawSystemPrompt({ agentName, workspaceId, channelName, endpoint, token, mode = 'execute', disabledModules }) {
247
+ const parts = [];
248
+ parts.push(buildWorkspaceIdentity(agentName, workspaceId, channelName, mode));
249
+ parts.push(buildCollaborationPrompt());
250
+ parts.push(buildModePrompt(mode));
251
+ parts.push(buildApiSkillsPrompt({
252
+ endpoint, workspaceId, token, agentName, channelName, disabledModules, mode,
253
+ }));
254
+ return parts.join('\n');
255
+ }
256
+
257
+ /**
258
+ * Build a SKILL.md file for OpenClaw's skill auto-discovery.
259
+ */
260
+ function buildOpenclawSkillMd({ endpoint, workspaceId, token, agentName, channelName, disabledModules }) {
261
+ const body = buildApiSkillsPrompt({
262
+ endpoint, workspaceId, token, agentName, channelName, disabledModules, mode: 'execute',
263
+ });
264
+
265
+ const identity = buildWorkspaceIdentity(agentName, workspaceId, channelName, 'execute');
266
+ const collab = buildCollaborationPrompt();
267
+
268
+ const frontmatter = (
269
+ '---\n' +
270
+ 'name: openagents-workspace\n' +
271
+ 'description: "Share files, browse websites, and collaborate ' +
272
+ 'with other agents in an OpenAgents workspace. Use when: ' +
273
+ '(1) sharing results or reports with the user or other agents, ' +
274
+ '(2) browsing a website to gather information, ' +
275
+ '(3) reading files shared by users or other agents, ' +
276
+ '(4) checking who else is in the workspace."\n' +
277
+ 'metadata:\n' +
278
+ ' {"openclaw": {"always": true, "emoji": "\\U0001F310"}}\n' +
279
+ '---\n\n'
280
+ );
281
+
282
+ return frontmatter + identity + '\n' + collab + '\n' + body;
283
+ }
284
+
285
+ module.exports = {
286
+ buildWorkspaceIdentity,
287
+ buildCollaborationPrompt,
288
+ buildModePrompt,
289
+ buildApiSkillsPrompt,
290
+ buildClaudeSystemPrompt,
291
+ buildOpenclawSystemPrompt,
292
+ buildOpenclawSkillMd,
293
+ };