@openagents-org/agent-launcher 0.2.63 → 0.2.65

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openagents-org/agent-launcher",
3
- "version": "0.2.63",
3
+ "version": "0.2.65",
4
4
  "description": "OpenAgents Launcher — install, configure, and run AI coding agents from your terminal",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -226,8 +226,12 @@ class ClaudeAdapter extends BaseAdapter {
226
226
 
227
227
  // Find openagents binary (multi-tier)
228
228
  let oaBin = null;
229
+ const home3 = os.homedir();
230
+ // Tier 0: Portable install at ~/.openagents/nodejs/node_modules/.bin/
231
+ const oaPortable = path.join(home3, '.openagents', 'nodejs', 'node_modules', '.bin', `openagents${IS_WINDOWS ? '.cmd' : ''}`);
232
+ if (fs.existsSync(oaPortable)) oaBin = oaPortable;
229
233
  // Tier 1: PATH
230
- try {
234
+ if (!oaBin) try {
231
235
  if (IS_WINDOWS) {
232
236
  oaBin = execSync('where openagents.cmd 2>nul || where openagents.exe 2>nul || where openagents 2>nul', {
233
237
  encoding: 'utf-8', timeout: 5000,
package/src/cli.js CHANGED
@@ -478,6 +478,7 @@ Commands:
478
478
  workspace create [name] Create a new workspace
479
479
  workspace join <token> Join workspace with token
480
480
  workspace list List configured workspaces
481
+ mcp-server Start MCP server (stdio) for workspace tools
481
482
  version Show version
482
483
  help Show this help
483
484
 
@@ -535,6 +536,23 @@ async function main() {
535
536
  workspace: () => cmdWorkspace(connector, flags, positional),
536
537
  env: () => cmdEnv(connector, flags, positional),
537
538
  'test-llm': () => cmdTestLLM(connector, flags, positional),
539
+ 'mcp-server': () => {
540
+ const { runMcpServer } = require('./mcp-server');
541
+ const workspaceId = flags['workspace-id'] || process.env.OPENAGENTS_WORKSPACE_ID;
542
+ const channelName = flags['channel-name'] || process.env.OPENAGENTS_CHANNEL_NAME || 'general';
543
+ const agentName = flags['agent-name'] || process.env.OPENAGENTS_AGENT_NAME || 'agent';
544
+ const endpoint = flags.endpoint || process.env.OPENAGENTS_ENDPOINT || 'https://workspace-endpoint.openagents.org';
545
+ const token = process.env.OA_WORKSPACE_TOKEN || '';
546
+ if (!workspaceId || !token) {
547
+ print('Error: --workspace-id required and OA_WORKSPACE_TOKEN env var must be set');
548
+ process.exitCode = 1;
549
+ return;
550
+ }
551
+ const disabledModules = new Set();
552
+ if (flags['disable-files']) disabledModules.add('files');
553
+ if (flags['disable-browser']) disabledModules.add('browser');
554
+ runMcpServer({ workspaceId, channelName, agentName, endpoint, token, disabledModules });
555
+ },
538
556
  };
539
557
 
540
558
  const handler = commands[cmd];
@@ -0,0 +1,463 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * MCP (Model Context Protocol) server for OpenAgents workspace tools.
5
+ *
6
+ * Implements JSON-RPC 2.0 over stdio, exposing workspace operations
7
+ * (history, files, browser, agents) as MCP tools that Claude Code can use.
8
+ *
9
+ * Usage:
10
+ * openagents mcp-server --workspace-id <id> --channel-name <ch> --agent-name <name>
11
+ *
12
+ * The workspace token is read from the OA_WORKSPACE_TOKEN env var.
13
+ */
14
+
15
+ const readline = require('readline');
16
+ const { WorkspaceClient } = require('./workspace-client');
17
+
18
+ // ── Tool definitions ────────────────────────────────────────────────────────
19
+
20
+ function buildToolDefs(disabledModules) {
21
+ const tools = [
22
+ // -- Workspace core (always enabled) --
23
+ {
24
+ name: 'workspace_get_history',
25
+ description: 'Read recent messages in the current workspace channel.',
26
+ inputSchema: {
27
+ type: 'object',
28
+ properties: {
29
+ limit: { type: 'integer', description: 'Number of messages to return (default 20)', default: 20 },
30
+ },
31
+ },
32
+ },
33
+ {
34
+ name: 'workspace_get_agents',
35
+ description: 'List all agents connected to the workspace with their status.',
36
+ inputSchema: { type: 'object', properties: {} },
37
+ },
38
+ {
39
+ name: 'workspace_status',
40
+ description: 'Post a short status update visible to workspace viewers (e.g. "analyzing code...").',
41
+ inputSchema: {
42
+ type: 'object',
43
+ properties: {
44
+ status: { type: 'string', description: 'Short status description' },
45
+ },
46
+ required: ['status'],
47
+ },
48
+ },
49
+ ];
50
+
51
+ // -- Files module --
52
+ if (!disabledModules.has('files')) {
53
+ tools.push(
54
+ {
55
+ name: 'workspace_list_files',
56
+ description: 'List files shared in the workspace.',
57
+ inputSchema: { type: 'object', properties: {} },
58
+ },
59
+ {
60
+ name: 'workspace_read_file',
61
+ description: 'Read a shared file by its ID. Returns text content or base64 for binary.',
62
+ inputSchema: {
63
+ type: 'object',
64
+ properties: {
65
+ file_id: { type: 'string', description: 'File ID to read' },
66
+ },
67
+ required: ['file_id'],
68
+ },
69
+ },
70
+ {
71
+ name: 'workspace_write_file',
72
+ description: 'Write/upload a file to shared workspace storage.',
73
+ inputSchema: {
74
+ type: 'object',
75
+ properties: {
76
+ filename: { type: 'string', description: 'Filename (e.g. "report.md")' },
77
+ content: { type: 'string', description: 'File content (text or base64 for binary)' },
78
+ content_type: { type: 'string', description: 'MIME type (auto-detected from filename if omitted)' },
79
+ },
80
+ required: ['filename', 'content'],
81
+ },
82
+ },
83
+ {
84
+ name: 'workspace_delete_file',
85
+ description: 'Delete a shared file by its ID.',
86
+ inputSchema: {
87
+ type: 'object',
88
+ properties: {
89
+ file_id: { type: 'string', description: 'File ID to delete' },
90
+ },
91
+ required: ['file_id'],
92
+ },
93
+ },
94
+ );
95
+ }
96
+
97
+ // -- Browser module --
98
+ if (!disabledModules.has('browser')) {
99
+ tools.push(
100
+ {
101
+ name: 'workspace_browser_open',
102
+ description: 'Open a new shared browser tab.',
103
+ inputSchema: {
104
+ type: 'object',
105
+ properties: {
106
+ url: { type: 'string', description: 'URL to open (default: about:blank)' },
107
+ },
108
+ },
109
+ },
110
+ {
111
+ name: 'workspace_browser_navigate',
112
+ description: 'Navigate a browser tab to a URL.',
113
+ inputSchema: {
114
+ type: 'object',
115
+ properties: {
116
+ tab_id: { type: 'string', description: 'Tab ID' },
117
+ url: { type: 'string', description: 'URL to navigate to' },
118
+ },
119
+ required: ['tab_id', 'url'],
120
+ },
121
+ },
122
+ {
123
+ name: 'workspace_browser_click',
124
+ description: 'Click an element in a browser tab by CSS selector.',
125
+ inputSchema: {
126
+ type: 'object',
127
+ properties: {
128
+ tab_id: { type: 'string', description: 'Tab ID' },
129
+ selector: { type: 'string', description: 'CSS selector of element to click' },
130
+ },
131
+ required: ['tab_id', 'selector'],
132
+ },
133
+ },
134
+ {
135
+ name: 'workspace_browser_type',
136
+ description: 'Type text into an element in a browser tab.',
137
+ inputSchema: {
138
+ type: 'object',
139
+ properties: {
140
+ tab_id: { type: 'string', description: 'Tab ID' },
141
+ selector: { type: 'string', description: 'CSS selector of input element' },
142
+ text: { type: 'string', description: 'Text to type' },
143
+ },
144
+ required: ['tab_id', 'selector', 'text'],
145
+ },
146
+ },
147
+ {
148
+ name: 'workspace_browser_screenshot',
149
+ description: 'Take a screenshot of a browser tab. Returns a base64 PNG image.',
150
+ inputSchema: {
151
+ type: 'object',
152
+ properties: {
153
+ tab_id: { type: 'string', description: 'Tab ID' },
154
+ },
155
+ required: ['tab_id'],
156
+ },
157
+ },
158
+ {
159
+ name: 'workspace_browser_snapshot',
160
+ description: 'Get the accessibility tree (DOM structure) of a browser tab as text.',
161
+ inputSchema: {
162
+ type: 'object',
163
+ properties: {
164
+ tab_id: { type: 'string', description: 'Tab ID' },
165
+ },
166
+ required: ['tab_id'],
167
+ },
168
+ },
169
+ {
170
+ name: 'workspace_browser_list_tabs',
171
+ description: 'List all open shared browser tabs.',
172
+ inputSchema: { type: 'object', properties: {} },
173
+ },
174
+ {
175
+ name: 'workspace_browser_close',
176
+ description: 'Close a shared browser tab.',
177
+ inputSchema: {
178
+ type: 'object',
179
+ properties: {
180
+ tab_id: { type: 'string', description: 'Tab ID to close' },
181
+ },
182
+ required: ['tab_id'],
183
+ },
184
+ },
185
+ );
186
+ }
187
+
188
+ return tools;
189
+ }
190
+
191
+ // ── MIME type detection ─────────────────────────────────────────────────────
192
+
193
+ const MIME_MAP = {
194
+ '.txt': 'text/plain', '.md': 'text/markdown', '.json': 'application/json',
195
+ '.js': 'text/javascript', '.ts': 'text/typescript', '.py': 'text/x-python',
196
+ '.html': 'text/html', '.css': 'text/css', '.xml': 'application/xml',
197
+ '.yaml': 'application/yaml', '.yml': 'application/yaml',
198
+ '.csv': 'text/csv', '.log': 'text/plain', '.sh': 'text/x-shellscript',
199
+ '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg',
200
+ '.gif': 'image/gif', '.svg': 'image/svg+xml', '.pdf': 'application/pdf',
201
+ };
202
+
203
+ function detectMime(filename) {
204
+ const ext = (filename.match(/\.[^.]+$/) || [''])[0].toLowerCase();
205
+ return MIME_MAP[ext] || 'application/octet-stream';
206
+ }
207
+
208
+ function isTextMime(mime) {
209
+ return mime.startsWith('text/') || ['application/json', 'application/xml',
210
+ 'application/javascript', 'application/yaml'].includes(mime);
211
+ }
212
+
213
+ // ── MCP Server ──────────────────────────────────────────────────────────────
214
+
215
+ class McpServer {
216
+ constructor({ wsClient, workspaceId, channelName, agentName, token, disabledModules }) {
217
+ this.ws = wsClient;
218
+ this.workspaceId = workspaceId;
219
+ this.channelName = channelName;
220
+ this.agentName = agentName;
221
+ this.token = token;
222
+ this.disabledModules = disabledModules || new Set();
223
+ this.tools = buildToolDefs(this.disabledModules);
224
+ }
225
+
226
+ start() {
227
+ const rl = readline.createInterface({ input: process.stdin, terminal: false });
228
+ rl.on('line', (line) => {
229
+ line = line.trim();
230
+ if (!line) return;
231
+ let msg;
232
+ try {
233
+ msg = JSON.parse(line);
234
+ } catch {
235
+ this._write(jsonRpcError(null, -32700, 'Parse error'));
236
+ return;
237
+ }
238
+ // Notifications (no id) — acknowledge but don't respond
239
+ if (msg.id === undefined || msg.id === null) return;
240
+ this._handleRequest(msg).catch((e) => {
241
+ this._write(jsonRpcError(msg.id, -32603, e.message));
242
+ });
243
+ });
244
+ rl.on('close', () => process.exit(0));
245
+ this._log('MCP server started');
246
+ }
247
+
248
+ async _handleRequest(msg) {
249
+ const { id, method, params } = msg;
250
+ switch (method) {
251
+ case 'initialize':
252
+ this._write(jsonRpcResponse(id, {
253
+ protocolVersion: '2024-11-05',
254
+ capabilities: { tools: {} },
255
+ serverInfo: { name: 'openagents-workspace', version: '1.0.0' },
256
+ }));
257
+ break;
258
+ case 'tools/list':
259
+ this._write(jsonRpcResponse(id, { tools: this.tools }));
260
+ break;
261
+ case 'tools/call':
262
+ await this._handleToolCall(id, params || {});
263
+ break;
264
+ default:
265
+ this._write(jsonRpcError(id, -32601, `Unknown method: ${method}`));
266
+ }
267
+ }
268
+
269
+ async _handleToolCall(id, { name, arguments: args = {} }) {
270
+ try {
271
+ const result = await this._dispatch(name, args);
272
+ this._write(jsonRpcResponse(id, result));
273
+ } catch (e) {
274
+ this._write(jsonRpcResponse(id, {
275
+ content: [{ type: 'text', text: `Error: ${e.message}` }],
276
+ isError: true,
277
+ }));
278
+ }
279
+ }
280
+
281
+ async _dispatch(name, args) {
282
+ const text = (t) => ({ content: [{ type: 'text', text: t }] });
283
+ const image = (data, mime) => ({ content: [{ type: 'image', data, mimeType: mime }] });
284
+
285
+ switch (name) {
286
+
287
+ // ── Workspace core ──
288
+
289
+ case 'workspace_get_history': {
290
+ const limit = args.limit || 20;
291
+ const data = await this.ws.pollMessages(this.workspaceId, this.channelName, this.token, { limit });
292
+ const events = data.events || data || [];
293
+ if (!events.length) return text('No messages yet.');
294
+ const lines = events.map((e) => {
295
+ const sender = (e.source || '').replace(/^(human|openagents):/, '');
296
+ const content = e.payload?.content || '';
297
+ const type = e.payload?.message_type || 'chat';
298
+ if (type === 'status') return null; // skip status updates
299
+ return `[${sender}] ${content}`;
300
+ }).filter(Boolean);
301
+ return text(lines.join('\n'));
302
+ }
303
+
304
+ case 'workspace_get_agents': {
305
+ const data = await this.ws.getAgents(this.workspaceId, this.token);
306
+ const agents = data.agents || data || [];
307
+ if (!agents.length) return text('No agents connected.');
308
+ const lines = agents.map((a) =>
309
+ `- ${a.name} (${a.type || 'unknown'}) — ${a.status || 'unknown'}${a.role ? ` [${a.role}]` : ''}`
310
+ );
311
+ return text(lines.join('\n'));
312
+ }
313
+
314
+ case 'workspace_status': {
315
+ await this.ws.sendMessage(this.workspaceId, this.channelName, this.token, args.status, {
316
+ senderType: 'agent',
317
+ senderName: this.agentName,
318
+ messageType: 'status',
319
+ });
320
+ return text(`Status updated: ${args.status}`);
321
+ }
322
+
323
+ // ── Files ──
324
+
325
+ case 'workspace_list_files': {
326
+ const data = await this.ws.listFiles(this.workspaceId, this.token, { limit: 50, offset: 0 });
327
+ const files = data.files || data || [];
328
+ if (!files.length) return text('No files shared yet.');
329
+ const lines = files.map((f) => {
330
+ const size = f.size ? `${(f.size / 1024).toFixed(1)}KB` : '?';
331
+ return `- ${f.filename || f.name} (id: ${f.id}, ${size}, by ${f.uploaded_by || f.source || 'unknown'})`;
332
+ });
333
+ return text(lines.join('\n'));
334
+ }
335
+
336
+ case 'workspace_read_file': {
337
+ const info = await this.ws.getFileInfo(this.token, args.file_id);
338
+ const buf = await this.ws.readFile(this.workspaceId, this.token, args.file_id);
339
+ const mime = info.content_type || 'application/octet-stream';
340
+ if (mime.startsWith('image/')) {
341
+ const b64 = Buffer.isBuffer(buf) ? buf.toString('base64') : buf;
342
+ return image(b64, mime);
343
+ }
344
+ // Try to decode as text
345
+ const str = Buffer.isBuffer(buf) ? buf.toString('utf-8') : String(buf);
346
+ return text(str);
347
+ }
348
+
349
+ case 'workspace_write_file': {
350
+ const mime = args.content_type || detectMime(args.filename);
351
+ let b64;
352
+ if (isTextMime(mime)) {
353
+ b64 = Buffer.from(args.content, 'utf-8').toString('base64');
354
+ } else {
355
+ b64 = args.content; // assume already base64
356
+ }
357
+ const result = await this.ws.uploadFile(this.workspaceId, this.token, args.filename, b64, {
358
+ contentType: mime,
359
+ source: `openagents:${this.agentName}`,
360
+ channelName: this.channelName,
361
+ });
362
+ const fileId = result.id || result.file_id || 'unknown';
363
+ return text(`File written: ${args.filename} (id: ${fileId})`);
364
+ }
365
+
366
+ case 'workspace_delete_file': {
367
+ await this.ws.deleteFile(this.workspaceId, this.token, args.file_id);
368
+ return text(`File deleted: ${args.file_id}`);
369
+ }
370
+
371
+ // ── Browser ──
372
+
373
+ case 'workspace_browser_open': {
374
+ const result = await this.ws.browserOpenTab(this.workspaceId, this.token, {
375
+ url: args.url || 'about:blank',
376
+ source: `openagents:${this.agentName}`,
377
+ });
378
+ const tabId = result.tab_id || result.id || 'unknown';
379
+ return text(`Browser tab opened: ${tabId} → ${args.url || 'about:blank'}`);
380
+ }
381
+
382
+ case 'workspace_browser_navigate': {
383
+ const result = await this.ws.browserNavigate(this.workspaceId, this.token, args.tab_id, args.url);
384
+ const title = result.title || '';
385
+ return text(`Navigated to: ${args.url}${title ? ` (title: ${title})` : ''}`);
386
+ }
387
+
388
+ case 'workspace_browser_click': {
389
+ const result = await this.ws.browserClick(this.workspaceId, this.token, args.tab_id, args.selector);
390
+ return text(`Clicked: ${args.selector}${result.url ? ` (url now: ${result.url})` : ''}`);
391
+ }
392
+
393
+ case 'workspace_browser_type': {
394
+ await this.ws.browserType(this.workspaceId, this.token, args.tab_id, args.selector, args.text);
395
+ return text(`Typed into ${args.selector}: ${args.text.slice(0, 50)}${args.text.length > 50 ? '...' : ''}`);
396
+ }
397
+
398
+ case 'workspace_browser_screenshot': {
399
+ const buf = await this.ws.browserScreenshot(this.workspaceId, this.token, args.tab_id);
400
+ const b64 = Buffer.isBuffer(buf) ? buf.toString('base64') : buf;
401
+ return image(b64, 'image/png');
402
+ }
403
+
404
+ case 'workspace_browser_snapshot': {
405
+ const snapshot = await this.ws.browserSnapshot(this.workspaceId, this.token, args.tab_id);
406
+ return text(typeof snapshot === 'string' ? snapshot : JSON.stringify(snapshot));
407
+ }
408
+
409
+ case 'workspace_browser_list_tabs': {
410
+ const data = await this.ws.browserListTabs(this.workspaceId, this.token);
411
+ const tabs = data.tabs || data || [];
412
+ if (!tabs.length) return text('No browser tabs open.');
413
+ const lines = tabs.map((t) =>
414
+ `- ${t.id || t.tab_id}: ${t.label || t.title || 'untitled'}\n URL: ${t.url || 'N/A'} | by ${t.created_by || 'unknown'}`
415
+ );
416
+ return text(lines.join('\n'));
417
+ }
418
+
419
+ case 'workspace_browser_close': {
420
+ await this.ws.browserCloseTab(this.workspaceId, this.token, args.tab_id);
421
+ return text(`Browser tab closed: ${args.tab_id}`);
422
+ }
423
+
424
+ default:
425
+ throw new Error(`Unknown tool: ${name}`);
426
+ }
427
+ }
428
+
429
+ _write(json) {
430
+ process.stdout.write(json + '\n');
431
+ }
432
+
433
+ _log(msg) {
434
+ process.stderr.write(`[mcp] ${msg}\n`);
435
+ }
436
+ }
437
+
438
+ // ── JSON-RPC helpers ────────────────────────────────────────────────────────
439
+
440
+ function jsonRpcResponse(id, result) {
441
+ return JSON.stringify({ jsonrpc: '2.0', id, result });
442
+ }
443
+
444
+ function jsonRpcError(id, code, message) {
445
+ return JSON.stringify({ jsonrpc: '2.0', id, error: { code, message } });
446
+ }
447
+
448
+ // ── Entry point (called from cli.js) ────────────────────────────────────────
449
+
450
+ function runMcpServer(opts) {
451
+ const wsClient = new WorkspaceClient(opts.endpoint);
452
+ const server = new McpServer({
453
+ wsClient,
454
+ workspaceId: opts.workspaceId,
455
+ channelName: opts.channelName,
456
+ agentName: opts.agentName,
457
+ token: opts.token,
458
+ disabledModules: opts.disabledModules,
459
+ });
460
+ server.start();
461
+ }
462
+
463
+ module.exports = { McpServer, runMcpServer, buildToolDefs };