a2acalling 0.6.47 → 0.6.49

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,93 @@
1
+ /**
2
+ * Update Checker
3
+ *
4
+ * Zero-dependency npm version checks for a2acalling.
5
+ */
6
+
7
+ const REGISTRY_URL = 'https://registry.npmjs.org/a2acalling/latest';
8
+ const FETCH_TIMEOUT_MS = 15000;
9
+
10
+ function parseVersion(str) {
11
+ if (!str || typeof str !== 'string') return null;
12
+ const match = str.trim().match(/^(\d+)\.(\d+)\.(\d+)/);
13
+ if (!match) return null;
14
+ return {
15
+ major: Number.parseInt(match[1], 10),
16
+ minor: Number.parseInt(match[2], 10),
17
+ patch: Number.parseInt(match[3], 10)
18
+ };
19
+ }
20
+
21
+ function compareVersions(a, b) {
22
+ const va = parseVersion(a);
23
+ const vb = parseVersion(b);
24
+ if (!va || !vb) return 0;
25
+ if (va.major !== vb.major) return va.major < vb.major ? -1 : 1;
26
+ if (va.minor !== vb.minor) return va.minor < vb.minor ? -1 : 1;
27
+ if (va.patch !== vb.patch) return va.patch < vb.patch ? -1 : 1;
28
+ return 0;
29
+ }
30
+
31
+ function isSameMajor(a, b) {
32
+ const va = parseVersion(a);
33
+ const vb = parseVersion(b);
34
+ if (!va || !vb) return false;
35
+ return va.major === vb.major;
36
+ }
37
+
38
+ async function fetchLatestVersion() {
39
+ try {
40
+ const controller = new AbortController();
41
+ const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
42
+ const res = await fetch(REGISTRY_URL, {
43
+ signal: controller.signal,
44
+ headers: { Accept: 'application/json' }
45
+ });
46
+ clearTimeout(timeout);
47
+ if (!res.ok) {
48
+ return { error: `Registry returned ${res.status}` };
49
+ }
50
+ const data = await res.json();
51
+ if (!data || typeof data.version !== 'string') {
52
+ return { error: 'No version field in registry response' };
53
+ }
54
+ return { version: data.version };
55
+ } catch (err) {
56
+ if (err && err.name === 'AbortError') {
57
+ return { error: 'Registry request timed out' };
58
+ }
59
+ return { error: err && err.message ? err.message : 'Unknown fetch error' };
60
+ }
61
+ }
62
+
63
+ async function checkForUpdate(currentVersion) {
64
+ const result = await fetchLatestVersion();
65
+ if (result.error) {
66
+ return {
67
+ available: false,
68
+ current: currentVersion,
69
+ latest: null,
70
+ sameMajor: false,
71
+ error: result.error
72
+ };
73
+ }
74
+
75
+ const latest = result.version;
76
+ return {
77
+ available: compareVersions(currentVersion, latest) < 0,
78
+ current: currentVersion,
79
+ latest,
80
+ sameMajor: isSameMajor(currentVersion, latest)
81
+ };
82
+ }
83
+
84
+ module.exports = {
85
+ REGISTRY_URL,
86
+ FETCH_TIMEOUT_MS,
87
+ parseVersion,
88
+ compareVersions,
89
+ isSameMajor,
90
+ fetchLatestVersion,
91
+ checkForUpdate
92
+ };
93
+
@@ -0,0 +1,313 @@
1
+ const { execFile } = require('child_process');
2
+ const { EventEmitter } = require('events');
3
+ const { createLogger } = require('./logger');
4
+ const { checkForUpdate, isSameMajor } = require('./update-checker');
5
+
6
+ const DEFAULT_INTERVAL_MS = 60 * 60 * 1000;
7
+ const DEFAULT_NPM_TIMEOUT_MS = 2 * 60 * 1000;
8
+
9
+ function isUpdateSafe(callMonitor) {
10
+ if (!callMonitor || typeof callMonitor.getActiveCount !== 'function') return true;
11
+ return Number(callMonitor.getActiveCount()) === 0;
12
+ }
13
+
14
+ function shouldApplyUpdate(currentVersion, latestVersion, options = {}) {
15
+ if (!latestVersion) return false;
16
+ if (options.allowMajor) return true;
17
+ return isSameMajor(currentVersion, latestVersion);
18
+ }
19
+
20
+ function autoUpdateDisabledByEnv() {
21
+ if (String(process.env.CI || '').toLowerCase() === 'true') return true;
22
+ if (String(process.env.A2A_AUTO_UPDATE || '').trim() === '0') return true;
23
+ if (String(process.env.NO_AUTO_UPDATE || '').trim() === '1') return true;
24
+ return false;
25
+ }
26
+
27
+ function execFilePromise(cmd, args, options = {}) {
28
+ return new Promise((resolve, reject) => {
29
+ execFile(cmd, args, options, (err, stdout, stderr) => {
30
+ if (err) {
31
+ err.stdout = stdout;
32
+ err.stderr = stderr;
33
+ reject(err);
34
+ return;
35
+ }
36
+ resolve({ stdout, stderr });
37
+ });
38
+ });
39
+ }
40
+
41
+ class UpdateManager extends EventEmitter {
42
+ constructor(options = {}) {
43
+ super();
44
+ this.currentVersion = options.currentVersion;
45
+ this.config = options.config || null;
46
+ this.logger = options.logger || createLogger({ component: 'a2a.updater' });
47
+ this.intervalMs = Number.isFinite(options.intervalMs) && options.intervalMs > 0
48
+ ? options.intervalMs
49
+ : DEFAULT_INTERVAL_MS;
50
+ this.allowMajor = Boolean(options.allowMajor);
51
+ this.enabled = options.enabled !== false;
52
+ this.getCallMonitor = typeof options.getCallMonitor === 'function'
53
+ ? options.getCallMonitor
54
+ : (() => null);
55
+ this.installTimeoutMs = Number.isFinite(options.installTimeoutMs) && options.installTimeoutMs > 0
56
+ ? options.installTimeoutMs
57
+ : DEFAULT_NPM_TIMEOUT_MS;
58
+ this.restartFn = typeof options.restartFn === 'function' ? options.restartFn : null;
59
+ this._execFile = options.execFile || execFilePromise;
60
+ this._status = {
61
+ state: 'up_to_date',
62
+ enabled: this.enabled,
63
+ current_version: this.currentVersion,
64
+ latest_version: this.currentVersion,
65
+ target_version: null,
66
+ active_calls: 0,
67
+ last_checked_at: null,
68
+ last_success_at: null,
69
+ last_error: null,
70
+ defer_reason: null
71
+ };
72
+ this._timer = null;
73
+ this._running = false;
74
+ this._pendingManualUpdate = false;
75
+
76
+ this._loadConfig();
77
+ if (autoUpdateDisabledByEnv()) {
78
+ this.enabled = false;
79
+ this._status.enabled = false;
80
+ this.logger.info('Auto-update disabled by environment', { event: 'updater_env_disabled' });
81
+ }
82
+ }
83
+
84
+ _loadConfig() {
85
+ if (!this.config || typeof this.config.getAutoUpdate !== 'function') return;
86
+ const cfg = this.config.getAutoUpdate();
87
+ if (cfg && typeof cfg === 'object') {
88
+ if (typeof cfg.enabled === 'boolean') this.enabled = cfg.enabled;
89
+ if (Number.isFinite(cfg.intervalMs) && cfg.intervalMs > 0) this.intervalMs = cfg.intervalMs;
90
+ if (typeof cfg.allowMajor === 'boolean') this.allowMajor = cfg.allowMajor;
91
+ }
92
+ this._status.enabled = this.enabled;
93
+ }
94
+
95
+ _persistConfigPatch(patch) {
96
+ if (!this.config || typeof this.config.setAutoUpdate !== 'function') return;
97
+ try {
98
+ this.config.setAutoUpdate(patch);
99
+ } catch (err) {
100
+ this.logger.warn('Failed to persist auto-update config patch', {
101
+ event: 'updater_config_patch_failed',
102
+ error: err
103
+ });
104
+ }
105
+ }
106
+
107
+ _setStatus(patch) {
108
+ this._status = { ...this._status, ...patch };
109
+ this.emit('status', this.getStatus());
110
+ }
111
+
112
+ getStatus() {
113
+ return {
114
+ ...this._status,
115
+ enabled: this.enabled,
116
+ interval_ms: this.intervalMs,
117
+ allow_major: this.allowMajor
118
+ };
119
+ }
120
+
121
+ async setEnabled(enabled) {
122
+ this.enabled = Boolean(enabled);
123
+ this._setStatus({ enabled: this.enabled });
124
+ this._persistConfigPatch({ enabled: this.enabled });
125
+ if (this.enabled && !this._timer) {
126
+ this.start();
127
+ await this.triggerCheck({ reason: 'manual_enable' });
128
+ }
129
+ if (!this.enabled && this._timer) {
130
+ this.stop();
131
+ }
132
+ return this.getStatus();
133
+ }
134
+
135
+ start() {
136
+ if (this._timer) return;
137
+ if (!this.enabled) {
138
+ this.logger.info('Auto-updater disabled', { event: 'updater_disabled' });
139
+ return;
140
+ }
141
+
142
+ this._timer = setInterval(() => {
143
+ this.triggerCheck({ reason: 'interval' }).catch((err) => {
144
+ this.logger.warn('Auto-update interval check failed', {
145
+ event: 'updater_interval_failed',
146
+ error: err
147
+ });
148
+ });
149
+ }, this.intervalMs);
150
+
151
+ this.logger.info('Auto-updater started', {
152
+ event: 'updater_started',
153
+ data: {
154
+ interval_ms: this.intervalMs,
155
+ allow_major: this.allowMajor
156
+ }
157
+ });
158
+ }
159
+
160
+ stop() {
161
+ if (!this._timer) return;
162
+ clearInterval(this._timer);
163
+ this._timer = null;
164
+ this.logger.info('Auto-updater stopped', { event: 'updater_stopped' });
165
+ }
166
+
167
+ async triggerCheck(options = {}) {
168
+ return this._runCycle({ ...options, manualUpdate: false, forceCheck: Boolean(options.forceCheck) });
169
+ }
170
+
171
+ async triggerUpdate(options = {}) {
172
+ this._pendingManualUpdate = true;
173
+ return this._runCycle({ ...options, manualUpdate: true });
174
+ }
175
+
176
+ async _runCycle(options = {}) {
177
+ if (this._running) {
178
+ return this.getStatus();
179
+ }
180
+ if (!this.enabled && !options.manualUpdate && !options.forceCheck) {
181
+ return this.getStatus();
182
+ }
183
+
184
+ this._running = true;
185
+ const nowIso = new Date().toISOString();
186
+ this._setStatus({
187
+ state: 'checking',
188
+ last_checked_at: nowIso,
189
+ last_error: null,
190
+ defer_reason: null
191
+ });
192
+
193
+ try {
194
+ const result = await checkForUpdate(this.currentVersion);
195
+ if (result.error) {
196
+ this._setStatus({
197
+ state: 'failed',
198
+ latest_version: this.currentVersion,
199
+ last_error: result.error
200
+ });
201
+ return this.getStatus();
202
+ }
203
+
204
+ const latest = result.latest || this.currentVersion;
205
+ if (!result.available) {
206
+ this._setStatus({
207
+ state: 'up_to_date',
208
+ latest_version: latest,
209
+ target_version: null
210
+ });
211
+ return this.getStatus();
212
+ }
213
+
214
+ if (!shouldApplyUpdate(this.currentVersion, latest, { allowMajor: this.allowMajor })) {
215
+ this._setStatus({
216
+ state: 'up_to_date',
217
+ latest_version: latest,
218
+ target_version: null,
219
+ defer_reason: 'cross_major_blocked'
220
+ });
221
+ return this.getStatus();
222
+ }
223
+
224
+ const monitor = this.getCallMonitor();
225
+ const activeCalls = monitor && typeof monitor.getActiveCount === 'function'
226
+ ? Number(monitor.getActiveCount()) || 0
227
+ : 0;
228
+ if (!isUpdateSafe(monitor) && !options.force) {
229
+ this._setStatus({
230
+ state: 'waiting_for_safe_restart',
231
+ latest_version: latest,
232
+ target_version: latest,
233
+ active_calls: activeCalls,
234
+ defer_reason: 'active_calls'
235
+ });
236
+ return this.getStatus();
237
+ }
238
+
239
+ this._setStatus({
240
+ state: 'downloading',
241
+ latest_version: latest,
242
+ target_version: latest,
243
+ active_calls: activeCalls
244
+ });
245
+
246
+ await this._installVersion(latest);
247
+ this.currentVersion = latest;
248
+ this._persistConfigPatch({ lastGoodVersion: latest });
249
+
250
+ this._setStatus({
251
+ state: 'restarting',
252
+ current_version: latest,
253
+ latest_version: latest,
254
+ target_version: latest,
255
+ last_success_at: new Date().toISOString(),
256
+ last_error: null,
257
+ defer_reason: null,
258
+ active_calls: 0
259
+ });
260
+
261
+ if (this.restartFn) {
262
+ await this.restartFn(latest);
263
+ } else {
264
+ this._setStatus({
265
+ state: 'up_to_date',
266
+ current_version: latest,
267
+ latest_version: latest,
268
+ target_version: null
269
+ });
270
+ }
271
+
272
+ return this.getStatus();
273
+ } catch (err) {
274
+ const message = err && err.message ? err.message : 'update_failed';
275
+ this._setStatus({
276
+ state: 'failed',
277
+ last_error: message
278
+ });
279
+ this.logger.error('Auto-update failed', {
280
+ event: 'updater_failed',
281
+ error: err
282
+ });
283
+ return this.getStatus();
284
+ } finally {
285
+ this._running = false;
286
+ this._pendingManualUpdate = false;
287
+ }
288
+ }
289
+
290
+ async _installVersion(version) {
291
+ const args = ['install', '-g', `a2acalling@${version}`];
292
+ this._setStatus({ state: 'applying' });
293
+ this.logger.info('Installing auto-update target', {
294
+ event: 'updater_install_start',
295
+ data: { version }
296
+ });
297
+ await this._execFile('npm', args, {
298
+ timeout: this.installTimeoutMs,
299
+ env: process.env
300
+ });
301
+ this.logger.info('Auto-update install complete', {
302
+ event: 'updater_install_done',
303
+ data: { version }
304
+ });
305
+ }
306
+ }
307
+
308
+ module.exports = {
309
+ DEFAULT_INTERVAL_MS,
310
+ isUpdateSafe,
311
+ shouldApplyUpdate,
312
+ UpdateManager
313
+ };
package/src/routes/a2a.js CHANGED
@@ -174,17 +174,24 @@ function createRoutes(options = {}) {
174
174
  maxDurationMs: options.maxDurationMs || 300000,
175
175
  logger: logger.child({ component: 'a2a.call-monitor' })
176
176
  });
177
+ if (typeof options.onCallMonitor === 'function') {
178
+ try {
179
+ options.onCallMonitor(monitor);
180
+ } catch (_) {}
181
+ }
177
182
 
178
183
  /**
179
184
  * GET /status
180
185
  * Check if A2A is enabled
181
186
  */
182
187
  router.get('/status', (req, res) => {
188
+ const activeCalls = monitor ? monitor.getActiveCount() : 0;
183
189
  res.json({
184
190
  a2a: true,
185
191
  version: require('../../package.json').version,
186
192
  capabilities: ['invoke', 'multi-turn'],
187
- rate_limits: limits
193
+ rate_limits: limits,
194
+ active_calls: activeCalls
188
195
  });
189
196
  });
190
197
 
@@ -210,12 +210,45 @@ function buildContext(options = {}) {
210
210
  config,
211
211
  convStore,
212
212
  callbookStore,
213
+ getUpdateManager: typeof options.getUpdateManager === 'function'
214
+ ? options.getUpdateManager
215
+ : (() => null),
213
216
  logger,
214
217
  agentContext,
215
218
  staticDir: DASHBOARD_STATIC_DIR
216
219
  };
217
220
  }
218
221
 
222
+ function resolveAutoUpdateStatus(context) {
223
+ const manager = context.getUpdateManager ? context.getUpdateManager() : null;
224
+ const config = context.config && typeof context.config.getAutoUpdate === 'function'
225
+ ? context.config.getAutoUpdate()
226
+ : {
227
+ enabled: true,
228
+ intervalMs: 60 * 60 * 1000,
229
+ allowMajor: false,
230
+ lastGoodVersion: null
231
+ };
232
+ const runtime = manager && typeof manager.getStatus === 'function'
233
+ ? manager.getStatus()
234
+ : null;
235
+ return {
236
+ enabled: runtime ? Boolean(runtime.enabled) : Boolean(config.enabled),
237
+ interval_ms: runtime && Number.isFinite(runtime.interval_ms) ? runtime.interval_ms : config.intervalMs,
238
+ allow_major: runtime ? Boolean(runtime.allow_major) : Boolean(config.allowMajor),
239
+ state: runtime ? runtime.state : 'up_to_date',
240
+ current_version: runtime ? runtime.current_version : require('../../package.json').version,
241
+ latest_version: runtime ? runtime.latest_version : null,
242
+ target_version: runtime ? runtime.target_version : null,
243
+ active_calls: runtime ? runtime.active_calls : 0,
244
+ last_checked_at: runtime ? runtime.last_checked_at : null,
245
+ last_success_at: runtime ? runtime.last_success_at : null,
246
+ last_error: runtime ? runtime.last_error : null,
247
+ defer_reason: runtime ? runtime.defer_reason : null,
248
+ last_good_version: config.lastGoodVersion || null
249
+ };
250
+ }
251
+
219
252
  function buildContactIndex(contacts) {
220
253
  const byName = new Map();
221
254
  const byId = new Map();
@@ -537,7 +570,76 @@ function createDashboardApiRouter(options = {}) {
537
570
  callbook: {
538
571
  enabled: Boolean(context.callbookStore && context.callbookStore.isAvailable()),
539
572
  device_count: Array.isArray(devices) ? devices.length : 0
540
- }
573
+ },
574
+ auto_update: resolveAutoUpdateStatus(context)
575
+ });
576
+ });
577
+
578
+ router.get('/update/status', (req, res) => {
579
+ return res.json({
580
+ success: true,
581
+ auto_update: resolveAutoUpdateStatus(context)
582
+ });
583
+ });
584
+
585
+ router.post('/update/check', async (req, res) => {
586
+ const manager = context.getUpdateManager ? context.getUpdateManager() : null;
587
+ if (!manager || typeof manager.triggerCheck !== 'function') {
588
+ return res.status(503).json({
589
+ success: false,
590
+ error: 'updater_unavailable',
591
+ message: 'Auto-updater is not initialized for this server.'
592
+ });
593
+ }
594
+ await manager.triggerCheck({ reason: 'dashboard_manual_check', forceCheck: true });
595
+ return res.json({
596
+ success: true,
597
+ auto_update: resolveAutoUpdateStatus(context)
598
+ });
599
+ });
600
+
601
+ router.post('/update/now', async (req, res) => {
602
+ const manager = context.getUpdateManager ? context.getUpdateManager() : null;
603
+ if (!manager || typeof manager.triggerUpdate !== 'function') {
604
+ return res.status(503).json({
605
+ success: false,
606
+ error: 'updater_unavailable',
607
+ message: 'Auto-updater is not initialized for this server.'
608
+ });
609
+ }
610
+ const force = parseBoolean(req.body && (req.body.force !== undefined ? req.body.force : req.body.force_update));
611
+ await manager.triggerUpdate({
612
+ reason: 'dashboard_manual_update',
613
+ force
614
+ });
615
+ return res.json({
616
+ success: true,
617
+ auto_update: resolveAutoUpdateStatus(context)
618
+ });
619
+ });
620
+
621
+ router.put('/update/config', async (req, res) => {
622
+ const manager = context.getUpdateManager ? context.getUpdateManager() : null;
623
+ const body = req.body || {};
624
+ const enabled = body.enabled !== undefined ? parseBoolean(body.enabled) : undefined;
625
+
626
+ if (enabled === undefined) {
627
+ return res.status(400).json({
628
+ success: false,
629
+ error: 'enabled_required',
630
+ message: 'Pass { enabled: true|false }.'
631
+ });
632
+ }
633
+
634
+ if (manager && typeof manager.setEnabled === 'function') {
635
+ await manager.setEnabled(enabled);
636
+ } else if (context.config && typeof context.config.setAutoUpdate === 'function') {
637
+ context.config.setAutoUpdate({ enabled });
638
+ }
639
+
640
+ return res.json({
641
+ success: true,
642
+ auto_update: resolveAutoUpdateStatus(context)
541
643
  });
542
644
  });
543
645