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/README.md +184 -0
- package/bin/ai2ai.js +87 -0
- package/lib/approve.js +137 -0
- package/lib/config.js +120 -0
- package/lib/connect.js +89 -0
- package/lib/contacts.js +62 -0
- package/lib/crypto.js +124 -0
- package/lib/init.js +148 -0
- package/lib/pending.js +114 -0
- package/lib/protocol.js +161 -0
- package/lib/send.js +135 -0
- package/lib/start.js +318 -0
- package/lib/status.js +70 -0
- package/package.json +37 -0
package/README.md
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# 🦞 ai2ai
|
|
2
|
+
|
|
3
|
+
**The open protocol for AI agents to talk to each other.**
|
|
4
|
+
|
|
5
|
+
AI2AI lets your AI agent communicate with other AI agents — scheduling meetings, relaying messages, asking questions, and more. No platform lock-in, no central server. Just agents talking to agents.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install -g ai2ai
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Or run directly:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npx ai2ai
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Quick Start
|
|
20
|
+
|
|
21
|
+
### 1. Set up your identity
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
ai2ai init
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The setup wizard will ask your name, create an agent identity, generate Ed25519 keys, and save everything to `~/.ai2ai/`.
|
|
28
|
+
|
|
29
|
+
### 2. Start your server
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
ai2ai start
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
This starts an HTTP server that listens for incoming AI2AI messages.
|
|
36
|
+
|
|
37
|
+
### 3. Connect to a friend
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
ai2ai connect http://friend.example.com:18800/ai2ai
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
This pings the remote agent, exchanges public keys, and saves them as a contact.
|
|
44
|
+
|
|
45
|
+
### 4. Send messages
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
# Simple message
|
|
49
|
+
ai2ai send alex "Hey, are you free this weekend?"
|
|
50
|
+
|
|
51
|
+
# Scheduling (auto-detected)
|
|
52
|
+
ai2ai send alex "dinner next Thursday at 7pm?"
|
|
53
|
+
|
|
54
|
+
# Questions (auto-detected)
|
|
55
|
+
ai2ai send alex "What time works for you?"
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
AI2AI automatically detects message intent:
|
|
59
|
+
- **Scheduling keywords** → `schedule.meeting` intent
|
|
60
|
+
- **Questions** → `info.request` intent
|
|
61
|
+
- **Everything else** → `message.relay` intent
|
|
62
|
+
|
|
63
|
+
### 5. Check for responses
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
ai2ai pending
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### 6. Approve or reject
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
ai2ai approve 1 "Thursday works for me!"
|
|
73
|
+
ai2ai reject 2
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Commands
|
|
77
|
+
|
|
78
|
+
| Command | Description |
|
|
79
|
+
|---------|-------------|
|
|
80
|
+
| `ai2ai init` | Interactive setup wizard |
|
|
81
|
+
| `ai2ai start` | Start the AI2AI server |
|
|
82
|
+
| `ai2ai connect <endpoint>` | Connect to another agent |
|
|
83
|
+
| `ai2ai send <contact> <msg>` | Send a message to a contact |
|
|
84
|
+
| `ai2ai pending` | Show pending messages |
|
|
85
|
+
| `ai2ai approve <id> [reply]` | Approve a pending message |
|
|
86
|
+
| `ai2ai reject <id>` | Reject a pending message |
|
|
87
|
+
| `ai2ai contacts` | List known contacts |
|
|
88
|
+
| `ai2ai status` | Show server & agent status |
|
|
89
|
+
|
|
90
|
+
## Protocol
|
|
91
|
+
|
|
92
|
+
AI2AI uses a simple JSON envelope over HTTP:
|
|
93
|
+
|
|
94
|
+
```json
|
|
95
|
+
{
|
|
96
|
+
"ai2ai": "0.1",
|
|
97
|
+
"id": "uuid",
|
|
98
|
+
"timestamp": "2025-02-07T12:00:00.000Z",
|
|
99
|
+
"from": {
|
|
100
|
+
"agent": "darren-assistant",
|
|
101
|
+
"node": "darren-assistant-node",
|
|
102
|
+
"human": "Darren"
|
|
103
|
+
},
|
|
104
|
+
"to": {
|
|
105
|
+
"agent": "alex-assistant",
|
|
106
|
+
"node": "alex-assistant-node",
|
|
107
|
+
"human": "Alex"
|
|
108
|
+
},
|
|
109
|
+
"conversation": "uuid",
|
|
110
|
+
"type": "request",
|
|
111
|
+
"intent": "message.relay",
|
|
112
|
+
"payload": {
|
|
113
|
+
"message": "Hey, are you free Thursday?",
|
|
114
|
+
"urgency": "low",
|
|
115
|
+
"reply_requested": true
|
|
116
|
+
},
|
|
117
|
+
"signature": "base64-ed25519-signature"
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Message Types
|
|
122
|
+
|
|
123
|
+
- **`ping`** — Handshake, exchange capabilities and public keys
|
|
124
|
+
- **`request`** — Ask another agent to do something
|
|
125
|
+
- **`response`** — Reply to a request
|
|
126
|
+
- **`confirm`** — Accept a proposal
|
|
127
|
+
- **`reject`** — Decline a proposal
|
|
128
|
+
- **`inform`** — One-way notification
|
|
129
|
+
|
|
130
|
+
### Supported Intents
|
|
131
|
+
|
|
132
|
+
- `schedule.meeting` — Schedule a meeting
|
|
133
|
+
- `schedule.call` — Schedule a call
|
|
134
|
+
- `message.relay` — Relay a message to a human
|
|
135
|
+
- `info.request` — Ask a question
|
|
136
|
+
- `info.share` — Share information
|
|
137
|
+
- `social.introduction` — Introduce two people
|
|
138
|
+
|
|
139
|
+
## Security
|
|
140
|
+
|
|
141
|
+
- **Ed25519 signatures** — Every message is signed with your agent's private key
|
|
142
|
+
- **Key exchange** — Public keys are exchanged during the `connect` handshake
|
|
143
|
+
- **Signature verification** — Incoming messages from known contacts are verified
|
|
144
|
+
- **Trust levels** — Contacts have trust levels (`none`, `known`, `trusted`)
|
|
145
|
+
- **Rate limiting** — 20 messages per minute per agent
|
|
146
|
+
- **Human approval** — All incoming requests go to a pending queue for human review
|
|
147
|
+
|
|
148
|
+
## Configuration
|
|
149
|
+
|
|
150
|
+
All data is stored in `~/.ai2ai/`:
|
|
151
|
+
|
|
152
|
+
```
|
|
153
|
+
~/.ai2ai/
|
|
154
|
+
├── config.json # Agent configuration
|
|
155
|
+
├── contacts.json # Known contacts
|
|
156
|
+
├── keys/
|
|
157
|
+
│ ├── agent.pub # Ed25519 public key
|
|
158
|
+
│ └── agent.key # Ed25519 private key (mode 0600)
|
|
159
|
+
├── pending/ # Messages awaiting approval
|
|
160
|
+
├── conversations/ # Conversation history
|
|
161
|
+
└── logs/ # Server logs
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Discovery
|
|
165
|
+
|
|
166
|
+
Your agent serves a `.well-known/ai2ai.json` endpoint for web-based discovery:
|
|
167
|
+
|
|
168
|
+
```
|
|
169
|
+
GET http://localhost:18800/.well-known/ai2ai.json
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Requirements
|
|
173
|
+
|
|
174
|
+
- **Node.js 18+**
|
|
175
|
+
- **Zero external dependencies** — Uses only Node.js built-in modules
|
|
176
|
+
|
|
177
|
+
## License
|
|
178
|
+
|
|
179
|
+
MIT
|
|
180
|
+
|
|
181
|
+
## Links
|
|
182
|
+
|
|
183
|
+
- [Protocol Specification](https://github.com/DarrenEdwards111/ai2ai-protocol)
|
|
184
|
+
- [Report Issues](https://github.com/DarrenEdwards111/ai2ai-protocol/issues)
|
package/bin/ai2ai.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ai2ai — The open protocol for AI agents to talk to each other
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* ai2ai init Interactive setup wizard
|
|
8
|
+
* ai2ai start Start the AI2AI server
|
|
9
|
+
* ai2ai connect <endpoint> Connect to another agent
|
|
10
|
+
* ai2ai send <contact> <msg> Send a message to a contact
|
|
11
|
+
* ai2ai pending Show pending messages
|
|
12
|
+
* ai2ai approve <id> [reply] Approve a pending message
|
|
13
|
+
* ai2ai reject <id> Reject a pending message
|
|
14
|
+
* ai2ai contacts List known contacts
|
|
15
|
+
* ai2ai status Show server & agent status
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
'use strict';
|
|
19
|
+
|
|
20
|
+
const HELP = `
|
|
21
|
+
🦞 ai2ai — Agent-to-Agent Communication Protocol
|
|
22
|
+
|
|
23
|
+
Usage:
|
|
24
|
+
ai2ai init Set up your AI2AI identity
|
|
25
|
+
ai2ai start Start the AI2AI server
|
|
26
|
+
ai2ai connect <endpoint> Connect to another agent
|
|
27
|
+
ai2ai send <contact> <message> Send a message to a contact
|
|
28
|
+
ai2ai pending Show pending messages
|
|
29
|
+
ai2ai approve <id> [reply] Approve a pending message
|
|
30
|
+
ai2ai reject <id> Reject a pending message
|
|
31
|
+
ai2ai contacts List known contacts
|
|
32
|
+
ai2ai status Show server & agent status
|
|
33
|
+
|
|
34
|
+
Examples:
|
|
35
|
+
ai2ai init
|
|
36
|
+
ai2ai start
|
|
37
|
+
ai2ai connect http://friend.example.com:18800/ai2ai
|
|
38
|
+
ai2ai send alex "dinner next Thursday?"
|
|
39
|
+
ai2ai pending
|
|
40
|
+
ai2ai approve 1 "Thursday works for me"
|
|
41
|
+
|
|
42
|
+
Options:
|
|
43
|
+
--help, -h Show this help message
|
|
44
|
+
--version, -v Show version number
|
|
45
|
+
`;
|
|
46
|
+
|
|
47
|
+
const VERSION = '0.1.0';
|
|
48
|
+
|
|
49
|
+
const args = process.argv.slice(2);
|
|
50
|
+
const command = args[0];
|
|
51
|
+
|
|
52
|
+
if (!command || command === '--help' || command === '-h') {
|
|
53
|
+
console.log(HELP);
|
|
54
|
+
process.exit(0);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (command === '--version' || command === '-v') {
|
|
58
|
+
console.log(`ai2ai v${VERSION}`);
|
|
59
|
+
process.exit(0);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const commands = {
|
|
63
|
+
init: () => require('../lib/init').run(),
|
|
64
|
+
start: () => require('../lib/start').run(),
|
|
65
|
+
connect: () => require('../lib/connect').run(args.slice(1)),
|
|
66
|
+
send: () => require('../lib/send').run(args.slice(1)),
|
|
67
|
+
pending: () => require('../lib/pending').run(),
|
|
68
|
+
approve: () => require('../lib/approve').run(args.slice(1)),
|
|
69
|
+
reject: () => require('../lib/approve').runReject(args.slice(1)),
|
|
70
|
+
contacts: () => require('../lib/contacts').run(),
|
|
71
|
+
status: () => require('../lib/status').run(),
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
if (!commands[command]) {
|
|
75
|
+
console.error(`\n❌ Unknown command: ${command}\n`);
|
|
76
|
+
console.log(HELP);
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Run the command
|
|
81
|
+
Promise.resolve(commands[command]()).catch(err => {
|
|
82
|
+
console.error(`\n❌ Error: ${err.message}\n`);
|
|
83
|
+
if (process.env.AI2AI_DEBUG) {
|
|
84
|
+
console.error(err.stack);
|
|
85
|
+
}
|
|
86
|
+
process.exit(1);
|
|
87
|
+
});
|
package/lib/approve.js
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ai2ai approve / reject — Handle pending messages
|
|
3
|
+
* Approve with an optional reply, or reject.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
const { requireConfig, findContact } = require('./config');
|
|
9
|
+
const { createEnvelope, sendRequest } = require('./protocol');
|
|
10
|
+
const { getPendingByNumber, removePending } = require('./pending');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Send a response back to the originating agent
|
|
14
|
+
*/
|
|
15
|
+
async function sendResponse(envelope, type, payload) {
|
|
16
|
+
const config = requireConfig();
|
|
17
|
+
const from = envelope.from;
|
|
18
|
+
|
|
19
|
+
// Find their endpoint
|
|
20
|
+
const contact = findContact(from.agent);
|
|
21
|
+
if (!contact?.endpoint) {
|
|
22
|
+
console.log(` ⚠️ No endpoint for ${from.agent}. Response saved locally only.`);
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const response = createEnvelope({
|
|
27
|
+
to: {
|
|
28
|
+
agent: from.agent,
|
|
29
|
+
node: from.node || `${from.agent}-node`,
|
|
30
|
+
human: from.human || from.agent,
|
|
31
|
+
},
|
|
32
|
+
type,
|
|
33
|
+
intent: envelope.intent,
|
|
34
|
+
conversationId: envelope.conversation,
|
|
35
|
+
payload,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
return sendRequest(contact.endpoint, response);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function run(args) {
|
|
42
|
+
if (!args[0]) {
|
|
43
|
+
console.error('\n ❌ Usage: ai2ai approve <number> [reply]');
|
|
44
|
+
console.error(' Example: ai2ai approve 1 "Thursday works!"\n');
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const num = parseInt(args[0]);
|
|
49
|
+
if (isNaN(num)) {
|
|
50
|
+
console.error(`\n ❌ "${args[0]}" is not a valid number.\n`);
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const pending = getPendingByNumber(num);
|
|
55
|
+
if (!pending) {
|
|
56
|
+
console.error(`\n ❌ No pending message #${num}. Run \`ai2ai pending\` to check.\n`);
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const reply = args.slice(1).join(' ').replace(/^["']|["']$/g, '') || null;
|
|
61
|
+
const from = pending.envelope?.from?.human || pending.envelope?.from?.agent || 'Unknown';
|
|
62
|
+
|
|
63
|
+
console.log(`\n ✅ Approving message from ${from}...`);
|
|
64
|
+
|
|
65
|
+
// Build response payload
|
|
66
|
+
const responsePayload = {};
|
|
67
|
+
|
|
68
|
+
if (pending.envelope?.intent === 'schedule.meeting') {
|
|
69
|
+
const times = pending.envelope.payload?.proposed_times || [];
|
|
70
|
+
const num = reply ? parseInt(reply) : null;
|
|
71
|
+
if (num && num >= 1 && num <= times.length) {
|
|
72
|
+
responsePayload.accepted_time = times[num - 1];
|
|
73
|
+
responsePayload.message = `Accepted: ${new Date(times[num - 1]).toLocaleString()}`;
|
|
74
|
+
} else {
|
|
75
|
+
responsePayload.counter_proposal = reply;
|
|
76
|
+
responsePayload.message = reply || 'Approved';
|
|
77
|
+
}
|
|
78
|
+
} else if (pending.envelope?.intent === 'info.request') {
|
|
79
|
+
responsePayload.answer = reply || 'Acknowledged';
|
|
80
|
+
} else {
|
|
81
|
+
responsePayload.message = reply || 'Approved';
|
|
82
|
+
responsePayload.is_reply = !!reply;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
const result = await sendResponse(pending.envelope, 'response', responsePayload);
|
|
87
|
+
if (result) {
|
|
88
|
+
console.log(` 📤 Response sent to ${from}'s agent.`);
|
|
89
|
+
}
|
|
90
|
+
} catch (err) {
|
|
91
|
+
console.log(` ⚠️ Could not send response: ${err.message}`);
|
|
92
|
+
console.log(' (The approval is still recorded locally.)');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Remove from pending
|
|
96
|
+
removePending(pending.id);
|
|
97
|
+
console.log(` 🗑️ Removed from pending.\n`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function runReject(args) {
|
|
101
|
+
if (!args[0]) {
|
|
102
|
+
console.error('\n ❌ Usage: ai2ai reject <number>');
|
|
103
|
+
console.error(' Example: ai2ai reject 1\n');
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const num = parseInt(args[0]);
|
|
108
|
+
if (isNaN(num)) {
|
|
109
|
+
console.error(`\n ❌ "${args[0]}" is not a valid number.\n`);
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const pending = getPendingByNumber(num);
|
|
114
|
+
if (!pending) {
|
|
115
|
+
console.error(`\n ❌ No pending message #${num}. Run \`ai2ai pending\` to check.\n`);
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const reason = args.slice(1).join(' ').replace(/^["']|["']$/g, '') || 'Declined';
|
|
120
|
+
const from = pending.envelope?.from?.human || pending.envelope?.from?.agent || 'Unknown';
|
|
121
|
+
|
|
122
|
+
console.log(`\n ❌ Rejecting message from ${from}...`);
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
const result = await sendResponse(pending.envelope, 'reject', { reason });
|
|
126
|
+
if (result) {
|
|
127
|
+
console.log(` 📤 Rejection sent to ${from}'s agent.`);
|
|
128
|
+
}
|
|
129
|
+
} catch (err) {
|
|
130
|
+
console.log(` ⚠️ Could not send rejection: ${err.message}`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
removePending(pending.id);
|
|
134
|
+
console.log(` 🗑️ Removed from pending.\n`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
module.exports = { run, runReject };
|
package/lib/config.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI2AI Config — Configuration and path management
|
|
3
|
+
* All config stored in ~/.ai2ai/
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const os = require('os');
|
|
11
|
+
|
|
12
|
+
const AI2AI_DIR = path.join(os.homedir(), '.ai2ai');
|
|
13
|
+
const CONFIG_PATH = path.join(AI2AI_DIR, 'config.json');
|
|
14
|
+
const CONTACTS_PATH = path.join(AI2AI_DIR, 'contacts.json');
|
|
15
|
+
const KEYS_DIR = path.join(AI2AI_DIR, 'keys');
|
|
16
|
+
const PENDING_DIR = path.join(AI2AI_DIR, 'pending');
|
|
17
|
+
const CONVERSATIONS_DIR = path.join(AI2AI_DIR, 'conversations');
|
|
18
|
+
const LOGS_DIR = path.join(AI2AI_DIR, 'logs');
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Ensure all required directories exist
|
|
22
|
+
*/
|
|
23
|
+
function ensureDirs() {
|
|
24
|
+
for (const dir of [AI2AI_DIR, KEYS_DIR, PENDING_DIR, CONVERSATIONS_DIR, LOGS_DIR]) {
|
|
25
|
+
if (!fs.existsSync(dir)) {
|
|
26
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Load config from disk
|
|
33
|
+
*/
|
|
34
|
+
function loadConfig() {
|
|
35
|
+
if (!fs.existsSync(CONFIG_PATH)) return null;
|
|
36
|
+
try {
|
|
37
|
+
return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));
|
|
38
|
+
} catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Save config to disk
|
|
45
|
+
*/
|
|
46
|
+
function saveConfig(config) {
|
|
47
|
+
ensureDirs();
|
|
48
|
+
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Load config or exit with a helpful message
|
|
53
|
+
*/
|
|
54
|
+
function requireConfig() {
|
|
55
|
+
const config = loadConfig();
|
|
56
|
+
if (!config) {
|
|
57
|
+
console.error('\n❌ No AI2AI configuration found.');
|
|
58
|
+
console.error(' Run `ai2ai init` to set up your agent.\n');
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
return config;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Load contacts from disk
|
|
66
|
+
*/
|
|
67
|
+
function loadContacts() {
|
|
68
|
+
if (!fs.existsSync(CONTACTS_PATH)) return {};
|
|
69
|
+
try {
|
|
70
|
+
return JSON.parse(fs.readFileSync(CONTACTS_PATH, 'utf-8'));
|
|
71
|
+
} catch {
|
|
72
|
+
return {};
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Save contacts to disk
|
|
78
|
+
*/
|
|
79
|
+
function saveContacts(contacts) {
|
|
80
|
+
ensureDirs();
|
|
81
|
+
fs.writeFileSync(CONTACTS_PATH, JSON.stringify(contacts, null, 2));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Find a contact by name or agent ID (fuzzy match)
|
|
86
|
+
*/
|
|
87
|
+
function findContact(query) {
|
|
88
|
+
const contacts = loadContacts();
|
|
89
|
+
const q = query.toLowerCase();
|
|
90
|
+
|
|
91
|
+
// Exact match on agent ID
|
|
92
|
+
if (contacts[q]) return { id: q, ...contacts[q] };
|
|
93
|
+
|
|
94
|
+
// Search by human name or agent ID prefix
|
|
95
|
+
for (const [id, contact] of Object.entries(contacts)) {
|
|
96
|
+
if (id.toLowerCase().includes(q)) return { id, ...contact };
|
|
97
|
+
if (contact.humanName && contact.humanName.toLowerCase().includes(q)) {
|
|
98
|
+
return { id, ...contact };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
module.exports = {
|
|
106
|
+
AI2AI_DIR,
|
|
107
|
+
CONFIG_PATH,
|
|
108
|
+
CONTACTS_PATH,
|
|
109
|
+
KEYS_DIR,
|
|
110
|
+
PENDING_DIR,
|
|
111
|
+
CONVERSATIONS_DIR,
|
|
112
|
+
LOGS_DIR,
|
|
113
|
+
ensureDirs,
|
|
114
|
+
loadConfig,
|
|
115
|
+
saveConfig,
|
|
116
|
+
requireConfig,
|
|
117
|
+
loadContacts,
|
|
118
|
+
saveContacts,
|
|
119
|
+
findContact,
|
|
120
|
+
};
|
package/lib/connect.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ai2ai connect — Connect to another AI agent
|
|
3
|
+
* Pings the remote endpoint, exchanges keys, and saves as contact.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
const { requireConfig, loadContacts, saveContacts } = require('./config');
|
|
9
|
+
const { createPingEnvelope, sendRequest } = require('./protocol');
|
|
10
|
+
const { getFingerprint } = require('./crypto');
|
|
11
|
+
|
|
12
|
+
async function run(args) {
|
|
13
|
+
if (!args[0]) {
|
|
14
|
+
console.error('\n ❌ Usage: ai2ai connect <endpoint>');
|
|
15
|
+
console.error(' Example: ai2ai connect http://friend.example.com:18800/ai2ai\n');
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let endpoint = args[0];
|
|
20
|
+
|
|
21
|
+
// Auto-append /ai2ai if needed
|
|
22
|
+
if (!endpoint.endsWith('/ai2ai')) {
|
|
23
|
+
if (endpoint.endsWith('/')) endpoint += 'ai2ai';
|
|
24
|
+
else endpoint += '/ai2ai';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const config = requireConfig();
|
|
28
|
+
|
|
29
|
+
console.log(`\n 🔗 Connecting to ${endpoint}...`);
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const envelope = createPingEnvelope();
|
|
33
|
+
const response = await sendRequest(endpoint, envelope);
|
|
34
|
+
|
|
35
|
+
if (response.status !== 'ok' || response.type !== 'ping') {
|
|
36
|
+
console.error(`\n ❌ Unexpected response: ${JSON.stringify(response)}\n`);
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const payload = response.payload || {};
|
|
41
|
+
const agentName = payload.agent_name || 'unknown';
|
|
42
|
+
const remoteFingerprint = payload.fingerprint || '(none)';
|
|
43
|
+
|
|
44
|
+
// Save as contact
|
|
45
|
+
const contacts = loadContacts();
|
|
46
|
+
|
|
47
|
+
// Try to find the agent name from the response
|
|
48
|
+
// The server may return its identity in various fields
|
|
49
|
+
let contactId = agentName;
|
|
50
|
+
if (contactId === 'unknown' && payload.public_key) {
|
|
51
|
+
// Use fingerprint-based ID as fallback
|
|
52
|
+
contactId = `agent-${getFingerprint(payload.public_key).slice(0, 9).replace(/:/g, '')}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
contacts[contactId] = {
|
|
56
|
+
...contacts[contactId],
|
|
57
|
+
endpoint,
|
|
58
|
+
publicKey: payload.public_key || null,
|
|
59
|
+
fingerprint: remoteFingerprint,
|
|
60
|
+
capabilities: payload.capabilities || [],
|
|
61
|
+
timezone: payload.timezone || null,
|
|
62
|
+
trustLevel: contacts[contactId]?.trustLevel || 'known',
|
|
63
|
+
connectedAt: contacts[contactId]?.connectedAt || new Date().toISOString(),
|
|
64
|
+
lastSeen: new Date().toISOString(),
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
saveContacts(contacts);
|
|
68
|
+
|
|
69
|
+
console.log(`
|
|
70
|
+
✅ Connected!
|
|
71
|
+
|
|
72
|
+
🤖 Agent: ${contactId}
|
|
73
|
+
🔑 Fingerprint: ${remoteFingerprint}
|
|
74
|
+
🕐 Timezone: ${payload.timezone || 'Unknown'}
|
|
75
|
+
🛠️ Capabilities: ${(payload.capabilities || []).join(', ') || 'None listed'}
|
|
76
|
+
📁 Saved to contacts
|
|
77
|
+
|
|
78
|
+
You can now:
|
|
79
|
+
ai2ai send ${contactId} "Hello!"
|
|
80
|
+
`);
|
|
81
|
+
|
|
82
|
+
} catch (err) {
|
|
83
|
+
console.error(`\n ❌ Connection failed: ${err.message}`);
|
|
84
|
+
console.error(' Make sure the remote agent is running.\n');
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
module.exports = { run };
|
package/lib/contacts.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ai2ai contacts — List all known contacts
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
'use strict';
|
|
6
|
+
|
|
7
|
+
const { loadContacts } = require('./config');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Format trust level with emoji
|
|
11
|
+
*/
|
|
12
|
+
function trustEmoji(level) {
|
|
13
|
+
switch (level) {
|
|
14
|
+
case 'trusted': return '🟢 trusted';
|
|
15
|
+
case 'known': return '🟡 known';
|
|
16
|
+
case 'none': return '⚪ none';
|
|
17
|
+
default: return '⚪ none';
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function run() {
|
|
22
|
+
const contacts = loadContacts();
|
|
23
|
+
const ids = Object.keys(contacts);
|
|
24
|
+
|
|
25
|
+
if (ids.length === 0) {
|
|
26
|
+
console.log('\n 📭 No contacts yet.');
|
|
27
|
+
console.log(' Run `ai2ai connect <endpoint>` to add one.\n');
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
console.log(`\n 👥 Contacts (${ids.length})\n`);
|
|
32
|
+
console.log(' ─'.repeat(25));
|
|
33
|
+
|
|
34
|
+
for (const id of ids) {
|
|
35
|
+
const c = contacts[id];
|
|
36
|
+
const human = c.humanName || '(unknown)';
|
|
37
|
+
const trust = trustEmoji(c.trustLevel);
|
|
38
|
+
const endpoint = c.endpoint || '(no endpoint)';
|
|
39
|
+
const lastSeen = c.lastSeen ? new Date(c.lastSeen).toLocaleString() : 'Never';
|
|
40
|
+
const caps = (c.capabilities || []).length;
|
|
41
|
+
|
|
42
|
+
console.log(`
|
|
43
|
+
🤖 ${id}
|
|
44
|
+
Human: ${human}
|
|
45
|
+
Trust: ${trust}
|
|
46
|
+
Endpoint: ${endpoint}
|
|
47
|
+
Capabilities: ${caps} intents
|
|
48
|
+
Last seen: ${lastSeen}`);
|
|
49
|
+
|
|
50
|
+
if (c.fingerprint) {
|
|
51
|
+
console.log(` Fingerprint: ${c.fingerprint}`);
|
|
52
|
+
}
|
|
53
|
+
if (c.timezone) {
|
|
54
|
+
console.log(` Timezone: ${c.timezone}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
console.log('\n ─'.repeat(25));
|
|
59
|
+
console.log('');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
module.exports = { run };
|