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 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 };
@@ -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 };