a2acalling 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.
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Call Monitor - Auto-concludes idle conversations
3
+ *
4
+ * Monitors active conversations and triggers summarization when:
5
+ * - No messages for 60 seconds (configurable)
6
+ * - Explicit end signal received
7
+ * - Max duration exceeded
8
+ */
9
+
10
+ class CallMonitor {
11
+ constructor(options = {}) {
12
+ this.convStore = options.convStore;
13
+ this.summarizer = options.summarizer;
14
+ this.notifyOwner = options.notifyOwner || (() => {});
15
+ this.ownerContext = options.ownerContext || {};
16
+
17
+ // Timing config
18
+ this.idleTimeoutMs = options.idleTimeoutMs || 60000; // 60s idle
19
+ this.maxDurationMs = options.maxDurationMs || 300000; // 5min max
20
+ this.checkIntervalMs = options.checkIntervalMs || 10000; // Check every 10s
21
+
22
+ this.intervalId = null;
23
+ this.activeConversations = new Map(); // conversationId -> { startTime, lastActivity, callerInfo }
24
+ }
25
+
26
+ /**
27
+ * Start monitoring
28
+ */
29
+ start() {
30
+ if (this.intervalId) return;
31
+
32
+ this.intervalId = setInterval(() => {
33
+ this._checkIdleConversations();
34
+ }, this.checkIntervalMs);
35
+
36
+ console.log('[a2a] Call monitor started');
37
+ }
38
+
39
+ /**
40
+ * Stop monitoring
41
+ */
42
+ stop() {
43
+ if (this.intervalId) {
44
+ clearInterval(this.intervalId);
45
+ this.intervalId = null;
46
+ console.log('[a2a] Call monitor stopped');
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Track a new or continuing conversation
52
+ */
53
+ trackActivity(conversationId, callerInfo = {}) {
54
+ const now = Date.now();
55
+ const existing = this.activeConversations.get(conversationId);
56
+
57
+ if (existing) {
58
+ existing.lastActivity = now;
59
+ } else {
60
+ this.activeConversations.set(conversationId, {
61
+ startTime: now,
62
+ lastActivity: now,
63
+ callerInfo
64
+ });
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Explicitly end a conversation
70
+ */
71
+ async endConversation(conversationId, reason = 'explicit') {
72
+ const convData = this.activeConversations.get(conversationId);
73
+ this.activeConversations.delete(conversationId);
74
+
75
+ if (!this.convStore) return { success: false, error: 'no_store' };
76
+
77
+ try {
78
+ const result = await this.convStore.concludeConversation(conversationId, {
79
+ summarizer: this.summarizer,
80
+ ownerContext: this.ownerContext
81
+ });
82
+
83
+ if (result.success) {
84
+ // Notify owner
85
+ const context = this.convStore.getConversationContext(conversationId);
86
+ this.notifyOwner({
87
+ type: 'conversation_concluded',
88
+ reason,
89
+ conversation: context,
90
+ callerInfo: convData?.callerInfo
91
+ }).catch(err => {
92
+ console.error('[a2a] Failed to notify owner:', err.message);
93
+ });
94
+ }
95
+
96
+ return result;
97
+ } catch (err) {
98
+ console.error('[a2a] Failed to conclude conversation:', err.message);
99
+ return { success: false, error: err.message };
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Check for idle conversations
105
+ */
106
+ async _checkIdleConversations() {
107
+ const now = Date.now();
108
+
109
+ for (const [convId, data] of this.activeConversations) {
110
+ const idleTime = now - data.lastActivity;
111
+ const duration = now - data.startTime;
112
+
113
+ // Check max duration
114
+ if (duration > this.maxDurationMs) {
115
+ console.log(`[a2a] Conversation ${convId} exceeded max duration, concluding...`);
116
+ await this.endConversation(convId, 'max_duration');
117
+ continue;
118
+ }
119
+
120
+ // Check idle timeout
121
+ if (idleTime > this.idleTimeoutMs) {
122
+ console.log(`[a2a] Conversation ${convId} idle for ${Math.round(idleTime / 1000)}s, concluding...`);
123
+ await this.endConversation(convId, 'idle_timeout');
124
+ }
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Get active conversation count
130
+ */
131
+ getActiveCount() {
132
+ return this.activeConversations.size;
133
+ }
134
+
135
+ /**
136
+ * Get all active conversation IDs
137
+ */
138
+ getActiveConversations() {
139
+ return Array.from(this.activeConversations.keys());
140
+ }
141
+ }
142
+
143
+ module.exports = { CallMonitor };
@@ -0,0 +1,208 @@
1
+ /**
2
+ * A2A Client - Make calls to remote agents
3
+ */
4
+
5
+ const https = require('https');
6
+ const http = require('http');
7
+
8
+ class A2AClient {
9
+ constructor(options = {}) {
10
+ this.timeout = options.timeout || 60000;
11
+ this.caller = options.caller || {};
12
+ }
13
+
14
+ /**
15
+ * Parse an a2a:// URL
16
+ */
17
+ static parseInvite(inviteUrl) {
18
+ // Support both a2a:// and legacy oclaw:// schemes
19
+ const match = inviteUrl.match(/^(?:a2a|oclaw):\/\/([^/]+)\/(.+)$/);
20
+ if (!match) {
21
+ throw new Error(`Invalid invite URL: ${inviteUrl}`);
22
+ }
23
+ return { host: match[1], token: match[2] };
24
+ }
25
+
26
+ /**
27
+ * Call a remote agent
28
+ *
29
+ * @param {string} endpoint - a2a:// URL or {host, token}
30
+ * @param {string} message - Message to send
31
+ * @param {object} options - Additional options
32
+ * @returns {Promise<object>} Response from remote agent
33
+ */
34
+ async call(endpoint, message, options = {}) {
35
+ let host, token;
36
+
37
+ if (typeof endpoint === 'string') {
38
+ ({ host, token } = A2AClient.parseInvite(endpoint));
39
+ } else {
40
+ ({ host, token } = endpoint);
41
+ }
42
+
43
+ const { conversationId, context, timeoutSeconds } = options;
44
+
45
+ const body = JSON.stringify({
46
+ message,
47
+ conversation_id: conversationId,
48
+ caller: this.caller,
49
+ context,
50
+ timeout_seconds: timeoutSeconds || 60
51
+ });
52
+
53
+ const isLocalhost = host === 'localhost' || host.startsWith('localhost:') || host.startsWith('127.');
54
+ const hasExplicitPort = host.includes(':');
55
+ const port = hasExplicitPort ? parseInt(host.split(':')[1]) : (isLocalhost ? 80 : 443);
56
+ // Use HTTP for localhost or explicit non-443 ports, HTTPS otherwise
57
+ const useHttp = isLocalhost || (hasExplicitPort && port !== 443);
58
+ const protocol = useHttp ? http : https;
59
+ const hostname = host.split(':')[0];
60
+
61
+ return new Promise((resolve, reject) => {
62
+ const req = protocol.request({
63
+ hostname,
64
+ port,
65
+ path: '/api/federation/invoke',
66
+ method: 'POST',
67
+ headers: {
68
+ 'Authorization': `Bearer ${token}`,
69
+ 'Content-Type': 'application/json',
70
+ 'Content-Length': Buffer.byteLength(body)
71
+ },
72
+ timeout: this.timeout
73
+ }, (res) => {
74
+ let data = '';
75
+ res.on('data', chunk => data += chunk);
76
+ res.on('end', () => {
77
+ try {
78
+ const json = JSON.parse(data);
79
+ if (res.statusCode >= 400) {
80
+ reject(new A2AError(json.error || 'request_failed', json.message || data, res.statusCode));
81
+ } else {
82
+ resolve(json);
83
+ }
84
+ } catch (e) {
85
+ reject(new A2AError('parse_error', `Failed to parse response: ${data}`, res.statusCode));
86
+ }
87
+ });
88
+ });
89
+
90
+ req.on('error', (e) => {
91
+ reject(new A2AError('network_error', e.message));
92
+ });
93
+
94
+ req.on('timeout', () => {
95
+ req.destroy();
96
+ reject(new A2AError('timeout', 'Request timed out'));
97
+ });
98
+
99
+ req.write(body);
100
+ req.end();
101
+ });
102
+ }
103
+
104
+ /**
105
+ * Check if a remote agent is available
106
+ */
107
+ async ping(endpoint) {
108
+ let host;
109
+
110
+ if (typeof endpoint === 'string') {
111
+ ({ host } = A2AClient.parseInvite(endpoint));
112
+ } else {
113
+ ({ host } = endpoint);
114
+ }
115
+
116
+ const isLocalhost = host === 'localhost' || host.startsWith('localhost:') || host.startsWith('127.');
117
+ const hasExplicitPort = host.includes(':');
118
+ const port = hasExplicitPort ? parseInt(host.split(':')[1]) : (isLocalhost ? 80 : 443);
119
+ const useHttp = isLocalhost || (hasExplicitPort && port !== 443);
120
+ const protocol = useHttp ? http : https;
121
+ const hostname = host.split(':')[0];
122
+
123
+ return new Promise((resolve, reject) => {
124
+ const req = protocol.request({
125
+ hostname,
126
+ port,
127
+ path: '/api/federation/ping',
128
+ method: 'GET',
129
+ timeout: 5000
130
+ }, (res) => {
131
+ let data = '';
132
+ res.on('data', chunk => data += chunk);
133
+ res.on('end', () => {
134
+ try {
135
+ resolve(JSON.parse(data));
136
+ } catch {
137
+ resolve({ pong: res.statusCode === 200 });
138
+ }
139
+ });
140
+ });
141
+
142
+ req.on('error', () => resolve({ pong: false }));
143
+ req.on('timeout', () => {
144
+ req.destroy();
145
+ resolve({ pong: false });
146
+ });
147
+ req.end();
148
+ });
149
+ }
150
+
151
+ /**
152
+ * Get federation status of a remote
153
+ */
154
+ async status(endpoint) {
155
+ let host;
156
+
157
+ if (typeof endpoint === 'string') {
158
+ ({ host } = A2AClient.parseInvite(endpoint));
159
+ } else {
160
+ ({ host } = endpoint);
161
+ }
162
+
163
+ const isLocalhost = host === 'localhost' || host.startsWith('localhost:') || host.startsWith('127.');
164
+ const hasExplicitPort = host.includes(':');
165
+ const port = hasExplicitPort ? parseInt(host.split(':')[1]) : (isLocalhost ? 80 : 443);
166
+ const useHttp = isLocalhost || (hasExplicitPort && port !== 443);
167
+ const protocol = useHttp ? http : https;
168
+ const hostname = host.split(':')[0];
169
+
170
+ return new Promise((resolve, reject) => {
171
+ const req = protocol.request({
172
+ hostname,
173
+ port,
174
+ path: '/api/federation/status',
175
+ method: 'GET',
176
+ timeout: 5000
177
+ }, (res) => {
178
+ let data = '';
179
+ res.on('data', chunk => data += chunk);
180
+ res.on('end', () => {
181
+ try {
182
+ resolve(JSON.parse(data));
183
+ } catch {
184
+ reject(new A2AError('parse_error', 'Invalid status response'));
185
+ }
186
+ });
187
+ });
188
+
189
+ req.on('error', (e) => reject(new A2AError('network_error', e.message)));
190
+ req.on('timeout', () => {
191
+ req.destroy();
192
+ reject(new A2AError('timeout', 'Request timed out'));
193
+ });
194
+ req.end();
195
+ });
196
+ }
197
+ }
198
+
199
+ class A2AError extends Error {
200
+ constructor(code, message, statusCode = null) {
201
+ super(message);
202
+ this.name = 'A2AError';
203
+ this.code = code;
204
+ this.statusCode = statusCode;
205
+ }
206
+ }
207
+
208
+ module.exports = { A2AClient, A2AError };
@@ -0,0 +1,173 @@
1
+ /**
2
+ * A2A Configuration Management
3
+ *
4
+ * Stores permission tiers, default settings, and user preferences.
5
+ */
6
+
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+
10
+ const CONFIG_DIR = process.env.A2A_CONFIG_DIR ||
11
+ process.env.OPENCLAW_CONFIG_DIR ||
12
+ path.join(process.env.HOME || '/tmp', '.config', 'openclaw');
13
+
14
+ const CONFIG_FILE = path.join(CONFIG_DIR, 'a2a-config.json');
15
+
16
+ const DEFAULT_CONFIG = {
17
+ // Has the user completed onboarding?
18
+ onboardingComplete: false,
19
+
20
+ // Permission tiers
21
+ tiers: {
22
+ public: {
23
+ name: 'Public',
24
+ description: 'Basic networking - safe for anyone',
25
+ capabilities: [],
26
+ disclosure: 'minimal',
27
+ examples: ['calendar availability', 'public social posts', 'general questions']
28
+ },
29
+ friends: {
30
+ name: 'Friends',
31
+ description: 'Most capabilities, no sensitive financial data',
32
+ capabilities: [],
33
+ disclosure: 'public',
34
+ examples: ['email summaries', 'schedule meetings', 'project discussions']
35
+ },
36
+ private: {
37
+ name: 'Private',
38
+ description: 'Full access - only for you',
39
+ capabilities: [],
40
+ disclosure: 'public',
41
+ examples: ['financial data', 'personal notes', 'private conversations']
42
+ },
43
+ custom: {
44
+ name: 'Custom',
45
+ description: 'User-defined permissions',
46
+ capabilities: [],
47
+ disclosure: 'minimal',
48
+ examples: []
49
+ }
50
+ },
51
+
52
+ // Default token settings
53
+ defaults: {
54
+ expiration: 'never', // never, 1d, 7d, 30d
55
+ maxCalls: 100, // per token
56
+ rateLimit: {
57
+ perMinute: 10,
58
+ perHour: 100,
59
+ perDay: 1000
60
+ },
61
+ maxPendingRequests: 5 // max connection requests per hour
62
+ },
63
+
64
+ // Agent info
65
+ agent: {
66
+ name: '',
67
+ description: '',
68
+ hostname: ''
69
+ },
70
+
71
+ // Timestamps
72
+ createdAt: null,
73
+ updatedAt: null
74
+ };
75
+
76
+ class A2AConfig {
77
+ constructor() {
78
+ this._ensureDir();
79
+ this.config = this._load();
80
+ }
81
+
82
+ _ensureDir() {
83
+ if (!fs.existsSync(CONFIG_DIR)) {
84
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
85
+ }
86
+ }
87
+
88
+ _load() {
89
+ if (fs.existsSync(CONFIG_FILE)) {
90
+ try {
91
+ const saved = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
92
+ return { ...DEFAULT_CONFIG, ...saved };
93
+ } catch (e) {
94
+ console.error('[a2a] Config corrupted, using defaults');
95
+ return { ...DEFAULT_CONFIG };
96
+ }
97
+ }
98
+ return { ...DEFAULT_CONFIG };
99
+ }
100
+
101
+ _save() {
102
+ this.config.updatedAt = new Date().toISOString();
103
+ if (!this.config.createdAt) {
104
+ this.config.createdAt = this.config.updatedAt;
105
+ }
106
+ const tmpPath = `${CONFIG_FILE}.tmp`;
107
+ fs.writeFileSync(tmpPath, JSON.stringify(this.config, null, 2));
108
+ fs.renameSync(tmpPath, CONFIG_FILE);
109
+ }
110
+
111
+ // Check if onboarding is complete
112
+ isOnboarded() {
113
+ return this.config.onboardingComplete === true;
114
+ }
115
+
116
+ // Mark onboarding complete
117
+ completeOnboarding() {
118
+ this.config.onboardingComplete = true;
119
+ this._save();
120
+ }
121
+
122
+ // Reset to run onboarding again
123
+ resetOnboarding() {
124
+ this.config.onboardingComplete = false;
125
+ this._save();
126
+ }
127
+
128
+ // Get/set tiers
129
+ getTiers() {
130
+ return this.config.tiers;
131
+ }
132
+
133
+ setTier(tierName, tierConfig) {
134
+ this.config.tiers[tierName] = { ...this.config.tiers[tierName], ...tierConfig };
135
+ this._save();
136
+ }
137
+
138
+ // Get/set defaults
139
+ getDefaults() {
140
+ return this.config.defaults;
141
+ }
142
+
143
+ setDefaults(defaults) {
144
+ this.config.defaults = { ...this.config.defaults, ...defaults };
145
+ this._save();
146
+ }
147
+
148
+ // Get/set agent info
149
+ getAgent() {
150
+ return this.config.agent;
151
+ }
152
+
153
+ setAgent(agent) {
154
+ this.config.agent = { ...this.config.agent, ...agent };
155
+ this._save();
156
+ }
157
+
158
+ // Get full config
159
+ getAll() {
160
+ return this.config;
161
+ }
162
+
163
+ // Export for sharing
164
+ export() {
165
+ return {
166
+ tiers: this.config.tiers,
167
+ defaults: this.config.defaults,
168
+ agent: this.config.agent
169
+ };
170
+ }
171
+ }
172
+
173
+ module.exports = { A2AConfig, DEFAULT_CONFIG, CONFIG_FILE };