@openagents-org/agent-launcher 0.2.116 → 0.2.118
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 +1 -1
- package/src/adapters/base.js +47 -0
- package/src/adapters/claude.js +103 -33
- package/src/adapters/workspace-prompt.js +97 -1
- package/src/cli.js +45 -0
- package/src/daemon.js +1 -0
- package/src/mcp-server.js +116 -0
- package/src/workspace-client.js +94 -0
package/package.json
CHANGED
package/src/adapters/base.js
CHANGED
|
@@ -391,6 +391,53 @@ class BaseAdapter {
|
|
|
391
391
|
}
|
|
392
392
|
}
|
|
393
393
|
|
|
394
|
+
async cleanupTodos(channel) {
|
|
395
|
+
try {
|
|
396
|
+
const result = await this.client.getTodos(this.workspaceId, channel, this.token, {
|
|
397
|
+
all: false,
|
|
398
|
+
});
|
|
399
|
+
const todos = (result && result.todos) || [];
|
|
400
|
+
const hasActive = todos.some((t) => t.status === 'pending' || t.status === 'in_progress');
|
|
401
|
+
if (!hasActive) return;
|
|
402
|
+
const updated = todos.map((t) => ({
|
|
403
|
+
content: t.content,
|
|
404
|
+
status: (t.status === 'pending' || t.status === 'in_progress') ? 'cancelled' : t.status,
|
|
405
|
+
assignee: t.assignee,
|
|
406
|
+
}));
|
|
407
|
+
await this.client.putTodos(this.workspaceId, channel, this.token, updated, {
|
|
408
|
+
source: `openagents:${this.agentName}`,
|
|
409
|
+
});
|
|
410
|
+
} catch {
|
|
411
|
+
// Best-effort cleanup
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
async sendTodos(channel, todos) {
|
|
416
|
+
try {
|
|
417
|
+
await this.client.putTodos(this.workspaceId, channel, this.token, todos, {
|
|
418
|
+
source: `openagents:${this.agentName}`,
|
|
419
|
+
});
|
|
420
|
+
} catch (e) {
|
|
421
|
+
if (e instanceof SessionRevokedError) { this._onSessionRevoked(); return; }
|
|
422
|
+
// Fallback to event-based approach for older backends
|
|
423
|
+
const lines = todos.map((t) => {
|
|
424
|
+
const icon = t.status === 'completed' ? '✅' : t.status === 'in_progress' ? '🔄' : '⬜';
|
|
425
|
+
return `${icon} ${t.content}`;
|
|
426
|
+
});
|
|
427
|
+
try {
|
|
428
|
+
await this.client.sendMessage(this.workspaceId, channel, this.token, lines.join('\n'), {
|
|
429
|
+
senderType: 'agent',
|
|
430
|
+
senderName: this.agentName,
|
|
431
|
+
messageType: 'todos',
|
|
432
|
+
metadata: { agent_mode: this._mode, todos },
|
|
433
|
+
sessionId: this._sessionId,
|
|
434
|
+
});
|
|
435
|
+
} catch (e2) {
|
|
436
|
+
if (e2 instanceof SessionRevokedError) this._onSessionRevoked();
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
394
441
|
async sendError(channel, error) {
|
|
395
442
|
try {
|
|
396
443
|
await this.client.sendMessage(this.workspaceId, channel, this.token, error, {
|
package/src/adapters/claude.js
CHANGED
|
@@ -18,7 +18,7 @@ const { execSync, spawn } = require('child_process');
|
|
|
18
18
|
|
|
19
19
|
const BaseAdapter = require('./base');
|
|
20
20
|
const { formatAttachmentsForPrompt, SESSION_DEFAULT_RE, generateSessionTitle } = require('./utils');
|
|
21
|
-
const { buildClaudeSystemPrompt } = require('./workspace-prompt');
|
|
21
|
+
const { buildClaudeSystemPrompt, buildClaudeSkillMd } = require('./workspace-prompt');
|
|
22
22
|
|
|
23
23
|
const IS_WINDOWS = process.platform === 'win32';
|
|
24
24
|
|
|
@@ -31,6 +31,8 @@ class ClaudeAdapter extends BaseAdapter {
|
|
|
31
31
|
constructor(opts) {
|
|
32
32
|
super(opts);
|
|
33
33
|
this.disabledModules = opts.disabledModules || new Set();
|
|
34
|
+
/** @type {'mcp' | 'skills'} Tool integration mode */
|
|
35
|
+
this.toolMode = opts.toolMode || 'mcp';
|
|
34
36
|
this._channelSessions = {}; // channel → Claude CLI session_id
|
|
35
37
|
this._channelProcesses = {}; // channel → child process
|
|
36
38
|
this._stoppingChannels = new Set();
|
|
@@ -285,15 +287,81 @@ class ClaudeAdapter extends BaseAdapter {
|
|
|
285
287
|
throw new Error('claude CLI not found. Install with: curl -fsSL https://claude.ai/install.sh | bash');
|
|
286
288
|
}
|
|
287
289
|
|
|
288
|
-
|
|
290
|
+
let systemPrompt = '\n' + buildClaudeSystemPrompt({
|
|
289
291
|
agentName: this.agentName,
|
|
290
292
|
workspaceId: this.workspaceId,
|
|
291
293
|
channelName,
|
|
292
294
|
mode: this._mode,
|
|
293
295
|
});
|
|
294
296
|
|
|
297
|
+
// In skills mode, replace MCP tool references with curl-based instructions
|
|
298
|
+
if (this.toolMode === 'skills') {
|
|
299
|
+
systemPrompt = systemPrompt
|
|
300
|
+
.replace(
|
|
301
|
+
'Use workspace_get_history to read previous messages.\n' +
|
|
302
|
+
'Use workspace_get_agents to see other agents.\n',
|
|
303
|
+
'Use the openagents-workspace skill (Bash + curl) for workspace operations:\n' +
|
|
304
|
+
'reading message history, discovering agents, sharing files, and browsing.\n' +
|
|
305
|
+
'Refer to the skill instructions for the exact curl commands.\n'
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
295
309
|
const cmd = [claudeBin, '-p', prompt, '--output-format', 'stream-json', '--verbose'];
|
|
296
310
|
|
|
311
|
+
cmd.push('--append-system-prompt', systemPrompt);
|
|
312
|
+
cmd.push('--disallowedTools', 'AskUserQuestion');
|
|
313
|
+
|
|
314
|
+
// Resume existing conversation (skipped on retry after stale session)
|
|
315
|
+
const sessionId = this._channelSessions[channelName];
|
|
316
|
+
if (sessionId && !skipResume) {
|
|
317
|
+
cmd.push('--resume', sessionId);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// ── Skills mode: write SKILL.md, no MCP server ──
|
|
321
|
+
if (this.toolMode === 'skills') {
|
|
322
|
+
return this._buildSkillsCmd(cmd, channelName);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// ── MCP mode (default): spawn MCP server ──
|
|
326
|
+
return this._buildMcpCmd(cmd, channelName);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Skills mode: write a SKILL.md file and allow Bash + curl for workspace ops.
|
|
331
|
+
*/
|
|
332
|
+
_buildSkillsCmd(cmd, channelName) {
|
|
333
|
+
if (this._mode === 'plan') {
|
|
334
|
+
cmd.push('--permission-mode', 'plan');
|
|
335
|
+
cmd.push('--allowedTools', 'Read', 'Glob', 'Grep', 'Bash');
|
|
336
|
+
} else {
|
|
337
|
+
cmd.push('--dangerously-skip-permissions');
|
|
338
|
+
cmd.push('--allowedTools', 'Read', 'Write', 'Edit', 'Bash', 'Glob', 'Grep');
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Write SKILL.md to .claude/skills/ in the working directory
|
|
342
|
+
const workDir = this.workingDir || process.cwd();
|
|
343
|
+
const skillDir = path.join(workDir, '.claude', 'skills');
|
|
344
|
+
fs.mkdirSync(skillDir, { recursive: true });
|
|
345
|
+
const skillFile = path.join(skillDir, 'openagents-workspace.md');
|
|
346
|
+
|
|
347
|
+
const skillContent = buildClaudeSkillMd({
|
|
348
|
+
endpoint: this.endpoint,
|
|
349
|
+
workspaceId: this.workspaceId,
|
|
350
|
+
token: this.token,
|
|
351
|
+
agentName: this.agentName,
|
|
352
|
+
channelName,
|
|
353
|
+
disabledModules: this.disabledModules,
|
|
354
|
+
});
|
|
355
|
+
fs.writeFileSync(skillFile, skillContent, 'utf-8');
|
|
356
|
+
this._log(`Wrote workspace skill to ${skillFile}`);
|
|
357
|
+
|
|
358
|
+
return { cmd, skillFile };
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* MCP mode (default): spawn MCP server subprocess for workspace tools.
|
|
363
|
+
*/
|
|
364
|
+
_buildMcpCmd(cmd, channelName) {
|
|
297
365
|
// Mode-dependent permission and tool flags
|
|
298
366
|
const pfx = 'mcp__openagents-workspace__';
|
|
299
367
|
const mcpTools = [
|
|
@@ -326,23 +394,16 @@ class ClaudeAdapter extends BaseAdapter {
|
|
|
326
394
|
mcpWriteTools.push(`${pfx}tunnel_expose`, `${pfx}tunnel_close`);
|
|
327
395
|
}
|
|
328
396
|
|
|
329
|
-
|
|
397
|
+
// Todos & Timers (always enabled)
|
|
398
|
+
mcpTools.push(`${pfx}workspace_get_todos`, `${pfx}workspace_list_timers`);
|
|
399
|
+
mcpWriteTools.push(`${pfx}workspace_put_todos`, `${pfx}workspace_create_timer`, `${pfx}workspace_cancel_timer`);
|
|
400
|
+
|
|
330
401
|
if (this._mode === 'plan') {
|
|
331
402
|
cmd.push('--permission-mode', 'plan');
|
|
332
|
-
|
|
403
|
+
cmd.push('--allowedTools', ...mcpTools, 'Read', 'Glob', 'Grep');
|
|
333
404
|
} else {
|
|
334
405
|
cmd.push('--dangerously-skip-permissions');
|
|
335
|
-
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
cmd.push('--append-system-prompt', systemPrompt);
|
|
339
|
-
cmd.push('--allowedTools', ...allowed);
|
|
340
|
-
cmd.push('--disallowedTools', 'AskUserQuestion');
|
|
341
|
-
|
|
342
|
-
// Resume existing conversation (skipped on retry after stale session)
|
|
343
|
-
const sessionId = this._channelSessions[channelName];
|
|
344
|
-
if (sessionId && !skipResume) {
|
|
345
|
-
cmd.push('--resume', sessionId);
|
|
406
|
+
cmd.push('--allowedTools', ...mcpTools, ...mcpWriteTools, 'Read', 'Write', 'Edit', 'Bash', 'Glob', 'Grep');
|
|
346
407
|
}
|
|
347
408
|
|
|
348
409
|
// MCP config for workspace tools
|
|
@@ -356,18 +417,13 @@ class ClaudeAdapter extends BaseAdapter {
|
|
|
356
417
|
if (this.disabledModules.has('files')) mcpArgs.push('--disable-files');
|
|
357
418
|
if (this.disabledModules.has('browser')) mcpArgs.push('--disable-browser');
|
|
358
419
|
|
|
359
|
-
// Resolve the MCP server entry point
|
|
360
|
-
// very package — it's guaranteed to exist whenever claude.js is executing,
|
|
361
|
-
// so it never falls through to a broken PATH lookup. If Claude Code can't
|
|
362
|
-
// spawn the MCP server, it silently hides every workspace tool and the
|
|
363
|
-
// agent reports "workspace_read_file isn't in my tool set".
|
|
420
|
+
// Resolve the MCP server entry point
|
|
364
421
|
let mcpCommand = this._findNodeBin();
|
|
365
422
|
let mcpFinalArgs = mcpArgs;
|
|
366
423
|
const siblingBin = path.resolve(__dirname, '..', '..', 'bin', 'agent-connector.js');
|
|
367
424
|
if (fs.existsSync(siblingBin)) {
|
|
368
425
|
mcpFinalArgs = [siblingBin, ...mcpArgs];
|
|
369
426
|
} else {
|
|
370
|
-
// Fallback: search installed locations (older layouts, global installs)
|
|
371
427
|
let oaBin = null;
|
|
372
428
|
const home3 = os.homedir();
|
|
373
429
|
const oaExt = IS_WINDOWS ? '.cmd' : '';
|
|
@@ -475,16 +531,20 @@ class ClaudeAdapter extends BaseAdapter {
|
|
|
475
531
|
let mcpConfigFile = null;
|
|
476
532
|
let cmd;
|
|
477
533
|
|
|
478
|
-
// Clean env: strip
|
|
479
|
-
//
|
|
480
|
-
//
|
|
481
|
-
//
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
534
|
+
// Clean env: strip CLAUDE_* / AI_AGENT variables that make the spawned
|
|
535
|
+
// `claude` think it's running under an SDK harness (org-scoped auth
|
|
536
|
+
// path → 403). But preserve config vars the child needs for cloud
|
|
537
|
+
// provider auth (Vertex, Bedrock) and model selection.
|
|
538
|
+
const CLAUDE_ENV_KEEP = new Set([
|
|
539
|
+
'CLAUDE_CODE_USE_VERTEX',
|
|
540
|
+
'CLAUDE_CODE_USE_BEDROCK',
|
|
541
|
+
'CLAUDE_MODEL',
|
|
542
|
+
'CLAUDE_API_KEY',
|
|
543
|
+
'CLAUDE_CODE_MAX_TURNS',
|
|
544
|
+
]);
|
|
485
545
|
const cleanEnv = { ...(this.agentEnv || process.env) };
|
|
486
546
|
for (const k of Object.keys(cleanEnv)) {
|
|
487
|
-
if (k.startsWith('CLAUDE_') || k === 'CLAUDECODE' || k === 'AI_AGENT') {
|
|
547
|
+
if ((k.startsWith('CLAUDE_') && !CLAUDE_ENV_KEEP.has(k)) || k === 'CLAUDECODE' || k === 'AI_AGENT') {
|
|
488
548
|
delete cleanEnv[k];
|
|
489
549
|
}
|
|
490
550
|
}
|
|
@@ -538,6 +598,7 @@ class ClaudeAdapter extends BaseAdapter {
|
|
|
538
598
|
const lastResponseText = [];
|
|
539
599
|
let hasToolUseSinceLastText = false;
|
|
540
600
|
let postedThinking = false;
|
|
601
|
+
let everPostedAnything = false;
|
|
541
602
|
let stderrBuf = '';
|
|
542
603
|
let lineBuffer = '';
|
|
543
604
|
let _pendingLines = Promise.resolve(); // chain of in-flight processLine calls
|
|
@@ -597,6 +658,7 @@ class ClaudeAdapter extends BaseAdapter {
|
|
|
597
658
|
}
|
|
598
659
|
lastResponseText.push(block.text.trim());
|
|
599
660
|
postedThinking = true;
|
|
661
|
+
everPostedAnything = true;
|
|
600
662
|
// Stream text in real-time as "thinking" (same as Python adapter)
|
|
601
663
|
try { await this.sendThinking(msgChannel, block.text.trim()); } catch {}
|
|
602
664
|
} else if (block.type === 'tool_use') {
|
|
@@ -604,6 +666,7 @@ class ClaudeAdapter extends BaseAdapter {
|
|
|
604
666
|
postedThinking = false;
|
|
605
667
|
lastResponseText.length = 0;
|
|
606
668
|
const toolName = block.name || '';
|
|
669
|
+
|
|
607
670
|
// Format tool input as readable text
|
|
608
671
|
let inputPreview = '';
|
|
609
672
|
if (block.input && typeof block.input === 'object') {
|
|
@@ -620,6 +683,7 @@ class ClaudeAdapter extends BaseAdapter {
|
|
|
620
683
|
inputPreview = String(block.input || '').slice(0, 150);
|
|
621
684
|
}
|
|
622
685
|
await this.sendStatus(msgChannel, `${toolName} › ${inputPreview}`);
|
|
686
|
+
everPostedAnything = true;
|
|
623
687
|
}
|
|
624
688
|
}
|
|
625
689
|
} else if (eventType === 'result') {
|
|
@@ -655,6 +719,10 @@ class ClaudeAdapter extends BaseAdapter {
|
|
|
655
719
|
}
|
|
656
720
|
|
|
657
721
|
delete this._channelProcesses[msgChannel];
|
|
722
|
+
|
|
723
|
+
// Clean up stale todos: mark pending/in_progress as cancelled
|
|
724
|
+
try { await this.cleanupTodos(msgChannel); } catch {}
|
|
725
|
+
|
|
658
726
|
const stoppedByUser = this._stoppingChannels.has(msgChannel);
|
|
659
727
|
if (stoppedByUser) {
|
|
660
728
|
this._stoppingChannels.delete(msgChannel);
|
|
@@ -662,8 +730,8 @@ class ClaudeAdapter extends BaseAdapter {
|
|
|
662
730
|
return;
|
|
663
731
|
}
|
|
664
732
|
|
|
733
|
+
this._log(`CLI exited: code=${code}, lastResponseText=${lastResponseText.length} items, everPosted=${everPostedAnything}, hasSession=${!!this._channelSessions[msgChannel]}`);
|
|
665
734
|
if (code !== 0) {
|
|
666
|
-
this._log(`CLI exited with code ${code}`);
|
|
667
735
|
if (stderrBuf.trim()) {
|
|
668
736
|
this._log(`stderr: ${stderrBuf.trim().slice(0, 500)}`);
|
|
669
737
|
}
|
|
@@ -675,14 +743,16 @@ class ClaudeAdapter extends BaseAdapter {
|
|
|
675
743
|
try { await this.sendResponse(msgChannel, fullResponse); } catch {}
|
|
676
744
|
}
|
|
677
745
|
resolve(false); // done, no retry
|
|
678
|
-
} else if (
|
|
679
|
-
// No output +
|
|
746
|
+
} else if (this._channelSessions[msgChannel] && !everPostedAnything) {
|
|
747
|
+
// No output + had a resume session → likely stale session, retry fresh.
|
|
748
|
+
// Covers both error exits (code !== 0) and silent success (code === 0)
|
|
749
|
+
// where Claude produced no text output on a resumed conversation.
|
|
680
750
|
this._log(`Stale session detected for ${msgChannel}, clearing and retrying without resume`);
|
|
681
751
|
delete this._channelSessions[msgChannel];
|
|
682
752
|
this._saveSessions();
|
|
683
753
|
resolve(true); // retry=true
|
|
684
754
|
} else {
|
|
685
|
-
if (!
|
|
755
|
+
if (!everPostedAnything) {
|
|
686
756
|
try { await this.sendResponse(msgChannel, 'No response generated. Please try again.'); } catch {}
|
|
687
757
|
}
|
|
688
758
|
resolve(false);
|
|
@@ -205,9 +205,73 @@ function buildApiSkillsPrompt({ endpoint, workspaceId, token, agentName, channel
|
|
|
205
205
|
sections.push(s);
|
|
206
206
|
}
|
|
207
207
|
|
|
208
|
+
// Message history
|
|
209
|
+
sections.push(
|
|
210
|
+
'\n### Message History\n\n' +
|
|
211
|
+
'**Get recent messages in the current channel:**\n' +
|
|
212
|
+
`\`curl -s -H "${h}" "${baseUrl}/v1/events?network=${workspaceId}&channel=${channelName}&type=workspace.message&limit=20"\`\n\n` +
|
|
213
|
+
'**Get messages from a specific channel:**\n' +
|
|
214
|
+
`\`curl -s -H "${h}" "${baseUrl}/v1/events?network=${workspaceId}&channel=CHANNEL_NAME&type=workspace.message&limit=20"\`\n`
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
// Post status update
|
|
218
|
+
if (!isPlan) {
|
|
219
|
+
sections.push(
|
|
220
|
+
'\n### Post Status Update\n\n' +
|
|
221
|
+
'Post a status/thinking message (visible in the workspace UI as an intermediate step):\n' +
|
|
222
|
+
`\`curl -s -X POST -H "${h}" -H "Content-Type: application/json" ` +
|
|
223
|
+
`${baseUrl}/v1/events -d '{"type":"workspace.message.posted",` +
|
|
224
|
+
`"source":"openagents:${agentName}","target":"channel/${channelName}",` +
|
|
225
|
+
`"payload":{"content":"YOUR_STATUS","message_type":"status"}}'\`\n`
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// To-Dos (planning)
|
|
230
|
+
if (!isPlan) {
|
|
231
|
+
sections.push(
|
|
232
|
+
'\n### To-Do List (Planning)\n\n' +
|
|
233
|
+
'Create or update your to-do list to track progress. The entire list ' +
|
|
234
|
+
'is replaced each time (send the full list with current statuses).\n\n' +
|
|
235
|
+
'**Status values:** `pending`, `in_progress`, `completed`\n\n' +
|
|
236
|
+
'**Update your to-do list:**\n' +
|
|
237
|
+
`\`curl -s -X PUT -H "${h}" -H "Content-Type: application/json" ` +
|
|
238
|
+
`${baseUrl}/v1/todos -d '{"todos":[` +
|
|
239
|
+
`{"content":"First task","status":"in_progress"},` +
|
|
240
|
+
`{"content":"Second task","status":"pending"}` +
|
|
241
|
+
`],"network":"${workspaceId}","channel":"${channelName}",` +
|
|
242
|
+
`"source":"openagents:${agentName}"}'\`\n\n` +
|
|
243
|
+
'**Get your to-do list:**\n' +
|
|
244
|
+
`\`curl -s -H "${h}" "${baseUrl}/v1/todos?network=${workspaceId}&channel=${channelName}"\`\n\n` +
|
|
245
|
+
'Use to-dos to plan multi-step work. Update the list as you complete each step.\n' +
|
|
246
|
+
'You can assign items to other agents: `"assignee": "other-agent-name"`\n'
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Timers
|
|
251
|
+
if (!isPlan) {
|
|
252
|
+
sections.push(
|
|
253
|
+
'\n### Timers\n\n' +
|
|
254
|
+
'Set a timer that will send you a message after a delay, waking you up ' +
|
|
255
|
+
'to continue work. Use this instead of `sleep` — timers let you release ' +
|
|
256
|
+
'the session and get called back later.\n\n' +
|
|
257
|
+
'Use cases: check back on a deploy, retry after a rate limit, remind ' +
|
|
258
|
+
'yourself to follow up.\n\n' +
|
|
259
|
+
'**Create a timer:**\n' +
|
|
260
|
+
`\`curl -s -X POST -H "${h}" -H "Content-Type: application/json" ` +
|
|
261
|
+
`${baseUrl}/v1/timers -d '{"delay":300,"message":"Check the build",` +
|
|
262
|
+
`"network":"${workspaceId}","channel":"${channelName}",` +
|
|
263
|
+
`"source":"openagents:${agentName}"}'\`\n\n` +
|
|
264
|
+
'**List active timers:**\n' +
|
|
265
|
+
`\`curl -s -H "${h}" "${baseUrl}/v1/timers?network=${workspaceId}&channel=${channelName}"\`\n\n` +
|
|
266
|
+
'**Cancel a timer:**\n' +
|
|
267
|
+
`\`curl -s -X DELETE -H "${h}" ${baseUrl}/v1/timers/TIMER_ID\`\n`
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
208
271
|
// Discovery
|
|
209
272
|
sections.push(
|
|
210
|
-
'\n### Discover Agents\n' +
|
|
273
|
+
'\n### Discover Agents\n\n' +
|
|
274
|
+
'**List all agents in the workspace (with status and role):**\n' +
|
|
211
275
|
`\`curl -s -H "${h}" ${baseUrl}/v1/discover?network=${workspaceId}\`\n`
|
|
212
276
|
);
|
|
213
277
|
|
|
@@ -321,6 +385,37 @@ function buildOpenCodeSkillMd({ endpoint, workspaceId, token, agentName, channel
|
|
|
321
385
|
return frontmatter + identity + api;
|
|
322
386
|
}
|
|
323
387
|
|
|
388
|
+
/**
|
|
389
|
+
* Build a SKILL.md file for Claude Code's skill auto-discovery.
|
|
390
|
+
*
|
|
391
|
+
* When tool_mode is 'skills', the Claude adapter writes this file instead
|
|
392
|
+
* of spawning an MCP server. Claude Code discovers the skill via its
|
|
393
|
+
* .claude/skills/ directory and uses Bash + curl to call workspace APIs.
|
|
394
|
+
*/
|
|
395
|
+
function buildClaudeSkillMd({ endpoint, workspaceId, token, agentName, channelName, disabledModules }) {
|
|
396
|
+
const api = buildApiSkillsPrompt({
|
|
397
|
+
endpoint, workspaceId, token, agentName,
|
|
398
|
+
channelName: channelName || 'general',
|
|
399
|
+
disabledModules,
|
|
400
|
+
mode: 'execute',
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
const identity = buildWorkspaceIdentity(agentName, workspaceId, channelName, 'execute');
|
|
404
|
+
const collab = buildCollaborationPrompt();
|
|
405
|
+
|
|
406
|
+
const frontmatter =
|
|
407
|
+
'---\n' +
|
|
408
|
+
'name: openagents-workspace\n' +
|
|
409
|
+
'description: |\n' +
|
|
410
|
+
' OpenAgents Workspace collaboration tools — shared files, browser,\n' +
|
|
411
|
+
' and multi-agent coordination. Use when: sharing files or reports,\n' +
|
|
412
|
+
' browsing websites, reading shared files, checking workspace agents,\n' +
|
|
413
|
+
' or collaborating with other agents via @mentions.\n' +
|
|
414
|
+
'---\n\n';
|
|
415
|
+
|
|
416
|
+
return frontmatter + identity + '\n' + collab + '\n' + api;
|
|
417
|
+
}
|
|
418
|
+
|
|
324
419
|
module.exports = {
|
|
325
420
|
buildWorkspaceIdentity,
|
|
326
421
|
buildCollaborationPrompt,
|
|
@@ -331,4 +426,5 @@ module.exports = {
|
|
|
331
426
|
buildOpenclawSkillMd,
|
|
332
427
|
buildOpenCodeSystemPrompt,
|
|
333
428
|
buildOpenCodeSkillMd,
|
|
429
|
+
buildClaudeSkillMd,
|
|
334
430
|
};
|
package/src/cli.js
CHANGED
|
@@ -440,6 +440,49 @@ async function cmdEnv(connector, flags, positional) {
|
|
|
440
440
|
}
|
|
441
441
|
}
|
|
442
442
|
|
|
443
|
+
async function cmdToolMode(connector, _flags, positional) {
|
|
444
|
+
const agentName = positional[0];
|
|
445
|
+
const mode = positional[1];
|
|
446
|
+
|
|
447
|
+
if (!agentName) {
|
|
448
|
+
// Show tool mode for all agents
|
|
449
|
+
const agents = connector.config.getAgents();
|
|
450
|
+
if (agents.length === 0) {
|
|
451
|
+
print('No agents configured');
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
for (const a of agents) {
|
|
455
|
+
print(` ${a.name}: ${a.tool_mode || 'mcp'}`);
|
|
456
|
+
}
|
|
457
|
+
print('\nUsage: agn tool-mode <agent> <mcp|skills>');
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if (!mode) {
|
|
462
|
+
// Show tool mode for specific agent
|
|
463
|
+
const agent = connector.config.getAgent(agentName);
|
|
464
|
+
if (!agent) { print(`Agent '${agentName}' not found`); process.exitCode = 1; return; }
|
|
465
|
+
print(`${agentName}: ${agent.tool_mode || 'mcp'}`);
|
|
466
|
+
print('\nUsage: agn tool-mode <agent> <mcp|skills>');
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
if (mode !== 'mcp' && mode !== 'skills') {
|
|
471
|
+
print(`Invalid mode: ${mode}. Must be 'mcp' or 'skills'.`);
|
|
472
|
+
process.exitCode = 1;
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
connector.config.updateAgent(agentName, { tool_mode: mode });
|
|
477
|
+
try { connector.sendDaemonCommand('reload'); } catch {}
|
|
478
|
+
print(`Set tool mode for ${agentName} to '${mode}'`);
|
|
479
|
+
if (mode === 'skills') {
|
|
480
|
+
print('Agent will use SKILL.md (Bash + curl) instead of MCP server for workspace tools.');
|
|
481
|
+
} else {
|
|
482
|
+
print('Agent will use MCP server for workspace tools (default).');
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
443
486
|
async function cmdTestLLM(connector, _flags, positional) {
|
|
444
487
|
const type = positional[0];
|
|
445
488
|
if (!type) { print('Usage: agn test-llm <type>'); return; }
|
|
@@ -504,6 +547,7 @@ Commands:
|
|
|
504
547
|
connect <agent> <token> Connect agent to workspace
|
|
505
548
|
disconnect <agent> Disconnect agent from workspace
|
|
506
549
|
env <type> [--set K=V] View/set env vars for agent type
|
|
550
|
+
tool-mode [agent] [mode] View/set tool mode (mcp or skills)
|
|
507
551
|
autostart [--disable] Enable/disable auto-start on login
|
|
508
552
|
test-llm <type> Test LLM connection
|
|
509
553
|
logs [agent] [--lines N] View daemon logs
|
|
@@ -589,6 +633,7 @@ async function main() {
|
|
|
589
633
|
autostart: () => cmdAutostart(connector, flags),
|
|
590
634
|
workspace: () => cmdWorkspace(connector, flags, positional),
|
|
591
635
|
env: () => cmdEnv(connector, flags, positional),
|
|
636
|
+
'tool-mode': () => cmdToolMode(connector, flags, positional),
|
|
592
637
|
'test-llm': () => cmdTestLLM(connector, flags, positional),
|
|
593
638
|
update: () => cmdUpdate(),
|
|
594
639
|
'mcp-server': () => {
|
package/src/daemon.js
CHANGED
|
@@ -433,6 +433,7 @@ class Daemon {
|
|
|
433
433
|
disabledModules: new Set(),
|
|
434
434
|
agentEnv: this._buildAgentEnv(agentCfg),
|
|
435
435
|
workingDir: agentCfg.path || undefined,
|
|
436
|
+
toolMode: agentCfg.tool_mode || 'mcp',
|
|
436
437
|
});
|
|
437
438
|
} catch (e) {
|
|
438
439
|
this._log(`${name} failed to create ${agentType} adapter: ${e.message}`);
|
package/src/mcp-server.js
CHANGED
|
@@ -235,6 +235,72 @@ function buildToolDefs(disabledModules) {
|
|
|
235
235
|
);
|
|
236
236
|
}
|
|
237
237
|
|
|
238
|
+
// -- Todos & Timers (always enabled) --
|
|
239
|
+
tools.push(
|
|
240
|
+
{
|
|
241
|
+
name: 'workspace_put_todos',
|
|
242
|
+
description: 'Update your to-do list. Replaces the entire list each time (send full list with current statuses). Channel is auto-resolved.',
|
|
243
|
+
inputSchema: {
|
|
244
|
+
type: 'object',
|
|
245
|
+
properties: {
|
|
246
|
+
todos: {
|
|
247
|
+
type: 'array',
|
|
248
|
+
items: {
|
|
249
|
+
type: 'object',
|
|
250
|
+
properties: {
|
|
251
|
+
content: { type: 'string', description: 'Task description' },
|
|
252
|
+
status: { type: 'string', enum: ['pending', 'in_progress', 'completed'], description: 'Task status' },
|
|
253
|
+
assignee: { type: 'string', description: 'Agent name to assign to (defaults to self)' },
|
|
254
|
+
},
|
|
255
|
+
required: ['content', 'status'],
|
|
256
|
+
},
|
|
257
|
+
description: 'Full to-do list with current statuses',
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
required: ['todos'],
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
name: 'workspace_get_todos',
|
|
265
|
+
description: 'Get to-do items for the current channel. Use all=true to see all agents\' todos.',
|
|
266
|
+
inputSchema: {
|
|
267
|
+
type: 'object',
|
|
268
|
+
properties: {
|
|
269
|
+
agent: { type: 'string', description: 'Filter by agent name' },
|
|
270
|
+
all: { type: 'boolean', description: 'Get all agents\' todos (default: own only)' },
|
|
271
|
+
},
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
name: 'workspace_create_timer',
|
|
276
|
+
description: 'Set a timer that posts a message to the channel after the specified delay.',
|
|
277
|
+
inputSchema: {
|
|
278
|
+
type: 'object',
|
|
279
|
+
properties: {
|
|
280
|
+
delay: { type: 'integer', description: 'Seconds until the timer fires (1-86400)' },
|
|
281
|
+
message: { type: 'string', description: 'Message to post when the timer fires' },
|
|
282
|
+
},
|
|
283
|
+
required: ['delay', 'message'],
|
|
284
|
+
},
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
name: 'workspace_list_timers',
|
|
288
|
+
description: 'List active timers in the current channel.',
|
|
289
|
+
inputSchema: { type: 'object', properties: {} },
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
name: 'workspace_cancel_timer',
|
|
293
|
+
description: 'Cancel an active timer by its ID.',
|
|
294
|
+
inputSchema: {
|
|
295
|
+
type: 'object',
|
|
296
|
+
properties: {
|
|
297
|
+
timer_id: { type: 'string', description: 'Timer ID to cancel' },
|
|
298
|
+
},
|
|
299
|
+
required: ['timer_id'],
|
|
300
|
+
},
|
|
301
|
+
},
|
|
302
|
+
);
|
|
303
|
+
|
|
238
304
|
return tools;
|
|
239
305
|
}
|
|
240
306
|
|
|
@@ -596,6 +662,56 @@ class McpServer {
|
|
|
596
662
|
return text(lines.join('\n'));
|
|
597
663
|
}
|
|
598
664
|
|
|
665
|
+
// ── Todos & Timers ──
|
|
666
|
+
|
|
667
|
+
case 'workspace_put_todos': {
|
|
668
|
+
await this.ws.putTodos(this.workspaceId, this.channelName, this.token, args.todos, {
|
|
669
|
+
source: `openagents:${this.agentName}`,
|
|
670
|
+
});
|
|
671
|
+
const summary = (args.todos || []).map((t) => {
|
|
672
|
+
const icon = t.status === 'completed' ? '[x]' : t.status === 'in_progress' ? '[~]' : '[ ]';
|
|
673
|
+
return `${icon} ${t.content}${t.assignee ? ` → ${t.assignee}` : ''}`;
|
|
674
|
+
}).join('\n');
|
|
675
|
+
return text(`Todos updated:\n${summary}`);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
case 'workspace_get_todos': {
|
|
679
|
+
const data = await this.ws.getTodos(this.workspaceId, this.channelName, this.token, {
|
|
680
|
+
agent: args.agent, all: args.all,
|
|
681
|
+
});
|
|
682
|
+
const todos = (data && data.todos) || [];
|
|
683
|
+
if (!todos.length) return text('No todos.');
|
|
684
|
+
const lines = todos.map((t) => {
|
|
685
|
+
const icon = t.status === 'completed' ? '[x]' : t.status === 'in_progress' ? '[~]' : '[ ]';
|
|
686
|
+
return `${icon} ${t.content} (${t.assignee || 'unassigned'})`;
|
|
687
|
+
});
|
|
688
|
+
return text(lines.join('\n'));
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
case 'workspace_create_timer': {
|
|
692
|
+
const result = await this.ws.createTimer(
|
|
693
|
+
this.workspaceId, this.channelName, this.token,
|
|
694
|
+
args.delay, args.message,
|
|
695
|
+
{ source: `openagents:${this.agentName}` },
|
|
696
|
+
);
|
|
697
|
+
return text(`Timer set: "${args.message}" fires in ${args.delay}s (id: ${result.id})`);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
case 'workspace_list_timers': {
|
|
701
|
+
const data = await this.ws.listTimers(this.workspaceId, this.channelName, this.token);
|
|
702
|
+
const timers = (data && data.timers) || [];
|
|
703
|
+
if (!timers.length) return text('No active timers.');
|
|
704
|
+
const lines = timers.map((t) =>
|
|
705
|
+
`- ${t.id}: "${t.message}" fires at ${t.fires_at} (by ${t.created_by})`
|
|
706
|
+
);
|
|
707
|
+
return text(lines.join('\n'));
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
case 'workspace_cancel_timer': {
|
|
711
|
+
await this.ws.cancelTimer(this.workspaceId, this.token, args.timer_id);
|
|
712
|
+
return text(`Timer cancelled: ${args.timer_id}`);
|
|
713
|
+
}
|
|
714
|
+
|
|
599
715
|
default:
|
|
600
716
|
throw new Error(`Unknown tool: ${name}`);
|
|
601
717
|
}
|
package/src/workspace-client.js
CHANGED
|
@@ -293,6 +293,11 @@ class WorkspaceClient {
|
|
|
293
293
|
// Legacy server (no target_agents): broadcast for compat
|
|
294
294
|
messages.push(this._eventToMessage(e));
|
|
295
295
|
}
|
|
296
|
+
} else if (source.startsWith('system:')) {
|
|
297
|
+
// System messages (timers, notifications): pick up if targeted
|
|
298
|
+
if (hasTargetList && targetAgents.includes(agentName)) {
|
|
299
|
+
messages.push(this._eventToMessage(e));
|
|
300
|
+
}
|
|
296
301
|
} else if (source.startsWith('openagents:')) {
|
|
297
302
|
// Agent messages: only pick up if explicitly listed
|
|
298
303
|
if (hasTargetList && targetAgents.includes(agentName)) {
|
|
@@ -524,6 +529,53 @@ class WorkspaceClient {
|
|
|
524
529
|
return data.data || data;
|
|
525
530
|
}
|
|
526
531
|
|
|
532
|
+
// ── Todos & Timers ──
|
|
533
|
+
|
|
534
|
+
async putTodos(workspaceId, channelName, token, todos, { source } = {}) {
|
|
535
|
+
const body = {
|
|
536
|
+
todos,
|
|
537
|
+
network: workspaceId,
|
|
538
|
+
channel: channelName,
|
|
539
|
+
source: source || 'openagents:unknown',
|
|
540
|
+
};
|
|
541
|
+
const data = await this._put('/v1/todos', body, this._wsHeaders(token));
|
|
542
|
+
return data.data || data;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
async getTodos(workspaceId, channelName, token, { agent, all } = {}) {
|
|
546
|
+
const params = new URLSearchParams({ network: workspaceId });
|
|
547
|
+
if (channelName) params.set('channel', channelName);
|
|
548
|
+
if (agent) params.set('agent', agent);
|
|
549
|
+
if (all) params.set('all', 'true');
|
|
550
|
+
const data = await this._get(`/v1/todos?${params}`, this._wsHeaders(token));
|
|
551
|
+
return data.data || data;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
async createTimer(workspaceId, channelName, token, delay, message, { source } = {}) {
|
|
555
|
+
const body = {
|
|
556
|
+
delay,
|
|
557
|
+
message,
|
|
558
|
+
network: workspaceId,
|
|
559
|
+
channel: channelName,
|
|
560
|
+
source: source || 'openagents:unknown',
|
|
561
|
+
};
|
|
562
|
+
const data = await this._post('/v1/timers', body, this._wsHeaders(token));
|
|
563
|
+
return data.data || data;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
async listTimers(workspaceId, channelName, token) {
|
|
567
|
+
const params = new URLSearchParams({ network: workspaceId });
|
|
568
|
+
if (channelName) params.set('channel', channelName);
|
|
569
|
+
const data = await this._get(`/v1/timers?${params}`, this._wsHeaders(token));
|
|
570
|
+
return data.data || data;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
async cancelTimer(workspaceId, token, timerId, network) {
|
|
574
|
+
const params = network ? `?network=${network}` : '';
|
|
575
|
+
const data = await this._delete(`/v1/timers/${timerId}`, this._wsHeaders(token));
|
|
576
|
+
return data.data || data;
|
|
577
|
+
}
|
|
578
|
+
|
|
527
579
|
// ── Internal helpers ──
|
|
528
580
|
|
|
529
581
|
/**
|
|
@@ -669,6 +721,48 @@ class WorkspaceClient {
|
|
|
669
721
|
});
|
|
670
722
|
}
|
|
671
723
|
|
|
724
|
+
_put(urlPath, body, headers = {}, timeout = 30000) {
|
|
725
|
+
if (!headers['Content-Type']) headers['Content-Type'] = 'application/json';
|
|
726
|
+
const jsonBody = JSON.stringify(body);
|
|
727
|
+
const fullUrl = this.endpoint + urlPath;
|
|
728
|
+
|
|
729
|
+
return new Promise((resolve, reject) => {
|
|
730
|
+
const parsedUrl = new URL(fullUrl);
|
|
731
|
+
const transport = parsedUrl.protocol === 'https:' ? https : http;
|
|
732
|
+
|
|
733
|
+
const req = transport.request(fullUrl, {
|
|
734
|
+
method: 'PUT',
|
|
735
|
+
headers: { ...headers, 'Content-Length': Buffer.byteLength(jsonBody) },
|
|
736
|
+
timeout,
|
|
737
|
+
}, (res) => {
|
|
738
|
+
let data = '';
|
|
739
|
+
res.on('data', (chunk) => { data += chunk; });
|
|
740
|
+
res.on('end', () => {
|
|
741
|
+
try {
|
|
742
|
+
const parsed = JSON.parse(data);
|
|
743
|
+
if (res.statusCode >= 400) {
|
|
744
|
+
const msg = parsed.message || `HTTP ${res.statusCode}`;
|
|
745
|
+
if (typeof msg === 'string' && msg.toLowerCase().includes('session_revoked')) {
|
|
746
|
+
reject(new SessionRevokedError(msg));
|
|
747
|
+
} else {
|
|
748
|
+
reject(new Error(msg));
|
|
749
|
+
}
|
|
750
|
+
} else {
|
|
751
|
+
resolve(parsed);
|
|
752
|
+
}
|
|
753
|
+
} catch {
|
|
754
|
+
reject(new Error(`Invalid response: ${data.slice(0, 200)}`));
|
|
755
|
+
}
|
|
756
|
+
});
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
req.on('error', reject);
|
|
760
|
+
req.on('timeout', () => { req.destroy(); reject(new Error('Request timed out')); });
|
|
761
|
+
req.write(jsonBody);
|
|
762
|
+
req.end();
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
|
|
672
766
|
_patch(urlPath, body, headers = {}, timeout = 15000) {
|
|
673
767
|
if (!headers['Content-Type']) headers['Content-Type'] = 'application/json';
|
|
674
768
|
const jsonBody = JSON.stringify(body);
|