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.
- package/extensions/chrome-browser/background.mjs +40 -5
- package/extensions/chrome-browser/popup.css +27 -2
- package/extensions/chrome-browser/popup.js +39 -0
- package/extensions/chrome-browser/protocol.mjs +3 -3
- package/lib/install_helpers.js +1 -1
- package/lib/manager.js +26 -44
- package/package.json +1 -1
- package/runtime/env.js +17 -0
- package/runtime/git_helpers.js +41 -0
- package/runtime/paths.js +37 -4
- package/runtime/release_channel.js +1 -12
- package/server/config/origins.js +4 -1
- package/server/db/database.js +45 -2
- package/server/guest_agent.js +52 -16
- package/server/http/errors.js +7 -2
- package/server/http/middleware.js +26 -20
- package/server/http/routes.js +1 -0
- package/server/http/socket.js +7 -1
- package/server/index.js +4 -1
- package/server/middleware/auth.js +31 -9
- package/server/public/assets/NOTICES +143 -0
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/public/main.dart.js +70615 -69343
- package/server/routes/_helpers/readChunkBody.js +2 -1
- package/server/routes/auth.js +33 -19
- package/server/routes/browser.js +8 -0
- package/server/routes/desktop.js +240 -0
- package/server/routes/integrations.js +12 -0
- package/server/routes/mcp.js +24 -4
- package/server/routes/scheduler.js +22 -3
- package/server/routes/voice_assistant.js +37 -0
- package/server/services/account/service_email.js +32 -0
- package/server/services/account/session_secret.js +7 -0
- package/server/services/ai/engine.js +20 -2
- package/server/services/ai/imageAnalysis.js +104 -0
- package/server/services/ai/outputSanitizer.js +19 -2
- package/server/services/ai/toolResult.js +16 -12
- package/server/services/ai/tools.js +224 -62
- package/server/services/browser/extension/gateway.js +48 -4
- package/server/services/browser/extension/protocol.js +3 -0
- package/server/services/browser/extension/registry.js +1 -1
- package/server/services/desktop/auth.js +70 -0
- package/server/services/desktop/gateway.js +215 -0
- package/server/services/desktop/protocol.js +64 -0
- package/server/services/desktop/provider.js +176 -0
- package/server/services/desktop/registry.js +521 -0
- package/server/services/integrations/google/provider.js +10 -2
- package/server/services/integrations/manager.js +12 -2
- package/server/services/integrations/microsoft/provider.js +3 -0
- package/server/services/integrations/oauth_provider.js +38 -7
- package/server/services/integrations/slack/provider.js +8 -1
- package/server/services/integrations/whatsapp/provider.js +73 -5
- package/server/services/manager.js +36 -0
- package/server/services/memory/embeddings.js +31 -11
- package/server/services/messaging/automation.js +11 -1
- package/server/services/messaging/http_platforms.js +8 -1
- package/server/services/voice/message.js +24 -0
- package/server/services/voice/screenshotContext.js +73 -0
- package/server/services/voice/turnRunner.js +25 -2
- package/server/services/websocket.js +496 -72
- package/server/utils/logger.js +2 -0
- package/server/utils/security.js +12 -8
- 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
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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.
|
|
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
|
-
//
|
|
41
|
-
//
|
|
42
|
-
return `(() =>
|
|
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) {
|
package/lib/install_helpers.js
CHANGED
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(
|
|
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
|
|
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.
|
|
365
|
-
entry.command.
|
|
366
|
-
entry.command
|
|
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
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 {
|
|
50
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|
package/server/config/origins.js
CHANGED
|
@@ -23,7 +23,10 @@ function isChromeExtensionOrigin(origin) {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
function isAllowedOrigin(origin, options = {}) {
|
|
26
|
-
if (
|
|
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;
|
package/server/db/database.js
CHANGED
|
@@ -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();
|