ai2ai 0.1.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.
package/lib/start.js ADDED
@@ -0,0 +1,318 @@
1
+ /**
2
+ * ai2ai start — Start the AI2AI HTTP server
3
+ * Runs the protocol server on the configured port.
4
+ */
5
+
6
+ 'use strict';
7
+
8
+ const http = require('http');
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+ const crypto = require('crypto');
12
+ const { requireConfig, loadContacts, saveContacts, PENDING_DIR, CONVERSATIONS_DIR } = require('./config');
13
+ const { loadOrCreateKeys, getFingerprint, signMessage, verifyMessage } = require('./crypto');
14
+
15
+ // Rate limiting
16
+ const rateLimiter = new Map();
17
+ const RATE_LIMIT = 20;
18
+ const RATE_WINDOW = 60000;
19
+
20
+ function checkRateLimit(agentId) {
21
+ const now = Date.now();
22
+ const window = rateLimiter.get(agentId) || [];
23
+ const recent = window.filter(t => now - t < RATE_WINDOW);
24
+ if (recent.length >= RATE_LIMIT) return false;
25
+ recent.push(now);
26
+ rateLimiter.set(agentId, recent);
27
+ return true;
28
+ }
29
+
30
+ /**
31
+ * Save a pending message for human review
32
+ */
33
+ function savePending(id, data) {
34
+ const pendingPath = path.join(PENDING_DIR, `${id}.json`);
35
+ fs.writeFileSync(pendingPath, JSON.stringify(data, null, 2));
36
+ }
37
+
38
+ /**
39
+ * Save to conversation history
40
+ */
41
+ function saveToConversation(conversationId, message) {
42
+ const convPath = path.join(CONVERSATIONS_DIR, `${conversationId}.jsonl`);
43
+ fs.appendFileSync(convPath, JSON.stringify(message) + '\n');
44
+ }
45
+
46
+ /**
47
+ * Format incoming message for human display
48
+ */
49
+ function formatForHuman(envelope) {
50
+ const from = envelope.from?.human || envelope.from?.agent || 'Unknown';
51
+ const intent = envelope.intent || envelope.type;
52
+
53
+ if (envelope.intent === 'schedule.meeting') {
54
+ const p = envelope.payload;
55
+ const times = (p.proposed_times || [])
56
+ .map((t, i) => ` ${i + 1}. ${new Date(t).toLocaleString()}`)
57
+ .join('\n');
58
+ return [
59
+ `📅 Meeting Request from ${from}`,
60
+ ` Subject: ${p.subject || 'No subject'}`,
61
+ ` Times:\n${times}`,
62
+ ` Duration: ${p.duration_minutes || 60} min`,
63
+ p.location_preference ? ` Location: ${p.location_preference}` : '',
64
+ p.notes ? ` Notes: ${p.notes}` : '',
65
+ ].filter(Boolean).join('\n');
66
+ }
67
+
68
+ if (envelope.intent === 'message.relay') {
69
+ const p = envelope.payload;
70
+ const emoji = p.urgency === 'high' ? '🔴' : p.urgency === 'medium' ? '🟡' : '💬';
71
+ return [
72
+ `${emoji} Message from ${from}:`,
73
+ ` "${p.message}"`,
74
+ p.reply_requested ? ' (Reply requested)' : '',
75
+ ].filter(Boolean).join('\n');
76
+ }
77
+
78
+ if (envelope.intent === 'info.request') {
79
+ return [
80
+ `❓ Question from ${from}:`,
81
+ ` "${envelope.payload.question}"`,
82
+ envelope.payload.context ? ` Context: ${envelope.payload.context}` : '',
83
+ ].filter(Boolean).join('\n');
84
+ }
85
+
86
+ if (envelope.type === 'response') {
87
+ return `💬 Response from ${from}:\n ${JSON.stringify(envelope.payload, null, 2)}`;
88
+ }
89
+
90
+ if (envelope.type === 'confirm') {
91
+ return `✅ Confirmed by ${from}: ${JSON.stringify(envelope.payload)}`;
92
+ }
93
+
94
+ if (envelope.type === 'reject') {
95
+ return `❌ Declined by ${from}: ${envelope.payload?.reason || 'No reason given'}`;
96
+ }
97
+
98
+ return `📨 ${intent || 'Message'} from ${from}:\n ${JSON.stringify(envelope.payload, null, 2)}`;
99
+ }
100
+
101
+ /**
102
+ * Handle a ping (handshake)
103
+ */
104
+ function handlePing(envelope, config) {
105
+ const keys = loadOrCreateKeys();
106
+ const fromAgent = envelope.from?.agent;
107
+
108
+ // Store their info as a contact
109
+ if (fromAgent && fromAgent !== 'unknown') {
110
+ const contacts = loadContacts();
111
+ contacts[fromAgent] = {
112
+ ...contacts[fromAgent],
113
+ humanName: envelope.from?.human,
114
+ publicKey: envelope.payload?.public_key,
115
+ capabilities: envelope.payload?.capabilities,
116
+ timezone: envelope.payload?.timezone,
117
+ trustLevel: contacts[fromAgent]?.trustLevel || 'none',
118
+ lastSeen: new Date().toISOString(),
119
+ };
120
+ saveContacts(contacts);
121
+ }
122
+
123
+ return {
124
+ status: 'ok',
125
+ type: 'ping',
126
+ payload: {
127
+ capabilities: [
128
+ 'schedule.meeting', 'schedule.call', 'message.relay',
129
+ 'info.request', 'info.share', 'social.introduction',
130
+ ],
131
+ languages: ['en'],
132
+ timezone: config.timezone || 'UTC',
133
+ model_info: 'local',
134
+ protocol_versions: ['0.1'],
135
+ public_key: keys.publicKey,
136
+ fingerprint: getFingerprint(keys.publicKey),
137
+ },
138
+ };
139
+ }
140
+
141
+ /**
142
+ * Process an incoming message
143
+ */
144
+ function processMessage(envelope, config) {
145
+ const fromAgent = envelope.from?.agent;
146
+
147
+ // Check rate limit
148
+ if (!checkRateLimit(fromAgent)) {
149
+ return { status: 'rejected', reason: 'rate_limited' };
150
+ }
151
+
152
+ // Verify signature if we have their public key
153
+ const contacts = loadContacts();
154
+ const contact = contacts[fromAgent];
155
+ if (contact?.publicKey && envelope.signature) {
156
+ const valid = verifyMessage(envelope, envelope.signature, contact.publicKey);
157
+ if (!valid) {
158
+ console.log(` ⚠️ Invalid signature from ${fromAgent}`);
159
+ return { status: 'rejected', reason: 'invalid_signature' };
160
+ }
161
+ console.log(` ✅ Signature verified for ${fromAgent}`);
162
+ }
163
+
164
+ // Save to conversation
165
+ saveToConversation(envelope.conversation, envelope);
166
+
167
+ // Handle by type
168
+ if (envelope.type === 'ping') {
169
+ return handlePing(envelope, config);
170
+ }
171
+
172
+ // Everything else goes to pending for human review
173
+ const humanMessage = formatForHuman(envelope);
174
+ savePending(envelope.id, {
175
+ envelope,
176
+ approvalMessage: humanMessage,
177
+ handler: envelope.intent || envelope.type,
178
+ createdAt: new Date().toISOString(),
179
+ });
180
+
181
+ console.log(`\n 📥 New message from ${fromAgent}:`);
182
+ console.log(` ${humanMessage.split('\n')[0]}`);
183
+ console.log(' Run `ai2ai pending` to review.\n');
184
+
185
+ return {
186
+ status: 'pending_approval',
187
+ message: 'Message received. Waiting for human approval.',
188
+ conversation: envelope.conversation,
189
+ };
190
+ }
191
+
192
+ async function run() {
193
+ const config = requireConfig();
194
+ const keys = loadOrCreateKeys();
195
+ const fingerprint = getFingerprint(keys.publicKey);
196
+
197
+ const server = http.createServer((req, res) => {
198
+ // CORS
199
+ res.setHeader('Access-Control-Allow-Origin', '*');
200
+ res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS');
201
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type, X-AI2AI-Version');
202
+
203
+ if (req.method === 'OPTIONS') {
204
+ res.writeHead(204);
205
+ res.end();
206
+ return;
207
+ }
208
+
209
+ // GET /ai2ai/health
210
+ if (req.method === 'GET' && req.url === '/ai2ai/health') {
211
+ res.writeHead(200, { 'Content-Type': 'application/json' });
212
+ res.end(JSON.stringify({
213
+ status: 'online',
214
+ protocol: 'ai2ai',
215
+ version: '0.1',
216
+ agent: config.agentName,
217
+ }));
218
+ return;
219
+ }
220
+
221
+ // GET /.well-known/ai2ai.json
222
+ if (req.method === 'GET' && req.url === '/.well-known/ai2ai.json') {
223
+ res.writeHead(200, { 'Content-Type': 'application/json' });
224
+ res.end(JSON.stringify({
225
+ ai2ai: '0.1',
226
+ agent: config.agentName,
227
+ human: config.humanName,
228
+ endpoint: `http://localhost:${config.port}/ai2ai`,
229
+ public_key: keys.publicKey,
230
+ fingerprint,
231
+ capabilities: [
232
+ 'schedule.meeting', 'schedule.call', 'message.relay',
233
+ 'info.request', 'info.share', 'social.introduction',
234
+ ],
235
+ }, null, 2));
236
+ return;
237
+ }
238
+
239
+ // POST /ai2ai
240
+ if (req.method === 'POST' && req.url === '/ai2ai') {
241
+ let body = '';
242
+ req.on('data', chunk => {
243
+ body += chunk;
244
+ if (body.length > 102400) {
245
+ res.writeHead(413, { 'Content-Type': 'application/json' });
246
+ res.end(JSON.stringify({ error: 'Payload too large' }));
247
+ req.destroy();
248
+ }
249
+ });
250
+
251
+ req.on('end', () => {
252
+ let envelope;
253
+ try {
254
+ envelope = JSON.parse(body);
255
+ } catch {
256
+ res.writeHead(400, { 'Content-Type': 'application/json' });
257
+ res.end(JSON.stringify({ error: 'Invalid JSON' }));
258
+ return;
259
+ }
260
+
261
+ if (!envelope.ai2ai || !envelope.type || !envelope.from) {
262
+ res.writeHead(400, { 'Content-Type': 'application/json' });
263
+ res.end(JSON.stringify({ error: 'Invalid AI2AI envelope' }));
264
+ return;
265
+ }
266
+
267
+ try {
268
+ const result = processMessage(envelope, config);
269
+ const statusCode = result.status === 'rejected' ? 403 : 200;
270
+ res.writeHead(statusCode, { 'Content-Type': 'application/json' });
271
+ res.end(JSON.stringify(result));
272
+ } catch (err) {
273
+ console.error(` ❌ Error processing message: ${err.message}`);
274
+ res.writeHead(500, { 'Content-Type': 'application/json' });
275
+ res.end(JSON.stringify({ error: 'Internal error' }));
276
+ }
277
+ });
278
+ return;
279
+ }
280
+
281
+ // 404
282
+ res.writeHead(404, { 'Content-Type': 'application/json' });
283
+ res.end(JSON.stringify({ error: 'Not found. POST to /ai2ai' }));
284
+ });
285
+
286
+ server.listen(config.port, () => {
287
+ const contacts = loadContacts();
288
+ const contactCount = Object.keys(contacts).length;
289
+
290
+ console.log(`
291
+ ╔══════════════════════════════════════════╗
292
+ ║ 🦞 AI2AI Server Running ║
293
+ ╚══════════════════════════════════════════╝
294
+
295
+ 🤖 Agent: ${config.agentName}
296
+ 👤 Human: ${config.humanName}
297
+ 🌐 Endpoint: http://localhost:${config.port}/ai2ai
298
+ 🏥 Health: http://localhost:${config.port}/ai2ai/health
299
+ 🔑 Fingerprint: ${fingerprint}
300
+ 👥 Contacts: ${contactCount}
301
+
302
+ Waiting for incoming messages...
303
+ Press Ctrl+C to stop.
304
+ `);
305
+ });
306
+
307
+ // Graceful shutdown
308
+ process.on('SIGINT', () => {
309
+ console.log('\n\n 👋 Shutting down AI2AI server...\n');
310
+ server.close(() => process.exit(0));
311
+ });
312
+
313
+ process.on('SIGTERM', () => {
314
+ server.close(() => process.exit(0));
315
+ });
316
+ }
317
+
318
+ module.exports = { run };
package/lib/status.js ADDED
@@ -0,0 +1,70 @@
1
+ /**
2
+ * ai2ai status — Show server status, identity, and diagnostics
3
+ */
4
+
5
+ 'use strict';
6
+
7
+ const { loadConfig, loadContacts, PENDING_DIR, AI2AI_DIR } = require('./config');
8
+ const { loadKeys, getFingerprint } = require('./crypto');
9
+ const { fetchGet } = require('./protocol');
10
+ const fs = require('fs');
11
+
12
+ async function run() {
13
+ const config = loadConfig();
14
+ const keys = loadKeys();
15
+
16
+ console.log(`
17
+ ╔══════════════════════════════════════════╗
18
+ ║ 🦞 AI2AI Status ║
19
+ ╚══════════════════════════════════════════╝
20
+ `);
21
+
22
+ // Config
23
+ if (!config) {
24
+ console.log(' ⚠️ Not configured. Run `ai2ai init` to set up.\n');
25
+ return;
26
+ }
27
+
28
+ const fingerprint = keys ? getFingerprint(keys.publicKey) : '(no keys)';
29
+ const contacts = loadContacts();
30
+ const contactCount = Object.keys(contacts).length;
31
+
32
+ // Count pending
33
+ let pendingCount = 0;
34
+ try {
35
+ if (fs.existsSync(PENDING_DIR)) {
36
+ pendingCount = fs.readdirSync(PENDING_DIR).filter(f => f.endsWith('.json')).length;
37
+ }
38
+ } catch { /* ignore */ }
39
+
40
+ console.log(` 👤 Human: ${config.humanName}`);
41
+ console.log(` 🤖 Agent: ${config.agentName}`);
42
+ console.log(` 🌐 Port: ${config.port}`);
43
+ console.log(` 🕐 Timezone: ${config.timezone || 'Not set'}`);
44
+ console.log(` 🔑 Fingerprint: ${fingerprint}`);
45
+ console.log(` 👥 Contacts: ${contactCount}`);
46
+ console.log(` 📬 Pending: ${pendingCount}`);
47
+ console.log(` 📁 Config dir: ${AI2AI_DIR}`);
48
+
49
+ if (config.telegramToken) {
50
+ console.log(` 📱 Telegram: Configured`);
51
+ }
52
+
53
+ // Check if server is running
54
+ console.log(`\n 🏥 Server health check...`);
55
+ try {
56
+ const result = await fetchGet(`http://localhost:${config.port}/ai2ai/health`);
57
+ if (result.data?.status === 'online') {
58
+ console.log(` ✅ Server is running on port ${config.port}`);
59
+ } else {
60
+ console.log(` ⚠️ Server responded but status is: ${result.data?.status || 'unknown'}`);
61
+ }
62
+ } catch {
63
+ console.log(` ❌ Server is not running on port ${config.port}`);
64
+ console.log(` Start it with: ai2ai start`);
65
+ }
66
+
67
+ console.log('');
68
+ }
69
+
70
+ module.exports = { run };
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "ai2ai",
3
+ "version": "0.1.0",
4
+ "description": "The open protocol for AI agents to talk to each other",
5
+ "license": "MIT",
6
+ "author": "Darren Edwards",
7
+ "bin": {
8
+ "ai2ai": "./bin/ai2ai.js"
9
+ },
10
+ "files": [
11
+ "bin/",
12
+ "lib/",
13
+ "README.md"
14
+ ],
15
+ "engines": {
16
+ "node": ">=18.0.0"
17
+ },
18
+ "keywords": [
19
+ "ai",
20
+ "agent",
21
+ "protocol",
22
+ "ai2ai",
23
+ "openclaw",
24
+ "decentralized",
25
+ "agent-to-agent",
26
+ "multi-agent",
27
+ "communication"
28
+ ],
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "https://github.com/DarrenEdwards111/ai2ai-protocol"
32
+ },
33
+ "homepage": "https://github.com/DarrenEdwards111/ai2ai-protocol#readme",
34
+ "bugs": {
35
+ "url": "https://github.com/DarrenEdwards111/ai2ai-protocol/issues"
36
+ }
37
+ }