a2acalling 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # 🤝 A2A Calling
2
2
 
3
- **Agent-to-Agent calling with OpenClaw support. Let your people talk to my people!**
3
+ **Agent-to-Agent calling with OpenClaw support. "I'll have my people call your people!"**
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/a2acalling.svg)](https://www.npmjs.com/package/a2acalling)
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "a2acalling",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Agent-to-agent calling for OpenClaw - federated agent communication",
5
5
  "main": "src/index.js",
6
6
  "bin": {
package/src/lib/tokens.js CHANGED
@@ -257,9 +257,9 @@ class TokenStore {
257
257
  options = { name: options };
258
258
  }
259
259
 
260
- const match = inviteUrl.match(/^oclaw:\/\/([^/]+)\/(.+)$/);
260
+ const match = inviteUrl.match(/^(?:a2a|oclaw):\/\/([^/]+)\/(.+)$/);
261
261
  if (!match) {
262
- throw new Error(`Invalid invite URL: ${inviteUrl}`);
262
+ throw new Error(`Invalid invite URL: ${inviteUrl}. Expected format: a2a://host/token`);
263
263
  }
264
264
 
265
265
  const [, host, token] = match;
package/src/server.js CHANGED
@@ -2,7 +2,7 @@
2
2
  /**
3
3
  * A2A Federation Server
4
4
  *
5
- * Standalone server for testing or running alongside OpenClaw.
5
+ * Routes federation calls to an LLM agent.
6
6
  *
7
7
  * Usage:
8
8
  * node src/server.js [--port 3001]
@@ -10,11 +10,208 @@
10
10
  */
11
11
 
12
12
  const express = require('express');
13
+ const https = require('https');
14
+ const fs = require('fs');
15
+ const path = require('path');
13
16
  const { createRoutes } = require('./routes/federation');
14
17
  const { TokenStore } = require('./lib/tokens');
15
18
 
16
19
  const port = process.env.PORT || parseInt(process.argv[2]) || 3001;
17
20
 
21
+ // Load API key from various sources
22
+ function getApiKey() {
23
+ // Check environment first
24
+ if (process.env.OPENROUTER_API_KEY) {
25
+ return { key: process.env.OPENROUTER_API_KEY, provider: 'openrouter' };
26
+ }
27
+ if (process.env.ANTHROPIC_API_KEY) {
28
+ return { key: process.env.ANTHROPIC_API_KEY, provider: 'anthropic' };
29
+ }
30
+
31
+ // Try ~/.openclaw/.env
32
+ try {
33
+ const envPath = path.join(process.env.HOME || '/root', '.openclaw', '.env');
34
+ if (fs.existsSync(envPath)) {
35
+ const content = fs.readFileSync(envPath, 'utf8');
36
+
37
+ // Try OpenRouter first (more reliable)
38
+ const orMatch = content.match(/OPENROUTER_API_KEY=(.+)/);
39
+ if (orMatch && orMatch[1]) return { key: orMatch[1].trim(), provider: 'openrouter' };
40
+
41
+ const anthropicMatch = content.match(/ANTHROPIC_API_KEY=(.+)/);
42
+ if (anthropicMatch && anthropicMatch[1]) return { key: anthropicMatch[1].trim(), provider: 'anthropic' };
43
+ }
44
+ } catch (e) {}
45
+
46
+ return null;
47
+ }
48
+
49
+ // Load workspace context for agent personality
50
+ function loadAgentContext() {
51
+ const workspaceDir = process.env.OPENCLAW_WORKSPACE || '/root/clawd';
52
+ let context = {
53
+ name: 'bappybot',
54
+ owner: 'Ben Pollack'
55
+ };
56
+
57
+ try {
58
+ const userPath = path.join(workspaceDir, 'USER.md');
59
+ if (fs.existsSync(userPath)) {
60
+ const content = fs.readFileSync(userPath, 'utf8');
61
+ const nameMatch = content.match(/\*\*Name:\*\*\s*([^\n]+)/);
62
+ if (nameMatch) {
63
+ const name = nameMatch[1].trim();
64
+ if (name && !name.includes('_') && !name.includes('(')) {
65
+ context.owner = name;
66
+ }
67
+ }
68
+ }
69
+ } catch (e) {}
70
+
71
+ try {
72
+ const soulPath = path.join(workspaceDir, 'SOUL.md');
73
+ if (fs.existsSync(soulPath)) {
74
+ context.soul = fs.readFileSync(soulPath, 'utf8').slice(0, 2000);
75
+ }
76
+ } catch (e) {}
77
+
78
+ return context;
79
+ }
80
+
81
+ const apiConfig = getApiKey();
82
+ const agentContext = loadAgentContext();
83
+
84
+ console.log(`[a2a] Agent: ${agentContext.name} (${agentContext.owner}'s agent)`);
85
+ console.log(`[a2a] API: ${apiConfig ? `${apiConfig.provider} ✓` : 'NOT FOUND ✗'}`);
86
+
87
+ /**
88
+ * Call LLM via OpenRouter or Anthropic
89
+ */
90
+ async function callAgent(message, federationContext) {
91
+ if (!apiConfig) {
92
+ return '[Agent configuration error: No API key available]';
93
+ }
94
+
95
+ const callerName = federationContext.caller?.name || 'Unknown Agent';
96
+ const callerOwner = federationContext.caller?.owner || '';
97
+ const ownerInfo = callerOwner ? ` (${callerOwner}'s agent)` : '';
98
+ const tierInfo = federationContext.tier || 'public';
99
+
100
+ const systemPrompt = `You are ${agentContext.name}, ${agentContext.owner}'s AI agent.
101
+
102
+ ${agentContext.soul || 'Be helpful, concise, and friendly.'}
103
+
104
+ You're receiving a federated call from another AI agent: ${callerName}${ownerInfo}.
105
+
106
+ Their access level: ${tierInfo}
107
+ Topics they can discuss: ${federationContext.allowed_topics?.join(', ') || 'general chat'}
108
+ Disclosure level: ${federationContext.disclosure || 'minimal'}
109
+
110
+ Respond naturally as yourself. Be collaborative but protect your owner's private information based on the disclosure level. Keep responses concise.`;
111
+
112
+ // Use OpenRouter or Anthropic based on config
113
+ const isOpenRouter = apiConfig.provider === 'openrouter';
114
+ const hostname = isOpenRouter ? 'openrouter.ai' : 'api.anthropic.com';
115
+ const apiPath = isOpenRouter ? '/api/v1/chat/completions' : '/v1/messages';
116
+ const model = isOpenRouter ? 'anthropic/claude-sonnet-4' : 'claude-sonnet-4-20250514';
117
+
118
+ const body = isOpenRouter
119
+ ? JSON.stringify({
120
+ model,
121
+ max_tokens: 1024,
122
+ messages: [
123
+ { role: 'system', content: systemPrompt },
124
+ { role: 'user', content: message }
125
+ ]
126
+ })
127
+ : JSON.stringify({
128
+ model,
129
+ max_tokens: 1024,
130
+ system: systemPrompt,
131
+ messages: [{ role: 'user', content: message }]
132
+ });
133
+
134
+ const headers = isOpenRouter
135
+ ? {
136
+ 'Content-Type': 'application/json',
137
+ 'Authorization': `Bearer ${apiConfig.key}`,
138
+ 'HTTP-Referer': 'https://openclaw.ai',
139
+ 'X-Title': 'A2A Federation'
140
+ }
141
+ : {
142
+ 'Content-Type': 'application/json',
143
+ 'x-api-key': apiConfig.key,
144
+ 'anthropic-version': '2023-06-01'
145
+ };
146
+
147
+ return new Promise((resolve) => {
148
+ const req = https.request({
149
+ hostname,
150
+ path: apiPath,
151
+ method: 'POST',
152
+ headers,
153
+ timeout: 55000
154
+ }, (res) => {
155
+ let data = '';
156
+ res.on('data', chunk => data += chunk);
157
+ res.on('end', () => {
158
+ try {
159
+ const json = JSON.parse(data);
160
+
161
+ // OpenRouter format
162
+ if (json.choices && json.choices[0]?.message?.content) {
163
+ resolve(json.choices[0].message.content);
164
+ return;
165
+ }
166
+
167
+ // Anthropic format
168
+ if (json.content && json.content[0]?.text) {
169
+ resolve(json.content[0].text);
170
+ return;
171
+ }
172
+
173
+ if (json.error) {
174
+ console.error('[a2a] API error:', json.error);
175
+ resolve(`[Agent error: ${json.error.message || JSON.stringify(json.error)}]`);
176
+ return;
177
+ }
178
+
179
+ console.error('[a2a] Unexpected response:', JSON.stringify(json).slice(0, 200));
180
+ resolve('[No response generated]');
181
+ } catch (e) {
182
+ console.error('[a2a] Parse error:', e.message, data.slice(0, 200));
183
+ resolve('[Agent response parsing error]');
184
+ }
185
+ });
186
+ });
187
+
188
+ req.on('error', (e) => {
189
+ console.error('[a2a] Request error:', e.message);
190
+ resolve(`[Agent temporarily unavailable: ${e.message}]`);
191
+ });
192
+
193
+ req.on('timeout', () => {
194
+ req.destroy();
195
+ resolve('[Agent response timeout]');
196
+ });
197
+
198
+ req.write(body);
199
+ req.end();
200
+ });
201
+ }
202
+
203
+ /**
204
+ * Notify owner via console (Telegram notification handled by OpenClaw)
205
+ */
206
+ async function notifyOwner({ level, token, caller, message, response, conversation_id }) {
207
+ const callerName = caller?.name || 'Unknown';
208
+ const callerOwner = caller?.owner ? ` (${caller.owner})` : '';
209
+
210
+ console.log(`[a2a] 📞 Call from ${callerName}${callerOwner}`);
211
+ console.log(`[a2a] Token: ${token.name}`);
212
+ console.log(`[a2a] Message: ${message.slice(0, 100)}...`);
213
+ }
214
+
18
215
  const app = express();
19
216
  app.use(express.json());
20
217
 
@@ -25,30 +222,30 @@ const tokenStore = new TokenStore();
25
222
  app.use('/api/federation', createRoutes({
26
223
  tokenStore,
27
224
 
28
- // Default message handler - in production, this connects to the agent
29
225
  async handleMessage(message, context, options) {
30
- console.log(`[a2a] Received message from ${context.caller?.name || 'unknown'}: ${message}`);
226
+ console.log(`[a2a] 📞 Incoming from ${context.caller?.name || 'unknown'}`);
227
+
228
+ const response = await callAgent(message, context);
229
+
230
+ console.log(`[a2a] 📤 Response: ${response.slice(0, 100)}...`);
231
+
31
232
  return {
32
- text: `[A2A Federation Active] Received: "${message}". Full agent integration pending.`,
233
+ text: response,
33
234
  canContinue: true
34
235
  };
35
236
  },
36
237
 
37
- // Default owner notification - in production, this sends to chat
38
- async notifyOwner({ level, token, caller, message, response }) {
39
- console.log(`[a2a] Notification (${level}): ${caller?.name || 'unknown'} called via token "${token.name}"`);
40
- console.log(`[a2a] Message: ${message}`);
41
- console.log(`[a2a] Response: ${response}`);
42
- }
238
+ notifyOwner
43
239
  }));
44
240
 
45
241
  // Health check at root
46
242
  app.get('/', (req, res) => {
47
- res.json({ service: 'a2a-federation', status: 'ok' });
243
+ res.json({ service: 'a2a-federation', status: 'ok', agent: agentContext.name });
48
244
  });
49
245
 
50
246
  app.listen(port, () => {
51
247
  console.log(`[a2a] Federation server listening on port ${port}`);
248
+ console.log(`[a2a] Agent: ${agentContext.name} - LIVE`);
52
249
  console.log(`[a2a] Endpoints:`);
53
250
  console.log(`[a2a] GET /api/federation/status`);
54
251
  console.log(`[a2a] GET /api/federation/ping`);