neoagent 2.4.1-beta.8 → 2.4.1
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/.env.example +33 -3
- package/LICENSE +111 -56
- package/README.md +8 -3
- package/docs/configuration.md +8 -0
- package/docs/getting-started.md +9 -3
- package/docs/index.md +4 -0
- package/extensions/chrome-browser/background.mjs +45 -7
- package/extensions/chrome-browser/icons/icon128.png +0 -0
- package/extensions/chrome-browser/icons/icon16.png +0 -0
- package/extensions/chrome-browser/icons/icon48.png +0 -0
- package/extensions/chrome-browser/icons/logo.svg +39 -8
- package/extensions/chrome-browser/manifest.json +3 -2
- package/extensions/chrome-browser/popup.html +5 -1
- package/extensions/chrome-browser/popup.js +15 -2
- package/flutter_app/android/app/src/main/AndroidManifest.xml +2 -1
- package/flutter_app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
- package/flutter_app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
- package/flutter_app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
- package/flutter_app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
- package/flutter_app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
- package/flutter_app/assets/branding/app_icon_1024.png +0 -0
- package/flutter_app/assets/branding/app_icon_128.png +0 -0
- package/flutter_app/assets/branding/app_icon_192.png +0 -0
- package/flutter_app/assets/branding/app_icon_256.png +0 -0
- package/flutter_app/assets/branding/app_icon_32.png +0 -0
- package/flutter_app/assets/branding/app_icon_512.png +0 -0
- package/flutter_app/assets/branding/app_icon_64.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_1024.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_128.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_192.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_256.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_32.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_512.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_64.png +0 -0
- package/flutter_app/assets/branding/onboarding_intro.mp4 +0 -0
- package/flutter_app/assets/branding/tray_icon_light_template.png +0 -0
- package/flutter_app/assets/branding/tray_icon_template.png +0 -0
- package/flutter_app/lib/features/location/location_service.dart +3 -0
- package/flutter_app/lib/features/onboarding/onboarding_chrome.dart +391 -382
- package/flutter_app/lib/features/onboarding/onboarding_companion_step.dart +743 -0
- package/flutter_app/lib/features/onboarding/onboarding_messaging_step.dart +18 -16
- package/flutter_app/lib/features/onboarding/onboarding_model_step.dart +19 -18
- package/flutter_app/lib/features/onboarding/onboarding_shell.dart +8 -1
- package/flutter_app/lib/features/onboarding/onboarding_video_step.dart +16 -13
- package/flutter_app/lib/features/onboarding/onboarding_welcome_step.dart +17 -13
- package/flutter_app/lib/main.dart +3 -0
- package/flutter_app/lib/main_account_settings.dart +10 -34
- package/flutter_app/lib/main_admin.dart +14 -1
- package/flutter_app/lib/main_app_shell.dart +377 -340
- package/flutter_app/lib/main_chat.dart +707 -227
- package/flutter_app/lib/main_controller.dart +338 -46
- package/flutter_app/lib/main_devices.dart +851 -122
- package/flutter_app/lib/main_integrations.dart +255 -6
- package/flutter_app/lib/main_launcher.dart +1 -1
- package/flutter_app/lib/main_model_picker.dart +685 -0
- package/flutter_app/lib/main_models.dart +212 -0
- package/flutter_app/lib/main_navigation.dart +1 -9
- package/flutter_app/lib/main_operations.dart +1417 -614
- package/flutter_app/lib/main_settings.dart +764 -887
- package/flutter_app/lib/main_shared.dart +971 -443
- package/flutter_app/lib/main_spacing.dart +4 -4
- package/flutter_app/lib/main_theme.dart +22 -14
- package/flutter_app/lib/main_unified.dart +5 -72
- package/flutter_app/lib/src/android_apk_drop_zone.dart +24 -0
- package/flutter_app/lib/src/android_apk_drop_zone_stub.dart +13 -0
- package/flutter_app/lib/src/android_apk_drop_zone_web.dart +219 -0
- package/flutter_app/lib/src/backend_client.dart +79 -0
- package/flutter_app/lib/src/desktop_companion_actions.dart +56 -7
- package/flutter_app/lib/src/desktop_companion_io.dart +77 -1
- package/flutter_app/lib/src/desktop_native_bridge.dart +13 -0
- package/flutter_app/lib/src/security/password_strength.dart +84 -0
- package/flutter_app/lib/src/stream_renderer.dart +205 -35
- package/flutter_app/lib/src/theme/palette.dart +76 -34
- package/flutter_app/linux/runner/resources/app_icon.png +0 -0
- package/flutter_app/macos/Runner/AppDelegate.swift +44 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png +0 -0
- package/flutter_app/pubspec.lock +2 -2
- package/flutter_app/pubspec.yaml +7 -1
- package/flutter_app/tool/branding_source/neoagent-icon-1024.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-128.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-16.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-180.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-192.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-256.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-32.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-48.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-512.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-64.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon.svg +43 -0
- package/flutter_app/tool/generate_desktop_branding.py +154 -152
- package/flutter_app/web/favicon.png +0 -0
- package/flutter_app/web/favicon.svg +40 -9
- package/flutter_app/web/favicon_light.svg +43 -0
- package/flutter_app/web/icons/Icon-192-light.png +0 -0
- package/flutter_app/web/icons/Icon-192.png +0 -0
- package/flutter_app/web/icons/Icon-512-light.png +0 -0
- package/flutter_app/web/icons/Icon-512.png +0 -0
- package/flutter_app/web/icons/Icon-maskable-192-light.png +0 -0
- package/flutter_app/web/icons/Icon-maskable-192.png +0 -0
- package/flutter_app/web/icons/Icon-maskable-512-light.png +0 -0
- package/flutter_app/web/icons/Icon-maskable-512.png +0 -0
- package/flutter_app/windows/runner/main.cpp +7 -1
- package/flutter_app/windows/runner/resources/app_icon.ico +0 -0
- package/lib/manager.js +445 -83
- package/package.json +17 -3
- package/runtime/paths.js +3 -1
- package/server/admin/access.js +198 -0
- package/server/admin/admin.css +268 -0
- package/server/admin/admin.js +348 -0
- package/server/admin/analytics.js +128 -0
- package/server/admin/index.html +1015 -0
- package/server/admin/login.html +290 -0
- package/server/admin/logo.svg +43 -0
- package/server/admin/sql.js +134 -0
- package/server/admin/users.js +147 -0
- package/server/config/origins.js +3 -1
- package/server/db/database.js +92 -0
- package/server/db/ftsQuery.js +27 -0
- package/server/http/routes.js +1 -0
- package/server/http/static.js +23 -6
- package/server/index.js +1 -1
- package/server/middleware/adminAuth.js +48 -0
- package/server/middleware/auth.js +1 -40
- package/server/public/.last_build_id +1 -1
- package/server/public/app_icon.png +0 -0
- package/server/public/assets/AssetManifest.bin +1 -1
- package/server/public/assets/AssetManifest.bin.json +1 -1
- package/server/public/assets/assets/branding/app_icon_1024.png +0 -0
- package/server/public/assets/assets/branding/app_icon_256.png +0 -0
- package/server/public/assets/assets/branding/app_icon_512.png +0 -0
- package/server/public/assets/assets/branding/app_icon_light_1024.png +0 -0
- package/server/public/assets/assets/branding/app_icon_light_256.png +0 -0
- package/server/public/assets/assets/branding/app_icon_light_512.png +0 -0
- package/server/public/assets/assets/branding/onboarding_intro.mp4 +0 -0
- package/server/public/assets/assets/branding/tray_icon_light_template.png +0 -0
- package/server/public/assets/assets/branding/tray_icon_template.png +0 -0
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/assets/web/icons/Icon-192.png +0 -0
- package/server/public/favicon.png +0 -0
- package/server/public/favicon.svg +40 -9
- package/server/public/favicon_light.svg +43 -0
- package/server/public/flutter_bootstrap.js +2 -2
- package/server/public/icons/Icon-192-light.png +0 -0
- package/server/public/icons/Icon-192.png +0 -0
- package/server/public/icons/Icon-512-light.png +0 -0
- package/server/public/icons/Icon-512.png +0 -0
- package/server/public/icons/Icon-maskable-192-light.png +0 -0
- package/server/public/icons/Icon-maskable-192.png +0 -0
- package/server/public/icons/Icon-maskable-512-light.png +0 -0
- package/server/public/icons/Icon-maskable-512.png +0 -0
- package/server/public/main.dart.js +88944 -85720
- package/server/routes/admin.js +632 -0
- package/server/routes/agent_profiles.js +3 -0
- package/server/routes/agents.js +1 -1
- package/server/routes/auth.js +3 -1
- package/server/routes/browser.js +14 -0
- package/server/routes/browser_extension.js +21 -4
- package/server/routes/desktop.js +10 -0
- package/server/routes/mcp.js +29 -13
- package/server/routes/memory.js +23 -1
- package/server/routes/messaging.js +2 -0
- package/server/routes/screenHistory.js +14 -8
- package/server/routes/settings.js +14 -2
- package/server/routes/stream.js +12 -3
- package/server/routes/triggers.js +4 -4
- package/server/routes/voice_assistant.js +36 -1
- package/server/routes/workspace.js +86 -0
- package/server/services/account/admin_two_factor.js +132 -0
- package/server/services/account/password_policy.js +6 -1
- package/server/services/ai/completion.js +44 -0
- package/server/services/ai/engine.js +321 -500
- package/server/services/ai/imageAnalysis.js +9 -5
- package/server/services/ai/logFormat.js +46 -0
- package/server/services/ai/loopPolicy.js +11 -0
- package/server/services/ai/messagingFallback.js +228 -0
- package/server/services/ai/models.js +194 -55
- package/server/services/ai/providerRetry.js +169 -0
- package/server/services/ai/providers/anthropic.js +15 -4
- package/server/services/ai/providers/google.js +38 -11
- package/server/services/ai/providers/grok.js +19 -68
- package/server/services/ai/providers/grokOauth.js +141 -0
- package/server/services/ai/providers/nvidia.js +154 -0
- package/server/services/ai/providers/ollama.js +76 -39
- package/server/services/ai/providers/openai.js +20 -31
- package/server/services/ai/providers/openaiCodex.js +6 -2
- package/server/services/ai/providers/openaiCompatible.js +70 -0
- package/server/services/ai/providers/openrouter.js +162 -0
- package/server/services/ai/settings.js +30 -0
- package/server/services/ai/systemPrompt.js +51 -29
- package/server/services/ai/taskAnalysis.js +60 -1
- package/server/services/ai/toolEvidence.js +207 -0
- package/server/services/ai/toolSelector.js +8 -1
- package/server/services/ai/tools.js +80 -15
- package/server/services/android/controller.js +36 -1
- package/server/services/browser/controller.js +18 -0
- package/server/services/browser/extension/manifest.js +33 -0
- package/server/services/browser/extension/provider.js +12 -6
- package/server/services/browser/extension/registry.js +229 -18
- package/server/services/desktop/protocol.js +1 -0
- package/server/services/desktop/provider.js +4 -0
- package/server/services/desktop/registry.js +104 -1
- package/server/services/desktop/screenRecorder.js +208 -98
- package/server/services/desktop/screen_recorder_support.js +46 -0
- package/server/services/integrations/google/provider.js +13 -0
- package/server/services/integrations/home_assistant/constants.js +88 -0
- package/server/services/integrations/home_assistant/network.js +207 -0
- package/server/services/integrations/home_assistant/provider.js +249 -0
- package/server/services/integrations/home_assistant/snapshot.js +98 -0
- package/server/services/integrations/home_assistant/tools.js +101 -0
- package/server/services/integrations/manager.js +5 -0
- package/server/services/integrations/registry.js +2 -0
- package/server/services/integrations/trello/provider.js +8 -2
- package/server/services/manager.js +51 -47
- package/server/services/mcp/client.js +208 -268
- package/server/services/mcp/client_support.js +172 -0
- package/server/services/mcp/recovery.js +116 -0
- package/server/services/mcp/tool_operations.js +123 -0
- package/server/services/memory/ingestion.js +185 -370
- package/server/services/memory/ingestion_coverage.js +129 -0
- package/server/services/memory/ingestion_documents.js +123 -0
- package/server/services/memory/ingestion_support.js +171 -0
- package/server/services/memory/intelligence.js +181 -0
- package/server/services/memory/manager.js +476 -32
- package/server/services/messaging/automation.js +71 -134
- package/server/services/messaging/formatting_guides.js +26 -1
- package/server/services/messaging/inbound_queue.js +111 -0
- package/server/services/messaging/manager.js +25 -8
- package/server/services/messaging/typing_keepalive.js +110 -0
- package/server/services/runtime/manager.js +7 -3
- package/server/services/runtime/settings.js +17 -0
- package/server/services/streaming/android-stream.js +296 -32
- package/server/services/streaming/stream-hub.js +1 -1
- package/server/services/tasks/integration_runtime.js +4 -1
- package/server/services/tasks/runtime.js +415 -56
- package/server/services/tasks/task_repository.js +48 -6
- package/server/services/websocket.js +8 -2
- package/server/services/widgets/service.js +17 -1
- package/server/services/workspace/manager.js +116 -0
- package/server/utils/logger.js +44 -6
- package/server/utils/security.js +3 -0
- package/server/utils/version.js +29 -19
- package/server/services/memory/openhuman_uplift.test.js +0 -98
|
@@ -59,16 +59,34 @@ class TaskRepository {
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
listTasksForAgent(userId, agentId, includeLegacyMainTasks) {
|
|
62
|
+
const select = `
|
|
63
|
+
SELECT
|
|
64
|
+
scheduled_tasks.*,
|
|
65
|
+
latest_run.id AS last_run_id,
|
|
66
|
+
latest_run.status AS last_run_status,
|
|
67
|
+
latest_run.error AS last_run_error,
|
|
68
|
+
latest_run.created_at AS last_run_started_at
|
|
69
|
+
FROM scheduled_tasks
|
|
70
|
+
LEFT JOIN agent_runs AS latest_run ON latest_run.id = (
|
|
71
|
+
SELECT candidate.id
|
|
72
|
+
FROM agent_runs AS candidate
|
|
73
|
+
WHERE candidate.user_id = scheduled_tasks.user_id
|
|
74
|
+
AND json_valid(candidate.metadata_json)
|
|
75
|
+
AND CAST(json_extract(candidate.metadata_json, '$.taskId') AS TEXT) = CAST(scheduled_tasks.id AS TEXT)
|
|
76
|
+
ORDER BY candidate.created_at DESC, candidate.rowid DESC
|
|
77
|
+
LIMIT 1
|
|
78
|
+
)`;
|
|
62
79
|
return includeLegacyMainTasks
|
|
63
80
|
? db.prepare(
|
|
64
|
-
|
|
65
|
-
WHERE user_id = ?
|
|
66
|
-
|
|
81
|
+
`${select}
|
|
82
|
+
WHERE scheduled_tasks.user_id = ?
|
|
83
|
+
AND (scheduled_tasks.agent_id = ? OR scheduled_tasks.agent_id IS NULL)
|
|
84
|
+
ORDER BY scheduled_tasks.created_at DESC`
|
|
67
85
|
).all(userId, agentId)
|
|
68
86
|
: db.prepare(
|
|
69
|
-
|
|
70
|
-
WHERE user_id = ? AND agent_id = ?
|
|
71
|
-
ORDER BY created_at DESC`
|
|
87
|
+
`${select}
|
|
88
|
+
WHERE scheduled_tasks.user_id = ? AND scheduled_tasks.agent_id = ?
|
|
89
|
+
ORDER BY scheduled_tasks.created_at DESC`
|
|
72
90
|
).all(userId, agentId);
|
|
73
91
|
}
|
|
74
92
|
|
|
@@ -126,11 +144,35 @@ class TaskRepository {
|
|
|
126
144
|
db.prepare('UPDATE scheduled_tasks SET last_run = datetime(\'now\') WHERE id = ? AND user_id = ?').run(taskId, userId);
|
|
127
145
|
}
|
|
128
146
|
|
|
147
|
+
markAgentRunFailed(runId, userId, error) {
|
|
148
|
+
db.prepare(
|
|
149
|
+
`UPDATE agent_runs
|
|
150
|
+
SET status = 'failed',
|
|
151
|
+
error = ?,
|
|
152
|
+
updated_at = datetime('now'),
|
|
153
|
+
completed_at = COALESCE(completed_at, datetime('now'))
|
|
154
|
+
WHERE id = ? AND user_id = ?`
|
|
155
|
+
).run(String(error || 'Task execution failed.'), runId, userId);
|
|
156
|
+
}
|
|
157
|
+
|
|
129
158
|
updateTaskConfig(taskId, userId, taskConfig) {
|
|
130
159
|
db.prepare('UPDATE scheduled_tasks SET task_config = ? WHERE id = ? AND user_id = ?')
|
|
131
160
|
.run(JSON.stringify(taskConfig), taskId, userId);
|
|
132
161
|
}
|
|
133
162
|
|
|
163
|
+
listRecentMessageTargets(userId, agentId, limit = 20) {
|
|
164
|
+
return db.prepare(
|
|
165
|
+
`SELECT platform, platform_chat_id
|
|
166
|
+
FROM messages
|
|
167
|
+
WHERE user_id = ?
|
|
168
|
+
AND agent_id = ?
|
|
169
|
+
AND platform IS NOT NULL
|
|
170
|
+
AND platform_chat_id IS NOT NULL
|
|
171
|
+
ORDER BY id DESC
|
|
172
|
+
LIMIT ?`
|
|
173
|
+
).all(userId, agentId, Math.max(1, Math.min(Number(limit) || 20, 100)));
|
|
174
|
+
}
|
|
175
|
+
|
|
134
176
|
getAgentSetting(userId, agentId, key) {
|
|
135
177
|
return db.prepare('SELECT value FROM agent_settings WHERE user_id = ? AND agent_id = ? AND key = ?')
|
|
136
178
|
.get(userId, agentId, key);
|
|
@@ -317,7 +317,7 @@ function setupWebSocket(io, services) {
|
|
|
317
317
|
const data = asObject(raw);
|
|
318
318
|
const runId = toOptionalString(data?.runId, 128);
|
|
319
319
|
console.warn(`[WS] agent:abort received from user ${userId} for run ${runId || 'unknown'}`);
|
|
320
|
-
agentEngine.abort(runId || null);
|
|
320
|
+
agentEngine.abort(runId || null, { userId });
|
|
321
321
|
socket.emit('agent:aborted', { runId: runId || null });
|
|
322
322
|
} catch (err) {
|
|
323
323
|
console.error(`[WS] agent:abort failed for user ${userId}:`, err);
|
|
@@ -366,8 +366,14 @@ function setupWebSocket(io, services) {
|
|
|
366
366
|
}
|
|
367
367
|
console.log(`[WS] agent:run_detail requested by user ${userId} run=${runId}`);
|
|
368
368
|
const run = db.prepare('SELECT * FROM agent_runs WHERE id = ? AND user_id = ?').get(runId, userId);
|
|
369
|
+
if (!run) {
|
|
370
|
+
// Don't leak steps/history/events for a run the user doesn't own:
|
|
371
|
+
// agent_steps and run events are keyed only by run_id, so the run
|
|
372
|
+
// ownership check is the sole authorization gate.
|
|
373
|
+
return socket.emit('agent:run_detail', { run: null, steps: [], history: [], events: [] });
|
|
374
|
+
}
|
|
369
375
|
const steps = db.prepare('SELECT * FROM agent_steps WHERE run_id = ? ORDER BY step_index ASC').all(runId);
|
|
370
|
-
const history = db.prepare('SELECT * FROM conversation_history WHERE agent_run_id = ? ORDER BY created_at ASC').all(runId);
|
|
376
|
+
const history = db.prepare('SELECT * FROM conversation_history WHERE agent_run_id = ? AND user_id = ? ORDER BY created_at ASC').all(runId, userId);
|
|
371
377
|
const events = listRunEvents(runId);
|
|
372
378
|
socket.emit('agent:run_detail', { run, steps, history, events });
|
|
373
379
|
} catch (err) {
|
|
@@ -366,7 +366,23 @@ class WidgetService {
|
|
|
366
366
|
throw new Error('Widget not found.');
|
|
367
367
|
}
|
|
368
368
|
if (existingRow.is_system) {
|
|
369
|
-
|
|
369
|
+
if (!('enabled' in input)) {
|
|
370
|
+
throw new Error('System widgets cannot be edited directly.');
|
|
371
|
+
}
|
|
372
|
+
// Only the enabled state may be toggled on system widgets.
|
|
373
|
+
const newEnabled = input.enabled !== false;
|
|
374
|
+
db.prepare(
|
|
375
|
+
`UPDATE ai_widgets SET enabled = ?, updated_at = datetime('now') WHERE id = ? AND user_id = ?`
|
|
376
|
+
).run(newEnabled ? 1 : 0, widgetId, userId);
|
|
377
|
+
if (existingRow.scheduled_task_id && this.taskRuntime) {
|
|
378
|
+
await this.taskRuntime.updateTask(
|
|
379
|
+
existingRow.scheduled_task_id,
|
|
380
|
+
userId,
|
|
381
|
+
{ enabled: newEnabled },
|
|
382
|
+
{ allowManaged: true },
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
return this.getWidget(userId, widgetId);
|
|
370
386
|
}
|
|
371
387
|
|
|
372
388
|
const current = this._serializeWidget(existingRow, null, []);
|
|
@@ -80,9 +80,125 @@ class WorkspaceManager {
|
|
|
80
80
|
if (relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
81
81
|
throw new Error(`${label} is outside the per-user workspace.`);
|
|
82
82
|
}
|
|
83
|
+
this._assertResolvedPathInsideRoot(root, absolute, label);
|
|
83
84
|
return absolute;
|
|
84
85
|
}
|
|
85
86
|
|
|
87
|
+
_assertResolvedPathInsideRoot(root, absolute, label = 'path') {
|
|
88
|
+
const realRoot = fs.realpathSync.native(root);
|
|
89
|
+
let targetToCheck = absolute;
|
|
90
|
+
while (!fs.existsSync(targetToCheck)) {
|
|
91
|
+
const parent = path.dirname(targetToCheck);
|
|
92
|
+
if (parent === targetToCheck) break;
|
|
93
|
+
targetToCheck = path.dirname(targetToCheck);
|
|
94
|
+
}
|
|
95
|
+
const realTarget = fs.realpathSync.native(targetToCheck);
|
|
96
|
+
const relative = path.relative(realRoot, realTarget);
|
|
97
|
+
if (relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
98
|
+
throw new Error(`${label} is outside the per-user workspace.`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
_toWorkspaceRelative(root, absolute) {
|
|
103
|
+
const relative = path.relative(root, absolute);
|
|
104
|
+
return relative && !relative.startsWith('..') && !path.isAbsolute(relative)
|
|
105
|
+
? relative.split(path.sep).join('/')
|
|
106
|
+
: '';
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
listExplorerDirectory(userId, options = {}) {
|
|
110
|
+
const root = this._ensureWorkspaceRootSync(userId);
|
|
111
|
+
const dirPath = this.resolvePath(userId, options.path || '.', 'path');
|
|
112
|
+
let stats;
|
|
113
|
+
try {
|
|
114
|
+
stats = fs.statSync(dirPath);
|
|
115
|
+
} catch (err) {
|
|
116
|
+
return { path: this._toWorkspaceRelative(root, dirPath), entries: [], error: err.message };
|
|
117
|
+
}
|
|
118
|
+
if (!stats.isDirectory()) {
|
|
119
|
+
return { path: this._toWorkspaceRelative(root, dirPath), entries: [], error: 'Path is not a directory.' };
|
|
120
|
+
}
|
|
121
|
+
const entries = fs.readdirSync(dirPath, { withFileTypes: true }).flatMap((entry) => {
|
|
122
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
123
|
+
let entryStats;
|
|
124
|
+
try {
|
|
125
|
+
entryStats = fs.lstatSync(fullPath);
|
|
126
|
+
if (entryStats.isSymbolicLink()) return [];
|
|
127
|
+
entryStats = fs.statSync(fullPath);
|
|
128
|
+
} catch {
|
|
129
|
+
return [];
|
|
130
|
+
}
|
|
131
|
+
if (!entryStats.isDirectory() && !entryStats.isFile()) return [];
|
|
132
|
+
return [{
|
|
133
|
+
name: entry.name,
|
|
134
|
+
type: entryStats.isDirectory() ? 'directory' : 'file',
|
|
135
|
+
path: this._toWorkspaceRelative(root, fullPath),
|
|
136
|
+
size: entryStats.size,
|
|
137
|
+
mtime: entryStats.mtime.toISOString(),
|
|
138
|
+
}];
|
|
139
|
+
}).sort((a, b) => {
|
|
140
|
+
if (a.type !== b.type) return a.type === 'directory' ? -1 : 1;
|
|
141
|
+
return a.name.localeCompare(b.name);
|
|
142
|
+
});
|
|
143
|
+
return {
|
|
144
|
+
path: this._toWorkspaceRelative(root, dirPath),
|
|
145
|
+
parentPath: this._toWorkspaceRelative(root, path.dirname(dirPath)),
|
|
146
|
+
entries,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
readExplorerFile(userId, options = {}) {
|
|
151
|
+
const root = this._ensureWorkspaceRootSync(userId);
|
|
152
|
+
const filePath = this.resolvePath(userId, options.path || '', 'path');
|
|
153
|
+
const stats = fs.statSync(filePath);
|
|
154
|
+
if (!stats.isFile()) {
|
|
155
|
+
throw new Error('Path is not a file.');
|
|
156
|
+
}
|
|
157
|
+
const maxBytes = Number(options.maxBytes || 1024 * 1024);
|
|
158
|
+
if (stats.size > maxBytes) {
|
|
159
|
+
const error = new Error(`File is too large to edit in the browser (${stats.size} bytes).`);
|
|
160
|
+
error.code = 'WORKSPACE_FILE_TOO_LARGE';
|
|
161
|
+
throw error;
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
path: this._toWorkspaceRelative(root, filePath),
|
|
165
|
+
name: path.basename(filePath),
|
|
166
|
+
content: fs.readFileSync(filePath, 'utf8'),
|
|
167
|
+
size: stats.size,
|
|
168
|
+
mtime: stats.mtime.toISOString(),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
writeExplorerFile(userId, options = {}) {
|
|
173
|
+
const root = this._ensureWorkspaceRootSync(userId);
|
|
174
|
+
const filePath = this.resolvePath(userId, options.path || '', 'path');
|
|
175
|
+
const content = String(options.content ?? '');
|
|
176
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
177
|
+
fs.writeFileSync(filePath, content, 'utf8');
|
|
178
|
+
const stats = fs.statSync(filePath);
|
|
179
|
+
return {
|
|
180
|
+
success: true,
|
|
181
|
+
path: this._toWorkspaceRelative(root, filePath),
|
|
182
|
+
size: stats.size,
|
|
183
|
+
mtime: stats.mtime.toISOString(),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
getExplorerDownload(userId, options = {}) {
|
|
188
|
+
const root = this._ensureWorkspaceRootSync(userId);
|
|
189
|
+
const filePath = this.resolvePath(userId, options.path || '', 'path');
|
|
190
|
+
const stats = fs.statSync(filePath);
|
|
191
|
+
if (!stats.isFile()) {
|
|
192
|
+
throw new Error('Path is not a file.');
|
|
193
|
+
}
|
|
194
|
+
return {
|
|
195
|
+
root,
|
|
196
|
+
absolutePath: filePath,
|
|
197
|
+
relativePath: this._toWorkspaceRelative(root, filePath),
|
|
198
|
+
filename: path.basename(filePath),
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
86
202
|
readFile(userId, options = {}) {
|
|
87
203
|
let filePath;
|
|
88
204
|
try {
|
package/server/utils/logger.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
3
6
|
const SENSITIVE_KEY_RE = /(?:^|_|-)(?:token|secret|password|api[_-]?key|authorization|cookie|session(?:id)?|sid)(?:$|_|-)/i;
|
|
4
7
|
|
|
5
8
|
function redactSecrets(input) {
|
|
@@ -96,22 +99,57 @@ function logRequestSummary(level, req, message, extra = null) {
|
|
|
96
99
|
console[level](redactSecrets(`${prefix} ${message}`), summary);
|
|
97
100
|
}
|
|
98
101
|
|
|
102
|
+
function getLogFile() {
|
|
103
|
+
try {
|
|
104
|
+
const { DATA_DIR } = require('../../runtime/paths');
|
|
105
|
+
return path.join(DATA_DIR, 'server-logs.jsonl');
|
|
106
|
+
} catch {
|
|
107
|
+
return path.join(require('os').homedir(), '.neoagent', 'data', 'server-logs.jsonl');
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function loadPersistedLogs(logFile) {
|
|
112
|
+
try {
|
|
113
|
+
const raw = fs.readFileSync(logFile, 'utf8');
|
|
114
|
+
const entries = [];
|
|
115
|
+
for (const line of raw.split('\n')) {
|
|
116
|
+
if (!line.trim()) continue;
|
|
117
|
+
try { entries.push(JSON.parse(line)); } catch {}
|
|
118
|
+
}
|
|
119
|
+
return entries.slice(-1000);
|
|
120
|
+
} catch {
|
|
121
|
+
return [];
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
99
125
|
/**
|
|
100
|
-
* Intercepts console methods and broadcasts logs via Socket.IO
|
|
101
|
-
*
|
|
126
|
+
* Intercepts console methods and broadcasts logs via Socket.IO.
|
|
127
|
+
* Persists up to 1000 entries to disk across server restarts.
|
|
128
|
+
* @param {import('socket.io').Server} io
|
|
102
129
|
*/
|
|
103
130
|
function setupConsoleInterceptor(io) {
|
|
104
|
-
const
|
|
105
|
-
const
|
|
131
|
+
const MAX_LOG_HISTORY = 1000;
|
|
132
|
+
const TRIM_AT = 1100;
|
|
133
|
+
const logFile = getLogFile();
|
|
106
134
|
const allowLogHistoryRequests = String(process.env.NEOAGENT_ENABLE_LOG_HISTORY_REQUESTS || '').trim().toLowerCase() === 'true';
|
|
107
135
|
|
|
136
|
+
try { fs.mkdirSync(path.dirname(logFile), { recursive: true }); } catch {}
|
|
137
|
+
const logHistory = loadPersistedLogs(logFile);
|
|
138
|
+
|
|
108
139
|
function broadcastLog(type, args) {
|
|
109
140
|
const msg = formatLogArgs(args);
|
|
110
141
|
const logEntry = { type, message: msg, timestamp: new Date().toISOString() };
|
|
111
142
|
logHistory.push(logEntry);
|
|
112
|
-
if (logHistory.length > MAX_LOG_HISTORY) logHistory.shift();
|
|
113
143
|
|
|
114
|
-
|
|
144
|
+
try { fs.appendFileSync(logFile, JSON.stringify(logEntry) + '\n'); } catch {}
|
|
145
|
+
|
|
146
|
+
if (logHistory.length > TRIM_AT) {
|
|
147
|
+
logHistory.splice(0, logHistory.length - MAX_LOG_HISTORY);
|
|
148
|
+
try {
|
|
149
|
+
fs.writeFileSync(logFile, logHistory.map(e => JSON.stringify(e)).join('\n') + '\n');
|
|
150
|
+
} catch {}
|
|
151
|
+
}
|
|
152
|
+
|
|
115
153
|
for (const [, socket] of io.sockets.sockets) {
|
|
116
154
|
const uid = socket.request?.session?.userId;
|
|
117
155
|
if (uid) socket.emit('server:log', logEntry);
|
package/server/utils/security.js
CHANGED
|
@@ -15,6 +15,9 @@ function sanitizeError(err) {
|
|
|
15
15
|
const raw = typeof err === 'string' ? err : err.message || String(err);
|
|
16
16
|
|
|
17
17
|
let msg = raw;
|
|
18
|
+
if (!msg || msg === '[object Object]') {
|
|
19
|
+
msg = 'An unexpected error occurred';
|
|
20
|
+
}
|
|
18
21
|
|
|
19
22
|
// Replace home directory path with ~
|
|
20
23
|
if (HOME) {
|
package/server/utils/version.js
CHANGED
|
@@ -12,6 +12,11 @@ const {
|
|
|
12
12
|
const { getDeploymentInfo } = require('./deployment');
|
|
13
13
|
|
|
14
14
|
const PACKAGE_JSON_PATH = path.join(APP_DIR, 'package.json');
|
|
15
|
+
const GIT_COMMAND_TIMEOUT_MS = 750;
|
|
16
|
+
const VERSION_CACHE_TTL_MS = 30 * 1000;
|
|
17
|
+
|
|
18
|
+
let cachedVersionInfo = null;
|
|
19
|
+
let cachedVersionInfoAt = 0;
|
|
15
20
|
|
|
16
21
|
function readPackageVersion() {
|
|
17
22
|
try {
|
|
@@ -22,7 +27,16 @@ function readPackageVersion() {
|
|
|
22
27
|
}
|
|
23
28
|
}
|
|
24
29
|
|
|
25
|
-
function
|
|
30
|
+
function readGitValue(command) {
|
|
31
|
+
return execSync(command, {
|
|
32
|
+
cwd: APP_DIR,
|
|
33
|
+
encoding: 'utf8',
|
|
34
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
35
|
+
timeout: GIT_COMMAND_TIMEOUT_MS,
|
|
36
|
+
}).trim();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function buildVersionInfo() {
|
|
26
40
|
const packageVersion = readPackageVersion() || '0.0.0';
|
|
27
41
|
const releaseChannel = readConfiguredReleaseChannel();
|
|
28
42
|
const deployment = getDeploymentInfo();
|
|
@@ -32,24 +46,9 @@ function getVersionInfo() {
|
|
|
32
46
|
let gitBranch = null;
|
|
33
47
|
|
|
34
48
|
try {
|
|
35
|
-
gitVersion =
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
encoding: 'utf8',
|
|
39
|
-
stdio: ['ignore', 'pipe', 'ignore']
|
|
40
|
-
})
|
|
41
|
-
.trim()
|
|
42
|
-
.replace(/^v/, '') || null;
|
|
43
|
-
gitSha = execSync('git rev-parse --short HEAD', {
|
|
44
|
-
cwd: APP_DIR,
|
|
45
|
-
encoding: 'utf8',
|
|
46
|
-
stdio: ['ignore', 'pipe', 'ignore']
|
|
47
|
-
}).trim();
|
|
48
|
-
gitBranch = execSync('git rev-parse --abbrev-ref HEAD', {
|
|
49
|
-
cwd: APP_DIR,
|
|
50
|
-
encoding: 'utf8',
|
|
51
|
-
stdio: ['ignore', 'pipe', 'ignore']
|
|
52
|
-
}).trim();
|
|
49
|
+
gitVersion = readGitValue('git describe --tags --always --dirty').replace(/^v/, '') || null;
|
|
50
|
+
gitSha = readGitValue('git rev-parse --short HEAD') || null;
|
|
51
|
+
gitBranch = readGitValue('git rev-parse --abbrev-ref HEAD') || null;
|
|
53
52
|
} catch {
|
|
54
53
|
gitSha = process.env.GIT_SHA || null;
|
|
55
54
|
}
|
|
@@ -80,4 +79,15 @@ function getVersionInfo() {
|
|
|
80
79
|
};
|
|
81
80
|
}
|
|
82
81
|
|
|
82
|
+
function getVersionInfo() {
|
|
83
|
+
const now = Date.now();
|
|
84
|
+
if (cachedVersionInfo && now - cachedVersionInfoAt < VERSION_CACHE_TTL_MS) {
|
|
85
|
+
return { ...cachedVersionInfo };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
cachedVersionInfo = buildVersionInfo();
|
|
89
|
+
cachedVersionInfoAt = now;
|
|
90
|
+
return { ...cachedVersionInfo };
|
|
91
|
+
}
|
|
92
|
+
|
|
83
93
|
module.exports = { getVersionInfo };
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const assert = require('node:assert/strict');
|
|
4
|
-
const test = require('node:test');
|
|
5
|
-
const { randomUUID } = require('node:crypto');
|
|
6
|
-
|
|
7
|
-
process.env.OPENAI_API_KEY = '';
|
|
8
|
-
process.env.GOOGLE_AI_KEY = '';
|
|
9
|
-
|
|
10
|
-
const db = require('../../db/database');
|
|
11
|
-
const { resolveAgentId } = require('../agents/manager');
|
|
12
|
-
const { compactTextPayload } = require('../ai/preModelCompaction');
|
|
13
|
-
const { MemoryManager } = require('./manager');
|
|
14
|
-
const { MemoryIngestionService, sourceTypesForConnection } = require('./ingestion');
|
|
15
|
-
const { buildAssistantFocusSnapshot } = require('../widgets/focus_widget');
|
|
16
|
-
|
|
17
|
-
function createTestUser() {
|
|
18
|
-
const username = `openhuman-${randomUUID()}`;
|
|
19
|
-
const result = db.prepare(
|
|
20
|
-
'INSERT INTO users (username, password) VALUES (?, ?)',
|
|
21
|
-
).run(username, 'test');
|
|
22
|
-
const userId = Number(result.lastInsertRowid);
|
|
23
|
-
return {
|
|
24
|
-
userId,
|
|
25
|
-
agentId: resolveAgentId(userId, null),
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function cleanupUser(userId) {
|
|
30
|
-
db.prepare('DELETE FROM users WHERE id = ?').run(userId);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
test('pre-model compaction strips noisy HTML and records metrics', () => {
|
|
34
|
-
const input = '<html><style>.x{}</style><body>Read https://example.com/a/really/long/path today.<br>Read https://example.com/a/really/long/path today.</body></html>';
|
|
35
|
-
const result = compactTextPayload(input, { maxChars: 120, maxLines: 5 });
|
|
36
|
-
|
|
37
|
-
assert.equal(result.metrics.applied, true);
|
|
38
|
-
assert.ok(result.metrics.strategies.includes('html_to_text'));
|
|
39
|
-
assert.ok(result.metrics.strategies.includes('url_shortening'));
|
|
40
|
-
assert.match(result.text, /example\.com/);
|
|
41
|
-
assert.doesNotMatch(result.text, /<body>/);
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
test('memory ingestion writes typed documents, memory, and materialized views', async () => {
|
|
45
|
-
const { userId, agentId } = createTestUser();
|
|
46
|
-
const memoryManager = new MemoryManager();
|
|
47
|
-
const service = new MemoryIngestionService({ memoryManager, intervalMs: 60_000 });
|
|
48
|
-
|
|
49
|
-
try {
|
|
50
|
-
const result = await service.ingestDocuments(userId, [
|
|
51
|
-
{
|
|
52
|
-
externalObjectId: 'thread-123',
|
|
53
|
-
sourceType: 'email',
|
|
54
|
-
normalizedType: 'email',
|
|
55
|
-
title: 'Launch review',
|
|
56
|
-
content: 'The launch review moved to Friday. Alice owns the deck and Bob owns QA.',
|
|
57
|
-
sourceAccount: 'team@example.com',
|
|
58
|
-
salience: 8,
|
|
59
|
-
},
|
|
60
|
-
], {
|
|
61
|
-
agentId,
|
|
62
|
-
sourceType: 'email',
|
|
63
|
-
providerKey: 'google_workspace',
|
|
64
|
-
sourceAccount: 'team@example.com',
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
assert.equal(result.status, 'completed');
|
|
68
|
-
assert.equal(result.documentIds.length, 1);
|
|
69
|
-
assert.equal(result.memoryIds.length, 1);
|
|
70
|
-
|
|
71
|
-
const docs = memoryManager.listIngestionDocuments(userId, {
|
|
72
|
-
agentId,
|
|
73
|
-
providerKey: 'google_workspace',
|
|
74
|
-
});
|
|
75
|
-
assert.equal(docs.length, 1);
|
|
76
|
-
assert.equal(docs[0].sourceType, 'email');
|
|
77
|
-
assert.equal(docs[0].sourceAccount, 'team@example.com');
|
|
78
|
-
|
|
79
|
-
const changes = memoryManager.listRecentKnowledgeChanges(userId, { agentId });
|
|
80
|
-
assert.ok(changes.some((change) => change.kind === 'document'));
|
|
81
|
-
|
|
82
|
-
const views = memoryManager.listKnowledgeViews(userId, { agentId });
|
|
83
|
-
assert.ok(views.some((view) => view.viewType === 'timeline'));
|
|
84
|
-
assert.ok(views.some((view) => view.viewType === 'account'));
|
|
85
|
-
|
|
86
|
-
const focus = buildAssistantFocusSnapshot(memoryManager, userId, agentId);
|
|
87
|
-
assert.equal(focus.backgroundAwareness.changedCount > 0, true);
|
|
88
|
-
assert.ok(focus.recentKnowledgeChanges.some((change) => change.title === 'Launch review'));
|
|
89
|
-
} finally {
|
|
90
|
-
cleanupUser(userId);
|
|
91
|
-
}
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
test('integration coverage maps connected apps to durable memory domains', () => {
|
|
95
|
-
assert.deepEqual(sourceTypesForConnection('google_workspace', 'gmail'), ['email']);
|
|
96
|
-
assert.deepEqual(sourceTypesForConnection('github', 'repos'), ['repos', 'tickets']);
|
|
97
|
-
assert.deepEqual(sourceTypesForConnection('spotify', 'spotify'), []);
|
|
98
|
-
});
|