colana 1.0.0-beta.78 → 1.0.0-beta.79

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "colana",
3
- "version": "1.0.0-beta.78",
3
+ "version": "1.0.0-beta.79",
4
4
  "description": "Agent-First. Multiplied. Multi-agent command center for AI coding agents.",
5
5
  "type": "module",
6
6
  "main": "server/index.js",
package/public/app.js CHANGED
@@ -2130,6 +2130,14 @@ function handleWebSocketMessage(message) {
2130
2130
  }
2131
2131
  renderPersonalStrip();
2132
2132
 
2133
+ // Show update banner if server already detected an available update
2134
+ if (message.updateInfo && message.updateInfo.updateAvailable) {
2135
+ if (!state.updateInfo || state.updateInfo.latestVersion !== message.updateInfo.latestVersion) {
2136
+ state.updateInfo = { ...message.updateInfo };
2137
+ }
2138
+ renderUpdateBanner();
2139
+ }
2140
+
2133
2141
  // Auto-start personal agent if setting enabled.
2134
2142
  // Delay 5s (not 2s) so server-side auto-resume (3s) completes first.
2135
2143
  // Debounce: clear any pending timer so only one auto-start attempt runs,
@@ -9286,6 +9294,8 @@ document.getElementById('btn-close-integration-modal')?.addEventListener('click'
9286
9294
  async function loadServerStatus() {
9287
9295
  const el = $('#server-status-info');
9288
9296
  const restartBtn = $('#btn-server-restart');
9297
+ const serviceToggleBtn = $('#btn-service-toggle');
9298
+ const serviceToggleText = $('#btn-service-toggle-text');
9289
9299
  if (!el) return;
9290
9300
  try {
9291
9301
  const info = await api('/server/status');
@@ -9304,6 +9314,28 @@ async function loadServerStatus() {
9304
9314
  : (info?.restart?.message || 'Restart unavailable in manual mode');
9305
9315
  }
9306
9316
 
9317
+ // Service toggle button
9318
+ if (serviceToggleBtn && serviceToggleText) {
9319
+ serviceToggleText.textContent = serviceInstalled ? 'Disable Auto-Start' : 'Enable Auto-Start';
9320
+ serviceToggleBtn.title = serviceInstalled
9321
+ ? 'Remove background service (stop auto-starting on login)'
9322
+ : 'Install background service (auto-start on login)';
9323
+ serviceToggleBtn.onclick = async () => {
9324
+ const action = serviceInstalled ? 'uninstall' : 'install';
9325
+ serviceToggleBtn.disabled = true;
9326
+ serviceToggleText.textContent = serviceInstalled ? 'Removing...' : 'Installing...';
9327
+ try {
9328
+ const result = await api(`/server/service/${action}`, { method: 'POST' });
9329
+ showToast(result.message || `Service ${action} completed`, result.success ? 'success' : 'error');
9330
+ await loadServerStatus();
9331
+ } catch (err) {
9332
+ showToast(err.message || `Failed to ${action} service`, 'error');
9333
+ serviceToggleBtn.disabled = false;
9334
+ serviceToggleText.textContent = serviceInstalled ? 'Disable Auto-Start' : 'Enable Auto-Start';
9335
+ }
9336
+ };
9337
+ }
9338
+
9307
9339
  const portBits = (info?.port && info?.preferredPort)
9308
9340
  ? ` &middot; Port: <strong>${info.port}</strong>${info.fallbackPort ? ` (preferred <strong>${info.preferredPort}</strong> busy)` : ''}`
9309
9341
  : '';
@@ -9311,9 +9343,11 @@ async function loadServerStatus() {
9311
9343
  const serviceBits = ` &middot; Service: <strong>${serviceInstalled ? (serviceRunning ? 'running' : 'installed, not running') : 'not installed'}</strong>`;
9312
9344
  const restartBits = ` &middot; Restart: <strong>${restartAvailable ? 'available' : 'blocked (manual mode)'}</strong>`;
9313
9345
 
9346
+ // All values come from server-controlled data (uptime, memory, client counts, port numbers) — safe for innerHTML
9314
9347
  el.innerHTML = `Uptime: <strong>${uptimeStr}</strong> &middot; Memory: <strong>${info.memory} MB</strong> &middot; Clients: <strong>${info.connectedClients}</strong> &middot; Active agents: <strong>${info.activeAgents}</strong>${serviceBits}${restartBits}${portBits}`;
9315
9348
  } catch {
9316
9349
  if (restartBtn) restartBtn.disabled = true;
9350
+ if (serviceToggleBtn) serviceToggleBtn.disabled = true;
9317
9351
  el.textContent = 'Could not fetch server status';
9318
9352
  }
9319
9353
  }
package/public/index.html CHANGED
@@ -976,6 +976,12 @@
976
976
  </svg>
977
977
  Restart Server
978
978
  </button>
979
+ <button type="button" class="btn btn-secondary" id="btn-service-toggle" title="Install or remove background service (auto-start on login)">
980
+ <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="vertical-align: -2px;">
981
+ <path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83"/>
982
+ </svg>
983
+ <span id="btn-service-toggle-text">Enable Auto-Start</span>
984
+ </button>
979
985
  </div>
980
986
  <p class="form-hint">Restart clears cached state and reconnects all clients. Available when co:lana runs as an installed background service.</p>
981
987
  </div>
package/server/config.js CHANGED
@@ -233,6 +233,9 @@ const config = {
233
233
  toolResultPreviewLen: envInt(process.env.TOOL_RESULT_PREVIEW_LEN, 500),
234
234
  maxResumableSessions: envInt(process.env.MAX_RESUMABLE_SESSIONS, 5),
235
235
 
236
+ // --- Update Checker ---
237
+ updateCheckIntervalMs: envInt(process.env.UPDATE_CHECK_INTERVAL_MS, 6 * 60 * 60 * 1000), // 6 hours
238
+
236
239
  // --- Log Retention ---
237
240
  logRetentionDays: envInt(process.env.LOG_RETENTION_DAYS, 0),
238
241
  logRetentionIntervalMs: envInt(process.env.LOG_RETENTION_INTERVAL_MS, 24 * 60 * 60 * 1000),
package/server/index.js CHANGED
@@ -58,7 +58,7 @@ import { createWizardRoutes } from './wizard-routes.js';
58
58
  import { checkForUpdate } from './updater.js';
59
59
  import { getSystemInfo } from './system-info.js';
60
60
  import { autoResumeInterruptedSessions } from './auto-resume.js';
61
- import { getServiceStatus } from './service-manager.js';
61
+ import { getServiceStatus, installService, uninstallService } from './service-manager.js';
62
62
  import { registerServerControlRoutes } from './server-control-routes.js';
63
63
  import { registerPersonalAgentRoutes } from './personal-agent-routes.js';
64
64
  import { registerIntegrationRoutes } from './integration-routes.js';
@@ -117,6 +117,7 @@ let runtimePort = null;
117
117
  let preferredPort = config.startPort;
118
118
  let isFallbackPort = false;
119
119
  let runtimeServiceManaged = false;
120
+ let _lastUpdateCheck = null;
120
121
 
121
122
  // --- M8: Per-IP WebSocket connection limit ---
122
123
  const WS_MAX_PER_IP = parseInt(process.env.WS_MAX_CONNECTIONS_PER_IP, 10) || 10;
@@ -217,6 +218,23 @@ if (ptyManager) {
217
218
  ptyManager.setBroadcastCallback(broadcastFn);
218
219
  }
219
220
 
221
+ // --- Update checker (cached + periodic) ---
222
+ let _updateCheckerInterval = null;
223
+ function startUpdateChecker() {
224
+ if (_updateCheckerInterval) return; // already running — guard against duplicate calls from port-retry path
225
+ const runCheck = () => {
226
+ checkForUpdate().then(update => {
227
+ _lastUpdateCheck = update;
228
+ if (update.updateAvailable) {
229
+ console.log(` ${c.info}Update available: v${update.latestVersion}${c.reset}${update.allowed ? '' : ' (subscription expired)'}`);
230
+ broadcastFn({ eventType: 'updateAvailable', ...update });
231
+ }
232
+ }).catch(() => {});
233
+ };
234
+ runCheck();
235
+ _updateCheckerInterval = setInterval(runCheck, config.updateCheckIntervalMs);
236
+ }
237
+
220
238
  // Dashboard WebSocket connection handling
221
239
  wss.on('connection', async (ws) => {
222
240
  console.log('WebSocket client connected');
@@ -272,6 +290,7 @@ wss.on('connection', async (ws) => {
272
290
  },
273
291
  systemInfo,
274
292
  personalAgentStatus,
293
+ updateInfo: _lastUpdateCheck,
275
294
  }));
276
295
  } catch (e) { /* client disconnected before init could be sent */ }
277
296
 
@@ -623,6 +642,9 @@ registerServerControlRoutes(app, {
623
642
  fallbackPort: isFallbackPort,
624
643
  serviceManagedRuntime: runtimeServiceManaged,
625
644
  }),
645
+ sensitiveLimiter,
646
+ installServiceFn: installService,
647
+ uninstallServiceFn: uninstallService,
626
648
  });
627
649
 
628
650
  // ============================================
@@ -640,11 +662,12 @@ app.use((err, _req, res, _next) => {
640
662
  function shutdown(signal) {
641
663
  console.log(`\n${signal} received. Shutting down gracefully...`);
642
664
 
643
- // Stop heartbeat intervals
665
+ // Stop heartbeat and background intervals
644
666
  clearInterval(wsHeartbeatInterval);
645
667
  clearInterval(terminalHeartbeatInterval);
646
668
  if (logRetentionInterval) clearInterval(logRetentionInterval);
647
669
  if (logRetentionTimeout) clearTimeout(logRetentionTimeout);
670
+ if (_updateCheckerInterval) clearInterval(_updateCheckerInterval);
648
671
 
649
672
  // Track app_stopped and stop analytics scheduler
650
673
  try { trackEvent('app_stopped', { uptimeSeconds: Math.round(process.uptime()) }); } catch { /* best-effort */ }
@@ -1050,12 +1073,7 @@ async function startServer() {
1050
1073
  // Background tasks
1051
1074
  scheduleLogRetention();
1052
1075
  backgroundRevalidate().catch(() => {});
1053
- checkForUpdate().then(update => {
1054
- if (update.updateAvailable) {
1055
- console.log(` ${c.info}Update available: v${update.latestVersion}${c.reset}${update.allowed ? '' : ' (subscription expired)'}`);
1056
- broadcastFn({ eventType: 'updateAvailable', ...update });
1057
- }
1058
- }).catch(() => {});
1076
+ startUpdateChecker();
1059
1077
 
1060
1078
  // Auto-start OpenClaw gateway (same as primary listen block)
1061
1079
  if (ptyManager) {
@@ -1132,14 +1150,8 @@ async function startServer() {
1132
1150
  trackEvent('license_tier', { tier: startupLicense.tier || 'free' });
1133
1151
  } catch { /* best-effort */ }
1134
1152
 
1135
- // Check for updates (non-blocking)
1136
- checkForUpdate().then(update => {
1137
- if (update.updateAvailable) {
1138
- console.log(` ${c.info}Update available: v${update.latestVersion}${c.reset}${update.allowed ? '' : ' (subscription expired)'}`);
1139
- // Broadcast to connected clients
1140
- broadcastFn({ eventType: 'updateAvailable', ...update });
1141
- }
1142
- }).catch(() => {});
1153
+ // Check for updates (non-blocking, cached + periodic)
1154
+ startUpdateChecker();
1143
1155
 
1144
1156
  // Auto-start OpenClaw gateway so the Personal Agent is immediately available
1145
1157
  if (ptyManager) {
@@ -1,15 +1,19 @@
1
1
  import { getServiceStatus } from './service-manager.js';
2
2
  import { evaluateRestartMode } from './runtime-policy.js';
3
+ import logger from './logger.js';
3
4
 
4
5
  /**
5
- * Register server control endpoints (restart + status).
6
+ * Register server control endpoints (restart + status + service management).
6
7
  *
7
8
  * @param {import('express').Express} app
8
9
  * @param {{
9
10
  * onRestart: (signal: string) => void,
10
11
  * getStatusSnapshot: () => Record<string, unknown>,
11
12
  * getServiceStatusFn?: () => any,
12
- * allowManualRestart?: boolean
13
+ * allowManualRestart?: boolean,
14
+ * sensitiveLimiter?: import('express').RequestHandler,
15
+ * installServiceFn?: () => { success: boolean, message: string },
16
+ * uninstallServiceFn?: () => { success: boolean, message: string },
13
17
  * }} deps
14
18
  */
15
19
  export function registerServerControlRoutes(app, deps) {
@@ -18,6 +22,9 @@ export function registerServerControlRoutes(app, deps) {
18
22
  getStatusSnapshot,
19
23
  getServiceStatusFn = getServiceStatus,
20
24
  allowManualRestart = process.env.COLANA_ALLOW_MANUAL_RESTART === 'true',
25
+ sensitiveLimiter,
26
+ installServiceFn,
27
+ uninstallServiceFn,
21
28
  } = deps;
22
29
 
23
30
  if (typeof onRestart !== 'function') {
@@ -27,6 +34,8 @@ export function registerServerControlRoutes(app, deps) {
27
34
  throw new Error('registerServerControlRoutes: getStatusSnapshot must be a function');
28
35
  }
29
36
 
37
+ const limiterMiddleware = sensitiveLimiter || ((req, res, next) => next());
38
+
30
39
  app.post('/api/server/restart', (req, res) => {
31
40
  let service = null;
32
41
  try {
@@ -84,9 +93,38 @@ export function registerServerControlRoutes(app, deps) {
84
93
  },
85
94
  });
86
95
  });
96
+
97
+ // --- Service install/uninstall (OS-level auto-start) ---
98
+
99
+ app.post('/api/server/service/install', limiterMiddleware, (req, res) => {
100
+ if (typeof installServiceFn !== 'function') {
101
+ return res.status(501).json({ success: false, error: 'Service management not available' });
102
+ }
103
+ try {
104
+ const result = installServiceFn();
105
+ logger.info('server', 'Service install requested', { success: result.success });
106
+ res.json(result);
107
+ } catch (err) {
108
+ logger.error('server', 'Service install failed', { error: err.message });
109
+ res.status(500).json({ success: false, error: err.message || 'Service install failed' });
110
+ }
111
+ });
112
+
113
+ app.post('/api/server/service/uninstall', limiterMiddleware, (req, res) => {
114
+ if (typeof uninstallServiceFn !== 'function') {
115
+ return res.status(501).json({ success: false, error: 'Service management not available' });
116
+ }
117
+ try {
118
+ const result = uninstallServiceFn();
119
+ logger.info('server', 'Service uninstall requested', { success: result.success });
120
+ res.json(result);
121
+ } catch (err) {
122
+ logger.error('server', 'Service uninstall failed', { error: err.message });
123
+ res.status(500).json({ success: false, error: err.message || 'Service uninstall failed' });
124
+ }
125
+ });
87
126
  }
88
127
 
89
128
  export default {
90
129
  registerServerControlRoutes,
91
130
  };
92
-