neoagent 2.1.18-beta.79 → 2.1.18-beta.80

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.
Files changed (64) hide show
  1. package/extensions/chrome-browser/background.mjs +40 -5
  2. package/extensions/chrome-browser/popup.css +27 -2
  3. package/extensions/chrome-browser/popup.js +39 -0
  4. package/extensions/chrome-browser/protocol.mjs +3 -3
  5. package/lib/install_helpers.js +1 -1
  6. package/lib/manager.js +26 -44
  7. package/package.json +1 -1
  8. package/runtime/env.js +17 -0
  9. package/runtime/git_helpers.js +41 -0
  10. package/runtime/paths.js +37 -4
  11. package/runtime/release_channel.js +1 -12
  12. package/server/config/origins.js +4 -1
  13. package/server/db/database.js +45 -2
  14. package/server/guest_agent.js +52 -16
  15. package/server/http/errors.js +7 -2
  16. package/server/http/middleware.js +26 -20
  17. package/server/http/routes.js +1 -0
  18. package/server/http/socket.js +7 -1
  19. package/server/index.js +4 -1
  20. package/server/middleware/auth.js +31 -9
  21. package/server/public/assets/NOTICES +143 -0
  22. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  23. package/server/public/flutter_bootstrap.js +1 -1
  24. package/server/public/main.dart.js +70615 -69343
  25. package/server/routes/_helpers/readChunkBody.js +2 -1
  26. package/server/routes/auth.js +33 -19
  27. package/server/routes/browser.js +8 -0
  28. package/server/routes/desktop.js +240 -0
  29. package/server/routes/integrations.js +12 -0
  30. package/server/routes/mcp.js +24 -4
  31. package/server/routes/scheduler.js +22 -3
  32. package/server/routes/voice_assistant.js +37 -0
  33. package/server/services/account/service_email.js +32 -0
  34. package/server/services/account/session_secret.js +7 -0
  35. package/server/services/ai/engine.js +20 -2
  36. package/server/services/ai/imageAnalysis.js +104 -0
  37. package/server/services/ai/outputSanitizer.js +19 -2
  38. package/server/services/ai/toolResult.js +16 -12
  39. package/server/services/ai/tools.js +224 -62
  40. package/server/services/browser/extension/gateway.js +48 -4
  41. package/server/services/browser/extension/protocol.js +3 -0
  42. package/server/services/browser/extension/registry.js +1 -1
  43. package/server/services/desktop/auth.js +70 -0
  44. package/server/services/desktop/gateway.js +215 -0
  45. package/server/services/desktop/protocol.js +64 -0
  46. package/server/services/desktop/provider.js +176 -0
  47. package/server/services/desktop/registry.js +521 -0
  48. package/server/services/integrations/google/provider.js +10 -2
  49. package/server/services/integrations/manager.js +12 -2
  50. package/server/services/integrations/microsoft/provider.js +3 -0
  51. package/server/services/integrations/oauth_provider.js +38 -7
  52. package/server/services/integrations/slack/provider.js +8 -1
  53. package/server/services/integrations/whatsapp/provider.js +73 -5
  54. package/server/services/manager.js +36 -0
  55. package/server/services/memory/embeddings.js +31 -11
  56. package/server/services/messaging/automation.js +11 -1
  57. package/server/services/messaging/http_platforms.js +8 -1
  58. package/server/services/voice/message.js +24 -0
  59. package/server/services/voice/screenshotContext.js +73 -0
  60. package/server/services/voice/turnRunner.js +25 -2
  61. package/server/services/websocket.js +496 -72
  62. package/server/utils/logger.js +2 -0
  63. package/server/utils/security.js +12 -8
  64. package/server/utils/update_status.js +7 -1
@@ -6,6 +6,9 @@ const protocol = createBrowserProtocol(chrome);
6
6
  let socket = null;
7
7
  let reconnectTimer = null;
8
8
  let suppressSocketClose = false;
9
+ const DEFAULT_FETCH_TIMEOUT_MS = 10000;
10
+ const DEFAULT_WS_CONNECT_TIMEOUT_MS = 10000;
11
+ const EXTENSION_PROTOCOL_VERSION = 1;
9
12
 
10
13
  function getStorage(keys = STORAGE_KEYS) {
11
14
  return chrome.storage.local.get(keys);
@@ -41,6 +44,19 @@ function websocketUrl(serverUrl, token) {
41
44
  return url.toString();
42
45
  }
43
46
 
47
+ async function fetchWithTimeout(url, options = {}, timeoutMs = DEFAULT_FETCH_TIMEOUT_MS) {
48
+ const controller = new AbortController();
49
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
50
+ try {
51
+ return await fetch(url, {
52
+ ...options,
53
+ signal: controller.signal,
54
+ });
55
+ } finally {
56
+ clearTimeout(timer);
57
+ }
58
+ }
59
+
44
60
  function compareVersions(a, b) {
45
61
  const left = String(a || '0').split('.').map((part) => Number(part) || 0);
46
62
  const right = String(b || '0').split('.').map((part) => Number(part) || 0);
@@ -100,17 +116,25 @@ async function connect() {
100
116
  const ws = new WebSocket(websocketUrl(serverUrl, token));
101
117
  socket = ws;
102
118
  await updateStatus('connecting');
119
+ const connectTimeout = setTimeout(() => {
120
+ if (socket === ws && ws.readyState !== WebSocket.OPEN) {
121
+ try { ws.close(); } catch {}
122
+ }
123
+ }, DEFAULT_WS_CONNECT_TIMEOUT_MS);
103
124
 
104
125
  ws.addEventListener('open', () => {
105
126
  if (socket !== ws) return;
127
+ clearTimeout(connectTimeout);
106
128
  updateStatus('connected');
107
129
  });
108
130
  ws.addEventListener('close', () => {
131
+ clearTimeout(connectTimeout);
109
132
  handleSocketDisconnected(ws).catch((error) => {
110
133
  console.error('NeoAgent disconnect handling failed', error);
111
134
  });
112
135
  });
113
136
  ws.addEventListener('error', () => {
137
+ clearTimeout(connectTimeout);
114
138
  handleSocketDisconnected(ws).catch((error) => {
115
139
  console.error('NeoAgent socket error handling failed', error);
116
140
  });
@@ -134,12 +158,23 @@ async function handleSocketMessage(raw) {
134
158
  if (!message || message.type !== 'command' || !message.id) {
135
159
  return;
136
160
  }
161
+ if (message.version != null && Number(message.version) !== EXTENSION_PROTOCOL_VERSION) {
162
+ socket?.send(JSON.stringify({
163
+ type: 'result',
164
+ version: EXTENSION_PROTOCOL_VERSION,
165
+ id: message.id,
166
+ ok: false,
167
+ error: `Unsupported protocol version: ${message.version}`,
168
+ }));
169
+ return;
170
+ }
137
171
  try {
138
172
  const result = await protocol.run(message.command, message.payload || {});
139
- socket?.send(JSON.stringify({ type: 'result', id: message.id, ok: true, result }));
173
+ socket?.send(JSON.stringify({ type: 'result', version: EXTENSION_PROTOCOL_VERSION, id: message.id, ok: true, result }));
140
174
  } catch (error) {
141
175
  socket?.send(JSON.stringify({
142
176
  type: 'result',
177
+ version: EXTENSION_PROTOCOL_VERSION,
143
178
  id: message.id,
144
179
  ok: false,
145
180
  error: error?.message || String(error),
@@ -150,7 +185,7 @@ async function handleSocketMessage(raw) {
150
185
  async function startPairing(serverUrl) {
151
186
  const normalized = await resolveServerUrl(serverUrl);
152
187
  if (!normalized) throw new Error('NeoAgent server URL required.');
153
- const response = await fetch(`${normalized}/api/browser-extension/pairing/request`, {
188
+ const response = await fetchWithTimeout(`${normalized}/api/browser-extension/pairing/request`, {
154
189
  method: 'POST',
155
190
  headers: { 'content-type': 'application/json' },
156
191
  body: JSON.stringify({ extensionName: 'NeoAgent Browser' }),
@@ -173,7 +208,7 @@ async function claimPairing() {
173
208
  if (!serverUrl || !pairingId || !pairingSecret) {
174
209
  throw new Error('No pending pairing request.');
175
210
  }
176
- const response = await fetch(`${serverUrl}/api/browser-extension/pairing/${encodeURIComponent(pairingId)}/claim`, {
211
+ const response = await fetchWithTimeout(`${serverUrl}/api/browser-extension/pairing/${encodeURIComponent(pairingId)}/claim`, {
177
212
  method: 'POST',
178
213
  headers: { 'content-type': 'application/json' },
179
214
  body: JSON.stringify({ pairingSecret, extensionName: 'NeoAgent Browser' }),
@@ -204,7 +239,7 @@ async function disconnect() {
204
239
  async function checkForUpdates(preferredServerUrl) {
205
240
  const serverUrl = await resolveServerUrl(preferredServerUrl);
206
241
  if (!serverUrl) throw new Error('NeoAgent server URL required.');
207
- const response = await fetch(`${serverUrl}/api/browser-extension/latest`);
242
+ const response = await fetchWithTimeout(`${serverUrl}/api/browser-extension/latest`);
208
243
  const latest = await response.json().catch(() => ({}));
209
244
  if (!response.ok) throw new Error(latest.error || `Update check failed: ${response.status}`);
210
245
  const currentVersion = chrome.runtime.getManifest().version;
@@ -249,7 +284,7 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
249
284
  };
250
285
  run()
251
286
  .then((result) => sendResponse({ ok: true, result }))
252
- .catch((error) => sendResponse({ ok: false, error: error.message || String(error) }));
287
+ .catch((error) => sendResponse({ ok: false, error: error?.message || String(error) }));
253
288
  return true;
254
289
  });
255
290
 
@@ -19,12 +19,22 @@
19
19
 
20
20
  body {
21
21
  margin: 0;
22
- width: 380px;
22
+ width: min(420px, 100vw);
23
+ min-width: 320px;
24
+ max-width: 420px;
23
25
  background: var(--bg);
24
26
  color: var(--text);
25
27
  font: 14px/1.45 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
26
28
  }
27
29
 
30
+ body[data-busy="true"] {
31
+ cursor: progress;
32
+ }
33
+
34
+ body[data-busy="true"] .status-dot {
35
+ animation: pulse 1s ease-in-out infinite;
36
+ }
37
+
28
38
  main {
29
39
  display: grid;
30
40
  gap: 12px;
@@ -215,7 +225,7 @@ button.danger:hover {
215
225
 
216
226
  button:disabled {
217
227
  cursor: not-allowed;
218
- opacity: 0.62;
228
+ opacity: 0.88;
219
229
  }
220
230
 
221
231
  .link-button {
@@ -261,3 +271,18 @@ button:disabled {
261
271
  .message[data-tone="success"] {
262
272
  color: #86efac;
263
273
  }
274
+
275
+ @keyframes pulse {
276
+ 0% {
277
+ transform: scale(1);
278
+ filter: saturate(1);
279
+ }
280
+ 50% {
281
+ transform: scale(1.08);
282
+ filter: saturate(1.2);
283
+ }
284
+ 100% {
285
+ transform: scale(1);
286
+ filter: saturate(1);
287
+ }
288
+ }
@@ -24,6 +24,7 @@ const STATUS_LABELS = {
24
24
  };
25
25
 
26
26
  let currentState = {};
27
+ let pendingActions = 0;
27
28
 
28
29
  function send(type, payload = {}) {
29
30
  return chrome.runtime.sendMessage({ type, ...payload }).then((response) => {
@@ -50,6 +51,40 @@ function setMessage(text, tone = '') {
50
51
  }
51
52
  }
52
53
 
54
+ function setBusy(isBusy, label = 'Working...') {
55
+ if (isBusy) {
56
+ pendingActions += 1;
57
+ } else {
58
+ pendingActions = Math.max(0, pendingActions - 1);
59
+ }
60
+ const busy = pendingActions > 0;
61
+
62
+ [primaryActionEl, secondaryActionEl, openAppEl, disconnectEl, checkUpdateEl, downloadEl].forEach((button) => {
63
+ if (!button || button.hidden) return;
64
+ if (busy) {
65
+ if (!Object.prototype.hasOwnProperty.call(button.dataset, 'wasDisabled')) {
66
+ button.dataset.wasDisabled = button.disabled ? 'true' : 'false';
67
+ }
68
+ button.disabled = true;
69
+ } else if (button.dataset.wasDisabled) {
70
+ button.disabled = button.dataset.wasDisabled === 'true';
71
+ delete button.dataset.wasDisabled;
72
+ }
73
+ });
74
+
75
+ if (busy) {
76
+ document.body.dataset.busy = 'true';
77
+ if (!messageEl.textContent) {
78
+ setMessage(label, 'success');
79
+ }
80
+ } else {
81
+ delete document.body.dataset.busy;
82
+ if (messageEl.dataset.tone === 'success' && messageEl.textContent === label) {
83
+ setMessage('');
84
+ }
85
+ }
86
+ }
87
+
53
88
  function setAction(button, { label, action, hidden = false, disabled = false }) {
54
89
  button.textContent = label;
55
90
  button.dataset.action = action;
@@ -193,9 +228,13 @@ function bindAsyncClick(element, handler) {
193
228
  element.addEventListener('click', async () => {
194
229
  try {
195
230
  setMessage('');
231
+ setBusy(true);
196
232
  await handler();
197
233
  } catch (error) {
198
234
  setMessage(error.message, 'error');
235
+ } finally {
236
+ setBusy(false);
237
+ updateFlow();
199
238
  }
200
239
  });
201
240
  }
@@ -37,9 +37,9 @@ function jsString(value) {
37
37
 
38
38
  function buildIsolatedEvaluationExpression(script) {
39
39
  const source = String(script ?? 'undefined');
40
- // Match the host browser controller: keep each arbitrary snippet inside its
41
- // own scope so repeated browser_evaluate calls cannot collide on const/let.
42
- return `(() => eval(${JSON.stringify(source)}))()`;
40
+ // Keep each snippet inside its own function scope so repeated browser_evaluate
41
+ // calls cannot collide on const/let declarations.
42
+ return `(() => {\nreturn (${source});\n})()`;
43
43
  }
44
44
 
45
45
  function keyCodeFor(key) {
@@ -68,7 +68,7 @@ function buildBundledWebClientIfPossible({
68
68
  ],
69
69
  {
70
70
  cwd: flutterAppDir,
71
- env: process.env,
71
+ env: withInstallEnv(),
72
72
  },
73
73
  );
74
74
 
package/lib/manager.js CHANGED
@@ -31,6 +31,8 @@ const {
31
31
  choosePreferredBranchForChannel,
32
32
  choosePreferredNpmTagForChannel,
33
33
  } = require('../runtime/release_channel');
34
+ const { parseEnv } = require('../runtime/env');
35
+ const { createGitHelpers } = require('../runtime/git_helpers');
34
36
  const { parseDeploymentMode } = require('../server/utils/deployment');
35
37
 
36
38
  const APP_NAME = 'NeoAgent';
@@ -109,19 +111,6 @@ function readEnvFileRaw() {
109
111
  return fs.readFileSync(ENV_FILE, 'utf8');
110
112
  }
111
113
 
112
- function parseEnv(raw) {
113
- const lines = raw.split('\n');
114
- const map = new Map();
115
- for (const line of lines) {
116
- if (!line || line.startsWith('#') || !line.includes('=')) continue;
117
- const idx = line.indexOf('=');
118
- const key = line.slice(0, idx).trim();
119
- const value = line.slice(idx + 1);
120
- if (key) map.set(key, value);
121
- }
122
- return map;
123
- }
124
-
125
114
  function upsertEnvValue(key, value) {
126
115
  const raw = readEnvFileRaw();
127
116
  const lines = raw ? raw.split('\n') : [];
@@ -167,6 +156,13 @@ function runQuiet(cmd, args, options = {}) {
167
156
  return spawnSync(cmd, args, { stdio: ['ignore', 'pipe', 'pipe'], encoding: 'utf8', cwd: APP_DIR, ...options });
168
157
  }
169
158
 
159
+ const {
160
+ latestGitTagVersion,
161
+ gitWorkingTreeDirty,
162
+ gitLocalBranchExists,
163
+ gitRemoteBranchExists,
164
+ } = createGitHelpers((cmd, args) => runQuiet(cmd, args));
165
+
170
166
  function readInstalledPackageVersion() {
171
167
  try {
172
168
  const pkg = JSON.parse(fs.readFileSync(PACKAGE_JSON_PATH, 'utf8'));
@@ -203,29 +199,6 @@ function releaseChannelSummary(channel) {
203
199
  return describeReleaseChannelPolicy(parseReleaseChannel(channel) || currentReleaseChannel());
204
200
  }
205
201
 
206
- function gitWorkingTreeDirty() {
207
- const res = runQuiet('git', ['status', '--porcelain']);
208
- return res.status === 0 && Boolean(res.stdout.trim());
209
- }
210
-
211
- function gitLocalBranchExists(branch) {
212
- return runQuiet('git', ['show-ref', '--verify', '--quiet', `refs/heads/${branch}`]).status === 0;
213
- }
214
-
215
- function gitRemoteBranchExists(branch) {
216
- return runQuiet('git', ['ls-remote', '--exit-code', '--heads', 'origin', branch]).status === 0;
217
- }
218
-
219
- function latestGitTagVersion(pattern) {
220
- const res = runQuiet('git', ['tag', '--list', pattern, '--sort=-v:refname']);
221
- if (res.status !== 0) return null;
222
- const tag = res.stdout
223
- .split('\n')
224
- .map((value) => value.trim())
225
- .find(Boolean);
226
- return tag ? tag.replace(/^v/, '') : null;
227
- }
228
-
229
202
  function resolvePreferredGitBranch(channel) {
230
203
  const normalized = parseReleaseChannel(channel) || currentReleaseChannel();
231
204
  if (normalized === 'stable') {
@@ -310,7 +283,7 @@ function ensureLogDir() {
310
283
  function backupRuntimeData() {
311
284
  const backupsDir = path.join(RUNTIME_HOME, 'backups');
312
285
  fs.mkdirSync(backupsDir, { recursive: true });
313
- const stamp = new Date().toISOString().replace(/[:.]/g, '-');
286
+ const stamp = new Date().toISOString().replace(/:/g, '-').replace(/\.\d{3}Z$/, 'Z');
314
287
  const target = path.join(backupsDir, `pre-update-${stamp}`);
315
288
  fs.mkdirSync(target, { recursive: true });
316
289
 
@@ -320,7 +293,11 @@ function backupRuntimeData() {
320
293
 
321
294
  function killByPort(port) {
322
295
  if (!commandExists('lsof')) return false;
323
- const res = runQuiet('bash', ['-lc', `lsof -ti tcp:${port}`]);
296
+ const normalizedPort = Number(port);
297
+ if (!Number.isInteger(normalizedPort) || normalizedPort <= 0 || normalizedPort > 65535) {
298
+ return false;
299
+ }
300
+ const res = runQuiet('lsof', ['-ti', `tcp:${normalizedPort}`]);
324
301
  if (res.status !== 0 || !res.stdout.trim()) return false;
325
302
  const pids = res.stdout
326
303
  .trim()
@@ -343,6 +320,12 @@ function listNeoAgentServerProcesses() {
343
320
  const res = runQuiet('ps', ['-axo', 'pid=,ppid=,command=']);
344
321
  if (res.status !== 0) return [];
345
322
 
323
+ const normalizedAppIndexPath = path.join(APP_DIR, 'server', 'index.js').replace(/\\/g, '/');
324
+ const escapedAppIndexPath = normalizedAppIndexPath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
325
+ const appIndexPattern = new RegExp(`(^|\\s|["'])${escapedAppIndexPath}(?=$|\\s|["'])`);
326
+ const genericNeoAgentPattern = /(^|[\s"'])[^\s"']*\/neoagent\/server\/index\.js(?=$|[\s"'])/i;
327
+ const repoNamePattern = new RegExp(`(^|[\\s"'])[^\\s"']*${path.sep === '\\' ? '\\\\' : '/'}NeoAgent${path.sep === '\\' ? '\\\\' : '/'}server${path.sep === '\\' ? '\\\\' : '/'}index\\.js(?=$|[\\s"'])`, 'i');
328
+
346
329
  return res.stdout
347
330
  .split('\n')
348
331
  .map((line) => line.trim())
@@ -361,9 +344,9 @@ function listNeoAgentServerProcesses() {
361
344
  entry.pid !== process.pid &&
362
345
  /(^|\s)node(\s|$)/.test(entry.command) &&
363
346
  (
364
- entry.command.includes('/neoagent/server/index.js') ||
365
- entry.command.includes(`${path.sep}NeoAgent${path.sep}server${path.sep}index.js`) ||
366
- entry.command.includes(`${APP_DIR}${path.sep}server${path.sep}index.js`)
347
+ appIndexPattern.test(String(entry.command || '').replace(/\\/g, '/')) ||
348
+ genericNeoAgentPattern.test(String(entry.command || '').replace(/\\/g, '/')) ||
349
+ repoNamePattern.test(String(entry.command || ''))
367
350
  )
368
351
  );
369
352
  }
@@ -675,6 +658,7 @@ function installLinuxService() {
675
658
  runOrThrow('systemctl', ['--user', 'daemon-reload']);
676
659
  runOrThrow('systemctl', ['--user', 'enable', 'neoagent']);
677
660
  runOrThrow('systemctl', ['--user', 'start', 'neoagent']);
661
+ runOrThrow('systemctl', ['--user', 'is-active', '--quiet', 'neoagent']);
678
662
  logOk('systemd user service installed and started');
679
663
  }
680
664
 
@@ -729,6 +713,7 @@ function cmdStart() {
729
713
 
730
714
  if (platform === 'linux' && fs.existsSync(SYSTEMD_UNIT)) {
731
715
  runOrThrow('systemctl', ['--user', 'start', 'neoagent']);
716
+ runOrThrow('systemctl', ['--user', 'is-active', '--quiet', 'neoagent']);
732
717
  logOk('systemd start requested');
733
718
  return;
734
719
  }
@@ -781,9 +766,6 @@ function cmdStop() {
781
766
  if (killed) {
782
767
  logOk(`Stopped ${processes.length} extra NeoAgent process${processes.length === 1 ? '' : 'es'}`);
783
768
  }
784
- if (killByPort(port)) {
785
- logOk(`Stopped process listening on port ${port}`);
786
- }
787
769
  }
788
770
 
789
771
  function cmdRestart() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neoagent",
3
- "version": "2.1.18-beta.79",
3
+ "version": "2.1.18-beta.80",
4
4
  "description": "Proactive personal AI agent with no limits",
5
5
  "license": "MIT",
6
6
  "main": "server/index.js",
package/runtime/env.js ADDED
@@ -0,0 +1,17 @@
1
+ 'use strict';
2
+
3
+ function parseEnv(raw) {
4
+ const map = new Map();
5
+ for (const line of String(raw ?? '').split(/\r?\n/)) {
6
+ if (!line || line.startsWith('#') || !line.includes('=')) continue;
7
+ const idx = line.indexOf('=');
8
+ const key = line.slice(0, idx).trim();
9
+ const value = line.slice(idx + 1);
10
+ if (key) map.set(key, value);
11
+ }
12
+ return map;
13
+ }
14
+
15
+ module.exports = {
16
+ parseEnv,
17
+ };
@@ -0,0 +1,41 @@
1
+ 'use strict';
2
+
3
+ function createGitHelpers(run) {
4
+ if (typeof run !== 'function') {
5
+ throw new TypeError('createGitHelpers(run) requires a run function');
6
+ }
7
+
8
+ function latestGitTagVersion(pattern) {
9
+ const res = run('git', ['tag', '--list', pattern, '--sort=-v:refname']);
10
+ if (res.status !== 0) return null;
11
+ const tag = String(res.stdout || '')
12
+ .split('\n')
13
+ .map((value) => value.trim())
14
+ .find(Boolean);
15
+ return tag ? tag.replace(/^v/, '') : null;
16
+ }
17
+
18
+ function gitWorkingTreeDirty() {
19
+ const res = run('git', ['status', '--porcelain']);
20
+ return res.status === 0 && Boolean(String(res.stdout || '').trim());
21
+ }
22
+
23
+ function gitLocalBranchExists(branch) {
24
+ return run('git', ['show-ref', '--verify', '--quiet', `refs/heads/${branch}`]).status === 0;
25
+ }
26
+
27
+ function gitRemoteBranchExists(branch) {
28
+ return run('git', ['ls-remote', '--exit-code', '--heads', 'origin', branch]).status === 0;
29
+ }
30
+
31
+ return {
32
+ latestGitTagVersion,
33
+ gitWorkingTreeDirty,
34
+ gitLocalBranchExists,
35
+ gitRemoteBranchExists,
36
+ };
37
+ }
38
+
39
+ module.exports = {
40
+ createGitHelpers,
41
+ };
package/runtime/paths.js CHANGED
@@ -45,17 +45,50 @@ function migrateLegacyRuntime(logger = () => {}) {
45
45
  ensureRuntimeDirs();
46
46
  let changed = false;
47
47
 
48
+ const log = (message) => {
49
+ if (typeof logger === 'function') {
50
+ logger(message);
51
+ return;
52
+ }
53
+ if (logger && typeof logger.info === 'function') {
54
+ logger.info(message);
55
+ }
56
+ };
57
+
58
+ const logError = (message) => {
59
+ if (logger && typeof logger.error === 'function') {
60
+ logger.error(message);
61
+ return;
62
+ }
63
+ if (typeof logger === 'function') {
64
+ logger(`error: ${message}`);
65
+ return;
66
+ }
67
+ try { console.error(message); } catch {}
68
+ };
69
+
48
70
  if (copyFileIfMissing(LEGACY_ENV_FILE, ENV_FILE)) {
49
- try { fs.chmodSync(ENV_FILE, 0o600); } catch {}
50
- logger(`migrated ${LEGACY_ENV_FILE} -> ${ENV_FILE}`);
71
+ try {
72
+ fs.chmodSync(ENV_FILE, 0o600);
73
+ } catch (error) {
74
+ try {
75
+ fs.rmSync(ENV_FILE, { force: true });
76
+ } catch {}
77
+ logError(
78
+ `failed to migrate ${LEGACY_ENV_FILE} -> ${ENV_FILE}: chmod(0600) failed (${error.message}). ` +
79
+ `Migration was reverted. Note: chmod behavior can differ on Windows filesystems.`
80
+ );
81
+ return changed;
82
+ }
83
+ log(`migrated ${LEGACY_ENV_FILE} -> ${ENV_FILE}`);
51
84
  changed = true;
52
85
  }
53
86
  if (copyDirMerge(LEGACY_DATA_DIR, DATA_DIR)) {
54
- logger(`migrated ${LEGACY_DATA_DIR} -> ${DATA_DIR}`);
87
+ log(`migrated ${LEGACY_DATA_DIR} -> ${DATA_DIR}`);
55
88
  changed = true;
56
89
  }
57
90
  if (copyDirMerge(LEGACY_AGENT_DATA_DIR, AGENT_DATA_DIR)) {
58
- logger(`migrated ${LEGACY_AGENT_DATA_DIR} -> ${AGENT_DATA_DIR}`);
91
+ log(`migrated ${LEGACY_AGENT_DATA_DIR} -> ${AGENT_DATA_DIR}`);
59
92
  changed = true;
60
93
  }
61
94
 
@@ -3,6 +3,7 @@
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
5
  const { ENV_FILE } = require('./paths');
6
+ const { parseEnv } = require('./env');
6
7
 
7
8
  const DEFAULT_RELEASE_CHANNEL = 'stable';
8
9
  const RELEASE_CHANNEL_ENV_KEY = 'NEOAGENT_RELEASE_CHANNEL';
@@ -23,18 +24,6 @@ const RELEASE_CHANNEL_NPM_POLICIES = Object.freeze({
23
24
  beta: 'newest of beta or latest',
24
25
  });
25
26
 
26
- function parseEnv(raw) {
27
- const map = new Map();
28
- for (const line of String(raw || '').split('\n')) {
29
- if (!line || line.startsWith('#') || !line.includes('=')) continue;
30
- const idx = line.indexOf('=');
31
- const key = line.slice(0, idx).trim();
32
- const value = line.slice(idx + 1);
33
- if (key) map.set(key, value);
34
- }
35
- return map;
36
- }
37
-
38
27
  function parseReleaseChannel(value) {
39
28
  const normalized = String(value || '').trim().toLowerCase();
40
29
  switch (normalized) {
@@ -23,7 +23,10 @@ function isChromeExtensionOrigin(origin) {
23
23
  }
24
24
 
25
25
  function isAllowedOrigin(origin, options = {}) {
26
- if (!origin) return true;
26
+ if (origin == null || origin === '') {
27
+ return options.allowMissingOrigin === true;
28
+ }
29
+ if (origin === 'null') return false;
27
30
  if (configuredOrigins.includes(origin)) return true;
28
31
  if (isLoopbackOrigin(origin)) return true;
29
32
  if (options.allowChromeExtension && isChromeExtensionOrigin(origin)) return true;
@@ -279,6 +279,34 @@ db.exec(`
279
279
  FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
280
280
  );
281
281
 
282
+ CREATE TABLE IF NOT EXISTS desktop_companion_devices (
283
+ id TEXT PRIMARY KEY,
284
+ user_id INTEGER NOT NULL,
285
+ device_id TEXT NOT NULL,
286
+ activation_id TEXT,
287
+ label TEXT NOT NULL,
288
+ hostname TEXT,
289
+ platform TEXT,
290
+ platform_version TEXT,
291
+ app_version TEXT,
292
+ companion_enabled INTEGER DEFAULT 0,
293
+ paused INTEGER DEFAULT 0,
294
+ status TEXT DEFAULT 'offline',
295
+ display_count INTEGER DEFAULT 0,
296
+ active_display_id TEXT,
297
+ permissions_json TEXT DEFAULT '{}',
298
+ capabilities_json TEXT DEFAULT '{}',
299
+ metadata_json TEXT DEFAULT '{}',
300
+ session_id INTEGER,
301
+ last_connected_at TEXT,
302
+ last_seen_at TEXT,
303
+ revoked_at TEXT,
304
+ created_at TEXT DEFAULT (datetime('now')),
305
+ updated_at TEXT DEFAULT (datetime('now')),
306
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
307
+ UNIQUE(user_id, device_id)
308
+ );
309
+
282
310
  CREATE TABLE IF NOT EXISTS scheduled_tasks (
283
311
  id INTEGER PRIMARY KEY AUTOINCREMENT,
284
312
  user_id INTEGER NOT NULL,
@@ -360,6 +388,8 @@ db.exec(`
360
388
  CREATE INDEX IF NOT EXISTS idx_integration_oauth_states_expires ON integration_oauth_states(expires_at);
361
389
  CREATE INDEX IF NOT EXISTS idx_browser_extension_pairing_status ON browser_extension_pairing_requests(status, expires_at);
362
390
  CREATE INDEX IF NOT EXISTS idx_browser_extension_tokens_user ON browser_extension_tokens(user_id, status, created_at DESC);
391
+ CREATE INDEX IF NOT EXISTS idx_browser_extension_tokens_hash_status ON browser_extension_tokens(token_hash, status);
392
+ CREATE INDEX IF NOT EXISTS idx_desktop_companion_devices_user ON desktop_companion_devices(user_id, status, created_at DESC);
363
393
  CREATE INDEX IF NOT EXISTS idx_messages_user ON messages(user_id, created_at DESC);
364
394
  CREATE INDEX IF NOT EXISTS idx_messages_platform ON messages(platform, platform_chat_id);
365
395
  CREATE INDEX IF NOT EXISTS idx_conv_messages ON conversation_messages(conversation_id, created_at);
@@ -732,6 +762,21 @@ for (const col of [
732
762
  "ALTER TABLE recording_sessions ADD COLUMN duration_ms INTEGER DEFAULT 0",
733
763
  "ALTER TABLE recording_sessions ADD COLUMN structured_content_json TEXT",
734
764
  "ALTER TABLE artifacts ADD COLUMN metadata_json TEXT DEFAULT '{}'",
765
+ "ALTER TABLE desktop_companion_devices ADD COLUMN activation_id TEXT",
766
+ "ALTER TABLE desktop_companion_devices ADD COLUMN app_version TEXT",
767
+ "ALTER TABLE desktop_companion_devices ADD COLUMN companion_enabled INTEGER DEFAULT 0",
768
+ "ALTER TABLE desktop_companion_devices ADD COLUMN paused INTEGER DEFAULT 0",
769
+ "ALTER TABLE desktop_companion_devices ADD COLUMN status TEXT DEFAULT 'offline'",
770
+ "ALTER TABLE desktop_companion_devices ADD COLUMN display_count INTEGER DEFAULT 0",
771
+ "ALTER TABLE desktop_companion_devices ADD COLUMN active_display_id TEXT",
772
+ "ALTER TABLE desktop_companion_devices ADD COLUMN permissions_json TEXT DEFAULT '{}'",
773
+ "ALTER TABLE desktop_companion_devices ADD COLUMN capabilities_json TEXT DEFAULT '{}'",
774
+ "ALTER TABLE desktop_companion_devices ADD COLUMN metadata_json TEXT DEFAULT '{}'",
775
+ "ALTER TABLE desktop_companion_devices ADD COLUMN session_id INTEGER",
776
+ "ALTER TABLE desktop_companion_devices ADD COLUMN last_connected_at TEXT",
777
+ "ALTER TABLE desktop_companion_devices ADD COLUMN last_seen_at TEXT",
778
+ "ALTER TABLE desktop_companion_devices ADD COLUMN revoked_at TEXT",
779
+ "ALTER TABLE desktop_companion_devices ADD COLUMN updated_at TEXT DEFAULT (datetime('now'))",
735
780
  ]) {
736
781
  try { db.exec(col); } catch { /* column already exists */ }
737
782
  }
@@ -1234,8 +1279,6 @@ rebuildPlatformConnectionsForAgents();
1234
1279
  rebuildCoreMemoryForAgents();
1235
1280
  migrateIntegrationConnectionsTable();
1236
1281
  migrateIntegrationOauthStatesTable();
1237
- backfillAgentIds();
1238
- backfillAgentPolicies();
1239
1282
  createAgentScopedIndexes();
1240
1283
  migrateIntegrationSecretStorage();
1241
1284
  backfillVerifiedAccountEmails();