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/crypto.js ADDED
@@ -0,0 +1,124 @@
1
+ /**
2
+ * AI2AI Crypto — Ed25519 signing, verification, and key management
3
+ * Zero external dependencies — uses only Node.js built-in crypto module.
4
+ */
5
+
6
+ 'use strict';
7
+
8
+ const crypto = require('crypto');
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+ const { KEYS_DIR, ensureDirs } = require('./config');
12
+
13
+ /**
14
+ * Generate a new Ed25519 key pair
15
+ */
16
+ function generateKeyPair() {
17
+ const { publicKey, privateKey } = crypto.generateKeyPairSync('ed25519', {
18
+ publicKeyEncoding: { type: 'spki', format: 'pem' },
19
+ privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
20
+ });
21
+ return { publicKey, privateKey };
22
+ }
23
+
24
+ /**
25
+ * Load keys from ~/.ai2ai/keys/
26
+ */
27
+ function loadKeys() {
28
+ const pubPath = path.join(KEYS_DIR, 'agent.pub');
29
+ const privPath = path.join(KEYS_DIR, 'agent.key');
30
+
31
+ if (!fs.existsSync(pubPath) || !fs.existsSync(privPath)) {
32
+ return null;
33
+ }
34
+
35
+ return {
36
+ publicKey: fs.readFileSync(pubPath, 'utf-8'),
37
+ privateKey: fs.readFileSync(privPath, 'utf-8'),
38
+ };
39
+ }
40
+
41
+ /**
42
+ * Save keys to ~/.ai2ai/keys/
43
+ */
44
+ function saveKeys(keys) {
45
+ ensureDirs();
46
+ const pubPath = path.join(KEYS_DIR, 'agent.pub');
47
+ const privPath = path.join(KEYS_DIR, 'agent.key');
48
+
49
+ fs.writeFileSync(pubPath, keys.publicKey, { mode: 0o644 });
50
+ fs.writeFileSync(privPath, keys.privateKey, { mode: 0o600 });
51
+ }
52
+
53
+ /**
54
+ * Load or create keys
55
+ */
56
+ function loadOrCreateKeys() {
57
+ let keys = loadKeys();
58
+ if (!keys) {
59
+ keys = generateKeyPair();
60
+ saveKeys(keys);
61
+ }
62
+ return keys;
63
+ }
64
+
65
+ /**
66
+ * Get a human-readable fingerprint of a public key
67
+ */
68
+ function getFingerprint(publicKeyPem) {
69
+ const hash = crypto.createHash('sha256').update(publicKeyPem).digest('hex');
70
+ return hash.match(/.{1,4}/g).slice(0, 8).join(':');
71
+ }
72
+
73
+ /**
74
+ * Sign a message envelope
75
+ * Signs the canonical fields to produce a deterministic signature.
76
+ */
77
+ function signMessage(envelope, privateKeyPem) {
78
+ const payload = JSON.stringify({
79
+ id: envelope.id,
80
+ timestamp: envelope.timestamp,
81
+ from: envelope.from,
82
+ to: envelope.to,
83
+ conversation: envelope.conversation,
84
+ type: envelope.type,
85
+ intent: envelope.intent,
86
+ payload: envelope.payload,
87
+ });
88
+
89
+ const privateKey = crypto.createPrivateKey(privateKeyPem);
90
+ return crypto.sign(null, Buffer.from(payload), privateKey).toString('base64');
91
+ }
92
+
93
+ /**
94
+ * Verify a message signature
95
+ */
96
+ function verifyMessage(envelope, signature, publicKeyPem) {
97
+ const payload = JSON.stringify({
98
+ id: envelope.id,
99
+ timestamp: envelope.timestamp,
100
+ from: envelope.from,
101
+ to: envelope.to,
102
+ conversation: envelope.conversation,
103
+ type: envelope.type,
104
+ intent: envelope.intent,
105
+ payload: envelope.payload,
106
+ });
107
+
108
+ try {
109
+ const publicKey = crypto.createPublicKey(publicKeyPem);
110
+ return crypto.verify(null, Buffer.from(payload), publicKey, Buffer.from(signature, 'base64'));
111
+ } catch {
112
+ return false;
113
+ }
114
+ }
115
+
116
+ module.exports = {
117
+ generateKeyPair,
118
+ loadKeys,
119
+ saveKeys,
120
+ loadOrCreateKeys,
121
+ getFingerprint,
122
+ signMessage,
123
+ verifyMessage,
124
+ };
package/lib/init.js ADDED
@@ -0,0 +1,148 @@
1
+ /**
2
+ * ai2ai init — Interactive setup wizard
3
+ * Creates identity, generates keys, saves config to ~/.ai2ai/
4
+ */
5
+
6
+ 'use strict';
7
+
8
+ const readline = require('readline');
9
+ const { saveConfig, loadConfig, ensureDirs, AI2AI_DIR } = require('./config');
10
+ const { generateKeyPair, saveKeys, getFingerprint } = require('./crypto');
11
+
12
+ /**
13
+ * Prompt the user for input
14
+ */
15
+ function ask(rl, question, defaultValue) {
16
+ const suffix = defaultValue ? ` (${defaultValue})` : '';
17
+ return new Promise(resolve => {
18
+ rl.question(` ${question}${suffix}: `, answer => {
19
+ resolve(answer.trim() || defaultValue || '');
20
+ });
21
+ });
22
+ }
23
+
24
+ /**
25
+ * Prompt for yes/no
26
+ */
27
+ function askYesNo(rl, question, defaultYes = false) {
28
+ const hint = defaultYes ? 'Y/n' : 'y/N';
29
+ return new Promise(resolve => {
30
+ rl.question(` ${question} [${hint}]: `, answer => {
31
+ const a = answer.trim().toLowerCase();
32
+ if (!a) return resolve(defaultYes);
33
+ resolve(a === 'y' || a === 'yes');
34
+ });
35
+ });
36
+ }
37
+
38
+ async function run() {
39
+ const existing = loadConfig();
40
+
41
+ console.log(`
42
+ ╔══════════════════════════════════════════╗
43
+ ║ 🦞 AI2AI — Agent Setup Wizard ║
44
+ ╚══════════════════════════════════════════╝
45
+ `);
46
+
47
+ if (existing) {
48
+ console.log(` ⚠️ Existing config found at ${AI2AI_DIR}`);
49
+ console.log(` Agent: ${existing.agentName} (${existing.humanName})\n`);
50
+ }
51
+
52
+ console.log(' Let\'s set up your AI agent identity.\n');
53
+
54
+ const rl = readline.createInterface({
55
+ input: process.stdin,
56
+ output: process.stdout,
57
+ });
58
+
59
+ try {
60
+ // 1. Human name
61
+ const defaultName = existing?.humanName || '';
62
+ const humanName = await ask(rl, '👤 Your name', defaultName);
63
+ if (!humanName) {
64
+ console.log('\n ❌ Name is required.\n');
65
+ return;
66
+ }
67
+
68
+ // 2. Agent name
69
+ const defaultAgent = existing?.agentName || `${humanName.toLowerCase().replace(/\s+/g, '-')}-assistant`;
70
+ const agentName = await ask(rl, '🤖 Agent name', defaultAgent);
71
+
72
+ // 3. Port
73
+ const defaultPort = existing?.port || 18800;
74
+ const portStr = await ask(rl, '🌐 Server port', String(defaultPort));
75
+ const port = parseInt(portStr) || defaultPort;
76
+
77
+ // 4. Timezone
78
+ const defaultTz = existing?.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone;
79
+ const timezone = await ask(rl, '🕐 Timezone', defaultTz);
80
+
81
+ // 5. Telegram integration
82
+ let telegramToken = existing?.telegramToken || '';
83
+ const wantTelegram = await askYesNo(rl, '📱 Enable Telegram integration?', !!telegramToken);
84
+ if (wantTelegram) {
85
+ telegramToken = await ask(rl, '🔑 Telegram bot token', telegramToken ? '(keep existing)' : '');
86
+ if (telegramToken === '(keep existing)') telegramToken = existing?.telegramToken || '';
87
+ } else {
88
+ telegramToken = '';
89
+ }
90
+
91
+ // 6. Generate keys
92
+ console.log('\n 🔑 Generating Ed25519 keypair...');
93
+ let keys;
94
+ if (existing && await askYesNo(rl, ' Keep existing keys?', true)) {
95
+ const { loadKeys } = require('./crypto');
96
+ keys = loadKeys();
97
+ if (!keys) {
98
+ console.log(' ⚠️ No existing keys found, generating new ones...');
99
+ keys = generateKeyPair();
100
+ }
101
+ } else {
102
+ keys = generateKeyPair();
103
+ }
104
+
105
+ rl.close();
106
+
107
+ // Save everything
108
+ ensureDirs();
109
+ saveKeys(keys);
110
+
111
+ const config = {
112
+ agentName,
113
+ humanName,
114
+ port,
115
+ timezone,
116
+ telegramToken: telegramToken || undefined,
117
+ createdAt: existing?.createdAt || new Date().toISOString(),
118
+ updatedAt: new Date().toISOString(),
119
+ };
120
+ saveConfig(config);
121
+
122
+ const fingerprint = getFingerprint(keys.publicKey);
123
+
124
+ console.log(`
125
+ ╔══════════════════════════════════════════╗
126
+ ║ ✅ Setup Complete! ║
127
+ ╚══════════════════════════════════════════╝
128
+
129
+ 👤 Human: ${humanName}
130
+ 🤖 Agent: ${agentName}
131
+ 🌐 Port: ${port}
132
+ 🕐 Timezone: ${timezone}
133
+ 🔑 Fingerprint: ${fingerprint}
134
+ 📁 Config: ${AI2AI_DIR}
135
+ ${telegramToken ? ' 📱 Telegram: Enabled\n' : ''}
136
+ Next steps:
137
+ ai2ai start Start your server
138
+ ai2ai connect <endpoint> Connect to a friend
139
+ ai2ai status Check your setup
140
+ `);
141
+
142
+ } catch (err) {
143
+ rl.close();
144
+ throw err;
145
+ }
146
+ }
147
+
148
+ module.exports = { run };
package/lib/pending.js ADDED
@@ -0,0 +1,114 @@
1
+ /**
2
+ * ai2ai pending — Show pending messages awaiting human review
3
+ * Lists all pending messages with numbered references for easy approve/reject.
4
+ */
5
+
6
+ 'use strict';
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+ const { PENDING_DIR, ensureDirs } = require('./config');
11
+
12
+ /**
13
+ * Load all pending messages, sorted by date
14
+ */
15
+ function loadPending() {
16
+ ensureDirs();
17
+ if (!fs.existsSync(PENDING_DIR)) return [];
18
+
19
+ const files = fs.readdirSync(PENDING_DIR).filter(f => f.endsWith('.json'));
20
+ const pending = [];
21
+
22
+ for (const file of files) {
23
+ try {
24
+ const data = JSON.parse(fs.readFileSync(path.join(PENDING_DIR, file), 'utf-8'));
25
+ pending.push({
26
+ file,
27
+ id: file.replace('.json', ''),
28
+ ...data,
29
+ });
30
+ } catch {
31
+ // Skip corrupted files
32
+ }
33
+ }
34
+
35
+ // Sort by creation date (oldest first)
36
+ pending.sort((a, b) => {
37
+ const da = new Date(a.createdAt || 0);
38
+ const db = new Date(b.createdAt || 0);
39
+ return da - db;
40
+ });
41
+
42
+ return pending;
43
+ }
44
+
45
+ /**
46
+ * Get a pending message by its list number (1-indexed)
47
+ */
48
+ function getPendingByNumber(num) {
49
+ const pending = loadPending();
50
+ if (num < 1 || num > pending.length) return null;
51
+ return pending[num - 1];
52
+ }
53
+
54
+ /**
55
+ * Remove a pending message
56
+ */
57
+ function removePending(id) {
58
+ const filePath = path.join(PENDING_DIR, `${id}.json`);
59
+ if (fs.existsSync(filePath)) {
60
+ fs.unlinkSync(filePath);
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Format a timestamp for display
66
+ */
67
+ function timeAgo(dateStr) {
68
+ const now = Date.now();
69
+ const then = new Date(dateStr).getTime();
70
+ const diff = now - then;
71
+
72
+ if (diff < 60000) return 'just now';
73
+ if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago`;
74
+ if (diff < 86400000) return `${Math.floor(diff / 3600000)}h ago`;
75
+ return `${Math.floor(diff / 86400000)}d ago`;
76
+ }
77
+
78
+ async function run() {
79
+ const pending = loadPending();
80
+
81
+ if (pending.length === 0) {
82
+ console.log('\n 📭 No pending messages.\n');
83
+ return;
84
+ }
85
+
86
+ console.log(`\n 📬 Pending Messages (${pending.length})\n`);
87
+ console.log(' ─'.repeat(25));
88
+
89
+ for (let i = 0; i < pending.length; i++) {
90
+ const p = pending[i];
91
+ const from = p.envelope?.from?.human || p.envelope?.from?.agent || 'Unknown';
92
+ const intent = p.handler || p.envelope?.intent || p.envelope?.type || 'unknown';
93
+ const time = p.createdAt ? timeAgo(p.createdAt) : '';
94
+
95
+ console.log(`\n ${i + 1}. ${intent} from ${from} (${time})`);
96
+
97
+ if (p.approvalMessage) {
98
+ // Indent the approval message
99
+ const lines = p.approvalMessage.split('\n');
100
+ for (const line of lines) {
101
+ console.log(` ${line}`);
102
+ }
103
+ }
104
+
105
+ console.log(` ID: ${p.id.slice(0, 8)}...`);
106
+ }
107
+
108
+ console.log('\n ─'.repeat(25));
109
+ console.log(`\n Commands:`);
110
+ console.log(` ai2ai approve <number> [reply] Accept with optional reply`);
111
+ console.log(` ai2ai reject <number> Decline\n`);
112
+ }
113
+
114
+ module.exports = { run, loadPending, getPendingByNumber, removePending };
@@ -0,0 +1,161 @@
1
+ /**
2
+ * AI2AI Protocol — Message creation and HTTP transport
3
+ * Handles envelope creation, signing, and sending.
4
+ */
5
+
6
+ 'use strict';
7
+
8
+ const crypto = require('crypto');
9
+ const http = require('http');
10
+ const https = require('https');
11
+ const { requireConfig } = require('./config');
12
+ const { loadOrCreateKeys, signMessage, getFingerprint } = require('./crypto');
13
+
14
+ /**
15
+ * Create a new AI2AI message envelope
16
+ */
17
+ function createEnvelope({ to, type, intent, payload, conversationId }) {
18
+ const config = requireConfig();
19
+ const keys = loadOrCreateKeys();
20
+
21
+ const envelope = {
22
+ ai2ai: '0.1',
23
+ id: crypto.randomUUID(),
24
+ timestamp: new Date().toISOString(),
25
+ from: {
26
+ agent: config.agentName,
27
+ node: `${config.agentName}-node`,
28
+ human: config.humanName,
29
+ },
30
+ to: to || { agent: 'unknown', node: 'unknown', human: 'unknown' },
31
+ conversation: conversationId || crypto.randomUUID(),
32
+ type,
33
+ intent: intent || null,
34
+ payload: payload || {},
35
+ requires_human_approval: true,
36
+ };
37
+
38
+ envelope.signature = signMessage(envelope, keys.privateKey);
39
+ return envelope;
40
+ }
41
+
42
+ /**
43
+ * Create a ping envelope for handshaking
44
+ */
45
+ function createPingEnvelope() {
46
+ const config = requireConfig();
47
+ const keys = loadOrCreateKeys();
48
+
49
+ return createEnvelope({
50
+ to: { agent: 'unknown', node: 'unknown', human: 'unknown' },
51
+ type: 'ping',
52
+ payload: {
53
+ capabilities: [
54
+ 'schedule.meeting', 'schedule.call', 'message.relay',
55
+ 'info.request', 'info.share', 'social.introduction',
56
+ ],
57
+ languages: ['en'],
58
+ timezone: config.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone,
59
+ model_info: 'local',
60
+ protocol_versions: ['0.1'],
61
+ public_key: keys.publicKey,
62
+ fingerprint: getFingerprint(keys.publicKey),
63
+ },
64
+ });
65
+ }
66
+
67
+ /**
68
+ * Send an HTTP request to an AI2AI endpoint
69
+ * Returns a promise that resolves with the parsed JSON response.
70
+ */
71
+ function sendRequest(endpoint, envelope) {
72
+ return new Promise((resolve, reject) => {
73
+ const url = new URL(endpoint);
74
+ const transport = url.protocol === 'https:' ? https : http;
75
+
76
+ const body = JSON.stringify(envelope);
77
+
78
+ const options = {
79
+ hostname: url.hostname,
80
+ port: url.port || (url.protocol === 'https:' ? 443 : 80),
81
+ path: url.pathname,
82
+ method: 'POST',
83
+ headers: {
84
+ 'Content-Type': 'application/json',
85
+ 'Content-Length': Buffer.byteLength(body),
86
+ 'X-AI2AI-Version': '0.1',
87
+ },
88
+ timeout: 30000,
89
+ };
90
+
91
+ const req = transport.request(options, (res) => {
92
+ let data = '';
93
+ res.on('data', chunk => { data += chunk; });
94
+ res.on('end', () => {
95
+ try {
96
+ const parsed = JSON.parse(data);
97
+ if (res.statusCode >= 400) {
98
+ reject(new Error(parsed.error || parsed.reason || `HTTP ${res.statusCode}`));
99
+ } else {
100
+ resolve(parsed);
101
+ }
102
+ } catch {
103
+ reject(new Error(`Invalid response from server (HTTP ${res.statusCode})`));
104
+ }
105
+ });
106
+ });
107
+
108
+ req.on('error', (err) => {
109
+ reject(new Error(`Connection failed: ${err.message}`));
110
+ });
111
+
112
+ req.on('timeout', () => {
113
+ req.destroy();
114
+ reject(new Error('Connection timed out (30s)'));
115
+ });
116
+
117
+ req.write(body);
118
+ req.end();
119
+ });
120
+ }
121
+
122
+ /**
123
+ * Fetch a URL via GET (for health checks etc.)
124
+ */
125
+ function fetchGet(endpoint) {
126
+ return new Promise((resolve, reject) => {
127
+ const url = new URL(endpoint);
128
+ const transport = url.protocol === 'https:' ? https : http;
129
+
130
+ const options = {
131
+ hostname: url.hostname,
132
+ port: url.port || (url.protocol === 'https:' ? 443 : 80),
133
+ path: url.pathname,
134
+ method: 'GET',
135
+ timeout: 10000,
136
+ };
137
+
138
+ const req = transport.request(options, (res) => {
139
+ let data = '';
140
+ res.on('data', chunk => { data += chunk; });
141
+ res.on('end', () => {
142
+ try {
143
+ resolve({ status: res.statusCode, data: JSON.parse(data) });
144
+ } catch {
145
+ resolve({ status: res.statusCode, data: data });
146
+ }
147
+ });
148
+ });
149
+
150
+ req.on('error', (err) => reject(new Error(`Connection failed: ${err.message}`)));
151
+ req.on('timeout', () => { req.destroy(); reject(new Error('Timed out')); });
152
+ req.end();
153
+ });
154
+ }
155
+
156
+ module.exports = {
157
+ createEnvelope,
158
+ createPingEnvelope,
159
+ sendRequest,
160
+ fetchGet,
161
+ };
package/lib/send.js ADDED
@@ -0,0 +1,135 @@
1
+ /**
2
+ * ai2ai send — Send a message to another agent
3
+ * Parses natural language to determine intent automatically.
4
+ */
5
+
6
+ 'use strict';
7
+
8
+ const crypto = require('crypto');
9
+ const { requireConfig, findContact } = require('./config');
10
+ const { createEnvelope, sendRequest } = require('./protocol');
11
+
12
+ /**
13
+ * Detect if a message looks like a scheduling request
14
+ */
15
+ function isSchedulingRequest(message) {
16
+ const patterns = [
17
+ /\b(meet|meeting|schedule|calendar|book|arrange)\b/i,
18
+ /\b(monday|tuesday|wednesday|thursday|friday|saturday|sunday)\b/i,
19
+ /\b(next week|this week|tomorrow|tonight|this evening)\b/i,
20
+ /\b(lunch|dinner|breakfast|coffee|drinks|call)\b/i,
21
+ /\b(\d{1,2}:\d{2}|noon|midnight|\d{1,2}\s*(am|pm))\b/i,
22
+ /\b(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)\w*\s+\d/i,
23
+ ];
24
+
25
+ let matches = 0;
26
+ for (const p of patterns) {
27
+ if (p.test(message)) matches++;
28
+ }
29
+ return matches >= 2; // Need at least 2 scheduling signals
30
+ }
31
+
32
+ /**
33
+ * Detect if a message is a question
34
+ */
35
+ function isQuestion(message) {
36
+ return /\?$/.test(message.trim()) ||
37
+ /^(what|where|when|who|why|how|can|could|would|do|does|is|are)\b/i.test(message.trim());
38
+ }
39
+
40
+ async function run(args) {
41
+ if (args.length < 2) {
42
+ console.error('\n ❌ Usage: ai2ai send <contact> <message>');
43
+ console.error(' Example: ai2ai send alex "dinner next Thursday?"\n');
44
+ process.exit(1);
45
+ }
46
+
47
+ const contactQuery = args[0];
48
+ const message = args.slice(1).join(' ').replace(/^["']|["']$/g, '');
49
+
50
+ const config = requireConfig();
51
+ const contact = findContact(contactQuery);
52
+
53
+ if (!contact) {
54
+ console.error(`\n ❌ Contact "${contactQuery}" not found.`);
55
+ console.error(' Run `ai2ai contacts` to see known contacts.\n');
56
+ process.exit(1);
57
+ }
58
+
59
+ if (!contact.endpoint) {
60
+ console.error(`\n ❌ No endpoint for "${contactQuery}".`);
61
+ console.error(' Run `ai2ai connect <endpoint>` to add one.\n');
62
+ process.exit(1);
63
+ }
64
+
65
+ // Determine intent based on message content
66
+ let intent, payload;
67
+
68
+ if (isSchedulingRequest(message)) {
69
+ intent = 'schedule.meeting';
70
+ payload = {
71
+ subject: message,
72
+ proposed_times: [], // The receiving agent will parse this
73
+ duration_minutes: 60,
74
+ notes: message,
75
+ flexibility: 'high',
76
+ };
77
+ console.log(`\n 📅 Detected scheduling request`);
78
+ } else if (isQuestion(message)) {
79
+ intent = 'info.request';
80
+ payload = {
81
+ question: message,
82
+ };
83
+ console.log(`\n ❓ Detected question`);
84
+ } else {
85
+ intent = 'message.relay';
86
+ payload = {
87
+ message,
88
+ urgency: 'low',
89
+ reply_requested: true,
90
+ };
91
+ console.log(`\n 💬 Sending message`);
92
+ }
93
+
94
+ console.log(` 📤 To: ${contact.humanName || contact.id} (${contact.id})`);
95
+ console.log(` 🎯 Intent: ${intent}`);
96
+
97
+ try {
98
+ const envelope = createEnvelope({
99
+ to: {
100
+ agent: contact.id,
101
+ node: `${contact.id}-node`,
102
+ human: contact.humanName || contact.id,
103
+ },
104
+ type: 'request',
105
+ intent,
106
+ payload,
107
+ });
108
+
109
+ const response = await sendRequest(contact.endpoint, envelope);
110
+
111
+ if (response.status === 'pending_approval') {
112
+ console.log(`\n ✅ Message delivered! Awaiting approval from ${contact.humanName || contact.id}.`);
113
+ console.log(` 💬 Conversation: ${envelope.conversation.slice(0, 8)}...`);
114
+ console.log(' Run `ai2ai pending` to check for responses.\n');
115
+ } else if (response.status === 'ok') {
116
+ console.log(`\n ✅ Message delivered and processed!`);
117
+ if (response.payload) {
118
+ console.log(` 📨 Response: ${JSON.stringify(response.payload)}`);
119
+ }
120
+ console.log('');
121
+ } else if (response.status === 'rejected') {
122
+ console.error(`\n ❌ Message rejected: ${response.reason}\n`);
123
+ process.exit(1);
124
+ } else {
125
+ console.log(`\n 📨 Response: ${JSON.stringify(response, null, 2)}\n`);
126
+ }
127
+
128
+ } catch (err) {
129
+ console.error(`\n ❌ Failed to send: ${err.message}`);
130
+ console.error(' Is the recipient\'s server running?\n');
131
+ process.exit(1);
132
+ }
133
+ }
134
+
135
+ module.exports = { run };