neoagent 3.1.1-beta.9 → 3.2.0
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 +2 -2
- package/README.md +5 -1
- package/docs/architecture.md +1 -1
- package/docs/benchmarking.md +68 -87
- package/docs/capabilities.md +1 -1
- package/docs/clients-and-devices.md +3 -4
- package/docs/configuration.md +1 -1
- package/docs/devices.md +1 -2
- package/docs/health.md +24 -0
- package/docs/index.md +2 -2
- package/docs/integrations-architecture.md +6 -0
- package/docs/integrations.md +15 -1
- package/docs/models.md +1 -1
- package/docs/persistence.md +1 -1
- package/docs/why-neoagent.md +1 -1
- package/flutter_app/README.md +0 -5
- package/flutter_app/android/app/src/main/AndroidManifest.xml +0 -14
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/MainActivity.kt +0 -98
- package/flutter_app/lib/main.dart +0 -6
- package/flutter_app/lib/main_app_shell.dart +0 -2
- package/flutter_app/lib/main_controller.dart +2 -867
- package/flutter_app/lib/main_devices.dart +0 -73
- package/flutter_app/lib/main_integrations.dart +339 -187
- package/flutter_app/lib/main_launcher.dart +2 -212
- package/flutter_app/lib/main_models.dart +0 -259
- package/flutter_app/lib/main_navigation.dart +2 -14
- package/flutter_app/lib/main_operations.dart +322 -364
- package/flutter_app/lib/main_runtime.dart +9 -208
- package/flutter_app/lib/main_settings.dart +17 -199
- package/flutter_app/lib/main_shared.dart +20 -591
- package/flutter_app/lib/main_timeline.dart +26 -122
- package/flutter_app/lib/main_voice_assistant.dart +0 -5
- package/flutter_app/lib/src/backend_client.dart +0 -102
- package/flutter_app/lib/src/desktop_companion_io.dart +1 -65
- package/flutter_app/lib/src/desktop_companion_stub.dart +0 -12
- package/flutter_app/linux/flutter/generated_plugin_registrant.cc +0 -4
- package/flutter_app/linux/flutter/generated_plugins.cmake +0 -1
- package/flutter_app/macos/Flutter/GeneratedPluginRegistrant.swift +0 -2
- package/flutter_app/macos/Podfile.lock +56 -5
- package/flutter_app/pubspec.lock +0 -7
- package/flutter_app/pubspec.yaml +0 -2
- package/flutter_app/web/index.html +174 -0
- package/flutter_app/windows/flutter/generated_plugin_registrant.cc +0 -3
- package/flutter_app/windows/flutter/generated_plugins.cmake +0 -1
- package/landing/index.html +0 -2
- package/lib/manager.js +1 -1
- package/lib/schema_migrations.js +75 -0
- package/package.json +1 -4
- package/server/db/database.js +1 -133
- package/server/http/middleware.js +2 -16
- package/server/http/routes.js +0 -2
- package/server/public/.last_build_id +1 -1
- package/server/public/assets/NOTICES +0 -25
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/public/index.html +174 -0
- package/server/public/main.dart.js +88081 -90036
- package/server/routes/auth.js +11 -2
- package/server/routes/settings.js +0 -5
- package/server/routes/voice_assistant.js +1 -128
- package/server/services/account/erasure.js +0 -3
- package/server/services/ai/deliverables/artifact_helpers.js +1 -1
- package/server/services/ai/loop/conversation_loop.js +6 -2
- package/server/services/ai/loop/tool_dispatch.js +0 -3
- package/server/services/ai/messagingFallback.js +1 -1
- package/server/services/ai/settings.js +0 -6
- package/server/services/ai/toolEvidence.js +1 -4
- package/server/services/ai/tools.js +0 -234
- package/server/services/desktop/auth.js +0 -3
- package/server/services/desktop/registry.js +2 -50
- package/server/services/integrations/neoarchive/provider.js +616 -0
- package/server/services/integrations/neorecall/client.js +140 -0
- package/server/services/integrations/neorecall/constants.js +87 -0
- package/server/services/integrations/neorecall/provider.js +163 -0
- package/server/services/integrations/registry.js +4 -2
- package/server/services/manager.js +0 -22
- package/server/services/memory/manager.js +44 -19
- package/server/services/memory/retrieval_reasoning.js +36 -14
- package/server/services/messaging/whatsapp.js +18 -0
- package/server/services/security/tool_categories.js +0 -5
- package/server/services/social_video/service.js +2 -20
- package/server/services/tasks/adapters/index.js +1 -1
- package/server/services/tasks/integration_runtime.js +3 -67
- package/server/services/tasks/runtime.js +1 -6
- package/server/services/tasks/task_repository.js +6 -13
- package/server/services/timeline/service.js +0 -224
- package/server/services/voice/message.js +0 -24
- package/server/services/voice/providers.js +42 -1
- package/server/services/voice/runtimeManager.js +9 -5
- package/server/services/voice/turnRunner.js +0 -22
- package/server/services/wearable/service.js +0 -5
- package/docs/recordings-and-health.md +0 -41
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingBootReceiver.kt +0 -21
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingForegroundService.kt +0 -586
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingStateStore.kt +0 -78
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingUploadClient.kt +0 -104
- package/flutter_app/lib/main_recordings.dart +0 -920
- package/flutter_app/lib/src/desktop_ocr_bridge.dart +0 -2
- package/flutter_app/lib/src/desktop_ocr_bridge_io.dart +0 -125
- package/flutter_app/lib/src/desktop_ocr_bridge_stub.dart +0 -30
- package/flutter_app/lib/src/desktop_passive_history.dart +0 -332
- package/flutter_app/lib/src/recording_bridge.dart +0 -232
- package/flutter_app/lib/src/recording_bridge_io.dart +0 -1027
- package/flutter_app/lib/src/recording_bridge_stub.dart +0 -120
- package/flutter_app/lib/src/recording_bridge_web.dart +0 -702
- package/flutter_app/lib/src/recording_chunk_queue.dart +0 -149
- package/flutter_app/lib/src/recording_chunk_queue_io.dart +0 -182
- package/flutter_app/lib/src/recording_payloads.dart +0 -95
- package/flutter_app/third_party/desktop_audio_capture/LICENSE +0 -21
- package/flutter_app/third_party/desktop_audio_capture/README.md +0 -262
- package/flutter_app/third_party/desktop_audio_capture/lib/audio_capture.dart +0 -65
- package/flutter_app/third_party/desktop_audio_capture/lib/config/mic_audio_config.dart +0 -153
- package/flutter_app/third_party/desktop_audio_capture/lib/config/system_adudio_config.dart +0 -110
- package/flutter_app/third_party/desktop_audio_capture/lib/mic/mic_audio_capture.dart +0 -461
- package/flutter_app/third_party/desktop_audio_capture/lib/model/audio_status.dart +0 -91
- package/flutter_app/third_party/desktop_audio_capture/lib/model/decibel_data.dart +0 -106
- package/flutter_app/third_party/desktop_audio_capture/lib/model/input_device_type.dart +0 -219
- package/flutter_app/third_party/desktop_audio_capture/lib/system/system_audio_capture.dart +0 -336
- package/flutter_app/third_party/desktop_audio_capture/linux/CMakeLists.txt +0 -101
- package/flutter_app/third_party/desktop_audio_capture/linux/audio_capture_plugin.cc +0 -692
- package/flutter_app/third_party/desktop_audio_capture/linux/include/audio_capture/audio_capture_plugin.h +0 -35
- package/flutter_app/third_party/desktop_audio_capture/linux/include/audio_capture/mic_capture_plugin.h +0 -36
- package/flutter_app/third_party/desktop_audio_capture/linux/include/desktop_audio_capture/audio_capture_plugin.h +0 -32
- package/flutter_app/third_party/desktop_audio_capture/linux/include/desktop_audio_capture/mic_capture_plugin.h +0 -32
- package/flutter_app/third_party/desktop_audio_capture/linux/mic_capture_plugin.cc +0 -878
- package/flutter_app/third_party/desktop_audio_capture/macos/Classes/AudioCapturePlugin.swift +0 -27
- package/flutter_app/third_party/desktop_audio_capture/macos/Classes/MicCapturePlugin.swift +0 -1172
- package/flutter_app/third_party/desktop_audio_capture/macos/Classes/SystemCapturePlugin.swift +0 -655
- package/flutter_app/third_party/desktop_audio_capture/macos/Resources/PrivacyInfo.xcprivacy +0 -12
- package/flutter_app/third_party/desktop_audio_capture/macos/desktop_audio_capture.podspec +0 -30
- package/flutter_app/third_party/desktop_audio_capture/pubspec.yaml +0 -87
- package/flutter_app/third_party/desktop_audio_capture/windows/CMakeLists.txt +0 -105
- package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin.cpp +0 -80
- package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin.h +0 -31
- package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin_c_api.cpp +0 -12
- package/flutter_app/third_party/desktop_audio_capture/windows/include/audio_capture/audio_capture_plugin_c_api.h +0 -23
- package/flutter_app/third_party/desktop_audio_capture/windows/include/desktop_audio_capture/audio_capture_plugin.h +0 -25
- package/flutter_app/third_party/desktop_audio_capture/windows/mic_capture_plugin.cpp +0 -1117
- package/flutter_app/third_party/desktop_audio_capture/windows/mic_capture_plugin.h +0 -115
- package/flutter_app/third_party/desktop_audio_capture/windows/system_audio_capture_plugin.cpp +0 -777
- package/flutter_app/third_party/desktop_audio_capture/windows/system_audio_capture_plugin.h +0 -87
- package/server/routes/recordings.js +0 -335
- package/server/routes/screenHistory.js +0 -190
- package/server/services/ai/recordingInsights.js +0 -105
- package/server/services/integrations/neomail/provider.js +0 -993
- package/server/services/recordings/deepgram.js +0 -58
- package/server/services/recordings/manager.js +0 -1273
- package/server/services/tasks/adapters/neomail_email_received.js +0 -46
- package/server/services/voice/screenshotContext.js +0 -73
- package/server/services/voice/turnCoordinator.js +0 -51
|
@@ -0,0 +1,616 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const crypto = require("crypto");
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
const db = require("../../../db/database");
|
|
7
|
+
const { resolveAgentId } = require("../../agents/manager");
|
|
8
|
+
const {
|
|
9
|
+
getProviderConfig,
|
|
10
|
+
setProviderConfig,
|
|
11
|
+
deleteProviderConfig,
|
|
12
|
+
} = require("../provider_config_store");
|
|
13
|
+
const { getConnectionAccessMode } = require("../access");
|
|
14
|
+
const { decryptValue } = require("../secrets");
|
|
15
|
+
const { appendQuery, fetchJson } = require("../oauth_provider");
|
|
16
|
+
const { resolvePublicBaseUrl } = require("../env");
|
|
17
|
+
|
|
18
|
+
const KEY = "neoarchive";
|
|
19
|
+
const APP = Object.freeze({
|
|
20
|
+
id: "archive",
|
|
21
|
+
label: "Archive",
|
|
22
|
+
description:
|
|
23
|
+
"Search, manage, and upload documents in a connected NeoArchive account.",
|
|
24
|
+
});
|
|
25
|
+
const SCOPES = Object.freeze([
|
|
26
|
+
"documents:read",
|
|
27
|
+
"documents:write",
|
|
28
|
+
"search:read",
|
|
29
|
+
"labels:read",
|
|
30
|
+
"authors:read",
|
|
31
|
+
"document-types:read",
|
|
32
|
+
]);
|
|
33
|
+
const TOOLS = Object.freeze(
|
|
34
|
+
[
|
|
35
|
+
{
|
|
36
|
+
name: "neoarchive_list_documents",
|
|
37
|
+
access: "read",
|
|
38
|
+
description: "List documents in NeoArchive.",
|
|
39
|
+
parameters: {
|
|
40
|
+
type: "object",
|
|
41
|
+
properties: {
|
|
42
|
+
status: { type: "string" },
|
|
43
|
+
archived: { type: "boolean" },
|
|
44
|
+
limit: { type: "number" },
|
|
45
|
+
offset: { type: "number" },
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: "neoarchive_search_documents",
|
|
51
|
+
access: "read",
|
|
52
|
+
description: "Search NeoArchive documents by text or semantic query.",
|
|
53
|
+
parameters: {
|
|
54
|
+
type: "object",
|
|
55
|
+
properties: {
|
|
56
|
+
query: { type: "string" },
|
|
57
|
+
mode: { type: "string", enum: ["fulltext", "semantic", "hybrid"] },
|
|
58
|
+
limit: { type: "number" },
|
|
59
|
+
},
|
|
60
|
+
required: ["query"],
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: "neoarchive_get_document",
|
|
65
|
+
access: "read",
|
|
66
|
+
description: "Get NeoArchive document metadata and detail.",
|
|
67
|
+
parameters: {
|
|
68
|
+
type: "object",
|
|
69
|
+
properties: { document_id: { type: "string" } },
|
|
70
|
+
required: ["document_id"],
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
name: "neoarchive_get_document_text",
|
|
75
|
+
access: "read",
|
|
76
|
+
description: "Get extracted or OCR text for a NeoArchive document.",
|
|
77
|
+
parameters: {
|
|
78
|
+
type: "object",
|
|
79
|
+
properties: { document_id: { type: "string" } },
|
|
80
|
+
required: ["document_id"],
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
name: "neoarchive_list_labels",
|
|
85
|
+
access: "read",
|
|
86
|
+
description: "List NeoArchive labels.",
|
|
87
|
+
parameters: { type: "object", properties: {} },
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
name: "neoarchive_list_authors",
|
|
91
|
+
access: "read",
|
|
92
|
+
description: "List NeoArchive authors.",
|
|
93
|
+
parameters: { type: "object", properties: {} },
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
name: "neoarchive_list_document_types",
|
|
97
|
+
access: "read",
|
|
98
|
+
description: "List NeoArchive document types.",
|
|
99
|
+
parameters: { type: "object", properties: {} },
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
name: "neoarchive_upload_document",
|
|
103
|
+
access: "write",
|
|
104
|
+
description: "Upload a readable local file to NeoArchive.",
|
|
105
|
+
parameters: {
|
|
106
|
+
type: "object",
|
|
107
|
+
properties: {
|
|
108
|
+
file_path: {
|
|
109
|
+
type: "string",
|
|
110
|
+
description: "Absolute path to the local file.",
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
required: ["file_path"],
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
name: "neoarchive_update_document",
|
|
118
|
+
access: "write",
|
|
119
|
+
description: "Update NeoArchive document metadata.",
|
|
120
|
+
parameters: {
|
|
121
|
+
type: "object",
|
|
122
|
+
properties: {
|
|
123
|
+
document_id: { type: "string" },
|
|
124
|
+
metadata: { type: "object" },
|
|
125
|
+
},
|
|
126
|
+
required: ["document_id", "metadata"],
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
name: "neoarchive_archive_document",
|
|
131
|
+
access: "write",
|
|
132
|
+
description: "Archive a NeoArchive document.",
|
|
133
|
+
parameters: {
|
|
134
|
+
type: "object",
|
|
135
|
+
properties: { document_id: { type: "string" } },
|
|
136
|
+
required: ["document_id"],
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
name: "neoarchive_restore_document",
|
|
141
|
+
access: "write",
|
|
142
|
+
description: "Restore an archived NeoArchive document.",
|
|
143
|
+
parameters: {
|
|
144
|
+
type: "object",
|
|
145
|
+
properties: { document_id: { type: "string" } },
|
|
146
|
+
required: ["document_id"],
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
name: "neoarchive_reprocess_document",
|
|
151
|
+
access: "write",
|
|
152
|
+
description: "Queue a NeoArchive document for reprocessing.",
|
|
153
|
+
parameters: {
|
|
154
|
+
type: "object",
|
|
155
|
+
properties: {
|
|
156
|
+
document_id: { type: "string" },
|
|
157
|
+
force_ocr: { type: "boolean" },
|
|
158
|
+
},
|
|
159
|
+
required: ["document_id"],
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
].map((tool) => ({ ...tool, appId: APP.id })),
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
function text(value) {
|
|
166
|
+
return String(value || "").trim();
|
|
167
|
+
}
|
|
168
|
+
function isPrivateHost(host) {
|
|
169
|
+
const value = String(host || "").toLowerCase();
|
|
170
|
+
return (
|
|
171
|
+
value === "localhost" ||
|
|
172
|
+
value === "::1" ||
|
|
173
|
+
value.startsWith("127.") ||
|
|
174
|
+
value.startsWith("10.") ||
|
|
175
|
+
value.startsWith("192.168.") ||
|
|
176
|
+
/^172\.(1[6-9]|2\d|3[01])\./.test(value)
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
function normalizeBaseUrl(value) {
|
|
180
|
+
const raw = text(value);
|
|
181
|
+
if (!raw) throw new Error("NeoArchive backend URL is required.");
|
|
182
|
+
const candidate = raw.includes("://")
|
|
183
|
+
? raw
|
|
184
|
+
: `${isPrivateHost(raw.split("/")[0]) ? "http" : "https"}://${raw}`;
|
|
185
|
+
let url;
|
|
186
|
+
try {
|
|
187
|
+
url = new URL(candidate);
|
|
188
|
+
} catch {
|
|
189
|
+
throw new Error(
|
|
190
|
+
"NeoArchive backend URL must be a valid HTTP or HTTPS URL.",
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
if (!["http:", "https:"].includes(url.protocol) || url.hash)
|
|
194
|
+
throw new Error(
|
|
195
|
+
"NeoArchive backend URL must be an HTTP or HTTPS URL without a fragment.",
|
|
196
|
+
);
|
|
197
|
+
url.pathname = url.pathname.replace(/\/+$/, "");
|
|
198
|
+
return url.toString().replace(/\/+$/, "");
|
|
199
|
+
}
|
|
200
|
+
function config(input, existing = {}) {
|
|
201
|
+
return { baseUrl: text(input?.baseUrl) || text(existing.baseUrl) };
|
|
202
|
+
}
|
|
203
|
+
function callbackUrl() {
|
|
204
|
+
return `${resolvePublicBaseUrl()}/api/integrations/oauth/callback`;
|
|
205
|
+
}
|
|
206
|
+
function credentials(connection) {
|
|
207
|
+
try {
|
|
208
|
+
return JSON.parse(
|
|
209
|
+
decryptValue(connection.credentials_json || "{}") || "{}",
|
|
210
|
+
);
|
|
211
|
+
} catch {
|
|
212
|
+
return {};
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
function account(connection, env) {
|
|
216
|
+
return !env.configured
|
|
217
|
+
? {
|
|
218
|
+
id: connection?.id || null,
|
|
219
|
+
status: "env_not_configured",
|
|
220
|
+
connected: false,
|
|
221
|
+
accountEmail: connection?.account_email || null,
|
|
222
|
+
}
|
|
223
|
+
: !connection
|
|
224
|
+
? {
|
|
225
|
+
id: null,
|
|
226
|
+
status: "not_connected",
|
|
227
|
+
connected: false,
|
|
228
|
+
accountEmail: null,
|
|
229
|
+
}
|
|
230
|
+
: {
|
|
231
|
+
id: connection.id,
|
|
232
|
+
status: connection.status,
|
|
233
|
+
connected: connection.status === "connected",
|
|
234
|
+
accountEmail: connection.account_email || null,
|
|
235
|
+
lastConnectedAt: connection.last_connected_at || null,
|
|
236
|
+
accessMode: getConnectionAccessMode(connection),
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
function snapshot(provider, rows, context) {
|
|
240
|
+
const env = provider.getEnvStatus(context);
|
|
241
|
+
const accounts = rows.map((row) => account(row, env));
|
|
242
|
+
const connected = accounts.filter((item) => item.connected);
|
|
243
|
+
const connection = {
|
|
244
|
+
status: !env.configured
|
|
245
|
+
? "env_not_configured"
|
|
246
|
+
: connected.length
|
|
247
|
+
? "connected"
|
|
248
|
+
: "not_connected",
|
|
249
|
+
connected: connected.length > 0,
|
|
250
|
+
accountCount: connected.length,
|
|
251
|
+
accountEmail: connected.length === 1 ? connected[0].accountEmail : null,
|
|
252
|
+
lastConnectedAt:
|
|
253
|
+
connected
|
|
254
|
+
.map((item) => item.lastConnectedAt)
|
|
255
|
+
.filter(Boolean)
|
|
256
|
+
.sort()
|
|
257
|
+
.reverse()[0] || null,
|
|
258
|
+
};
|
|
259
|
+
return {
|
|
260
|
+
id: KEY,
|
|
261
|
+
label: "NeoArchive",
|
|
262
|
+
description: provider.description,
|
|
263
|
+
icon: "neoarchive",
|
|
264
|
+
apps: [
|
|
265
|
+
{
|
|
266
|
+
...APP,
|
|
267
|
+
accounts,
|
|
268
|
+
connection,
|
|
269
|
+
availableToolCount: connection.connected ? TOOLS.length : 0,
|
|
270
|
+
},
|
|
271
|
+
],
|
|
272
|
+
env,
|
|
273
|
+
connection,
|
|
274
|
+
availableToolCount: connection.connected ? TOOLS.length : 0,
|
|
275
|
+
connectPrompt: provider.connectPrompt,
|
|
276
|
+
supportsMultipleAccounts: true,
|
|
277
|
+
connectionMethod: "oauth",
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
async function bootstrap(baseUrl) {
|
|
281
|
+
return fetchJson(
|
|
282
|
+
`${baseUrl}/api/oauth/companion/neoagent/bootstrap`,
|
|
283
|
+
{
|
|
284
|
+
method: "POST",
|
|
285
|
+
json: { redirectUri: callbackUrl(), appName: "NeoAgent" },
|
|
286
|
+
},
|
|
287
|
+
{ serviceName: "NeoArchive companion bootstrap" },
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
async function token(baseUrl, form) {
|
|
291
|
+
return fetchJson(
|
|
292
|
+
`${baseUrl}/oauth/token`,
|
|
293
|
+
{ method: "POST", form },
|
|
294
|
+
{ serviceName: "NeoArchive OAuth token" },
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
async function access(connection) {
|
|
298
|
+
const saved = credentials(connection);
|
|
299
|
+
const baseUrl = normalizeBaseUrl(saved.baseUrl);
|
|
300
|
+
if (saved.access_token && Number(saved.expires_at_ms) > Date.now() + 60000)
|
|
301
|
+
return { baseUrl, accessToken: saved.access_token, credentials: saved };
|
|
302
|
+
if (!saved.refresh_token || !saved.client_id)
|
|
303
|
+
throw new Error(
|
|
304
|
+
"NeoArchive refresh token is missing. Reconnect the NeoArchive account.",
|
|
305
|
+
);
|
|
306
|
+
const refreshed = await token(baseUrl, {
|
|
307
|
+
grant_type: "refresh_token",
|
|
308
|
+
client_id: saved.client_id,
|
|
309
|
+
refresh_token: saved.refresh_token,
|
|
310
|
+
});
|
|
311
|
+
const next = {
|
|
312
|
+
...saved,
|
|
313
|
+
access_token: text(refreshed.access_token),
|
|
314
|
+
refresh_token: text(refreshed.refresh_token) || saved.refresh_token,
|
|
315
|
+
expires_at_ms:
|
|
316
|
+
Date.now() + Math.max(1, Number(refreshed.expires_in) || 3600) * 1000,
|
|
317
|
+
scope: text(refreshed.scope) || saved.scope,
|
|
318
|
+
};
|
|
319
|
+
return { baseUrl, accessToken: next.access_token, credentials: next };
|
|
320
|
+
}
|
|
321
|
+
async function request(connection, apiPath, options = {}) {
|
|
322
|
+
const auth = await access(connection);
|
|
323
|
+
const response = await fetchJson(
|
|
324
|
+
`${auth.baseUrl}${apiPath}`,
|
|
325
|
+
{
|
|
326
|
+
method: options.method || "GET",
|
|
327
|
+
headers: { Authorization: `Bearer ${auth.accessToken}` },
|
|
328
|
+
...(options.json === undefined ? {} : { json: options.json }),
|
|
329
|
+
},
|
|
330
|
+
{ serviceName: "NeoArchive API" },
|
|
331
|
+
);
|
|
332
|
+
return { response, credentials: auth.credentials };
|
|
333
|
+
}
|
|
334
|
+
function required(value, name) {
|
|
335
|
+
const normalized = text(value);
|
|
336
|
+
if (!normalized) throw new Error(`${name} is required.`);
|
|
337
|
+
return normalized;
|
|
338
|
+
}
|
|
339
|
+
async function upload(connection, filePath) {
|
|
340
|
+
const raw = text(filePath);
|
|
341
|
+
if (!raw || raw.split(/[\\/]+/).includes(".."))
|
|
342
|
+
throw new Error(
|
|
343
|
+
"file_path must be an absolute readable file path without parent traversal.",
|
|
344
|
+
);
|
|
345
|
+
const resolved = path.resolve(raw);
|
|
346
|
+
const stats = await fs.promises.stat(resolved);
|
|
347
|
+
if (!stats.isFile())
|
|
348
|
+
throw new Error("file_path must point to a readable file.");
|
|
349
|
+
await fs.promises.access(resolved, fs.constants.R_OK);
|
|
350
|
+
const auth = await access(connection);
|
|
351
|
+
const form = new FormData();
|
|
352
|
+
form.append(
|
|
353
|
+
"files",
|
|
354
|
+
new Blob([await fs.promises.readFile(resolved)]),
|
|
355
|
+
path.basename(resolved),
|
|
356
|
+
);
|
|
357
|
+
const response = await fetch(`${auth.baseUrl}/api/v1/documents`, {
|
|
358
|
+
method: "POST",
|
|
359
|
+
headers: { Authorization: `Bearer ${auth.accessToken}` },
|
|
360
|
+
body: form,
|
|
361
|
+
});
|
|
362
|
+
const result = await response.json().catch(() => null);
|
|
363
|
+
if (!response.ok)
|
|
364
|
+
throw new Error(
|
|
365
|
+
`NeoArchive upload request failed: ${result?.error || response.statusText}`,
|
|
366
|
+
);
|
|
367
|
+
return { result, credentials: auth.credentials };
|
|
368
|
+
}
|
|
369
|
+
async function execute(toolName, args, connection) {
|
|
370
|
+
let pathName;
|
|
371
|
+
let method = "GET";
|
|
372
|
+
let json;
|
|
373
|
+
const query = {};
|
|
374
|
+
switch (toolName) {
|
|
375
|
+
case "neoarchive_list_documents":
|
|
376
|
+
["status", "archived", "limit", "offset"].forEach((key) => {
|
|
377
|
+
if (args[key] !== undefined) query[key] = args[key];
|
|
378
|
+
});
|
|
379
|
+
pathName = appendQuery("/api/v1/documents", query);
|
|
380
|
+
break;
|
|
381
|
+
case "neoarchive_search_documents":
|
|
382
|
+
pathName = appendQuery("/api/v1/search", {
|
|
383
|
+
q: required(args.query, "query"),
|
|
384
|
+
mode: text(args.mode) || undefined,
|
|
385
|
+
limit: args.limit,
|
|
386
|
+
});
|
|
387
|
+
break;
|
|
388
|
+
case "neoarchive_get_document":
|
|
389
|
+
pathName = `/api/v1/documents/${encodeURIComponent(required(args.document_id, "document_id"))}`;
|
|
390
|
+
break;
|
|
391
|
+
case "neoarchive_get_document_text":
|
|
392
|
+
pathName = `/api/v1/documents/${encodeURIComponent(required(args.document_id, "document_id"))}/text`;
|
|
393
|
+
break;
|
|
394
|
+
case "neoarchive_list_labels":
|
|
395
|
+
pathName = "/api/v1/labels";
|
|
396
|
+
break;
|
|
397
|
+
case "neoarchive_list_authors":
|
|
398
|
+
pathName = "/api/v1/authors";
|
|
399
|
+
break;
|
|
400
|
+
case "neoarchive_list_document_types":
|
|
401
|
+
pathName = "/api/v1/document-types";
|
|
402
|
+
break;
|
|
403
|
+
case "neoarchive_upload_document":
|
|
404
|
+
return upload(connection, args.file_path);
|
|
405
|
+
case "neoarchive_update_document":
|
|
406
|
+
pathName = `/api/v1/documents/${encodeURIComponent(required(args.document_id, "document_id"))}`;
|
|
407
|
+
method = "PATCH";
|
|
408
|
+
json =
|
|
409
|
+
args.metadata && typeof args.metadata === "object" ? args.metadata : {};
|
|
410
|
+
break;
|
|
411
|
+
case "neoarchive_archive_document":
|
|
412
|
+
pathName = `/api/v1/documents/${encodeURIComponent(required(args.document_id, "document_id"))}/archive`;
|
|
413
|
+
method = "POST";
|
|
414
|
+
json = {};
|
|
415
|
+
break;
|
|
416
|
+
case "neoarchive_restore_document":
|
|
417
|
+
pathName = `/api/v1/documents/${encodeURIComponent(required(args.document_id, "document_id"))}/restore`;
|
|
418
|
+
method = "POST";
|
|
419
|
+
json = {};
|
|
420
|
+
break;
|
|
421
|
+
case "neoarchive_reprocess_document":
|
|
422
|
+
pathName = `/api/v1/documents/${encodeURIComponent(required(args.document_id, "document_id"))}/reprocess`;
|
|
423
|
+
method = "POST";
|
|
424
|
+
json = { forceOcr: args.force_ocr === true };
|
|
425
|
+
break;
|
|
426
|
+
default:
|
|
427
|
+
throw new Error(`Unsupported NeoArchive tool: ${toolName}`);
|
|
428
|
+
}
|
|
429
|
+
const result = await request(connection, pathName, { method, json });
|
|
430
|
+
return { result: result.response, credentials: result.credentials };
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function createNeoArchiveProvider() {
|
|
434
|
+
return {
|
|
435
|
+
key: KEY,
|
|
436
|
+
label: "NeoArchive",
|
|
437
|
+
description:
|
|
438
|
+
"Connect a self-hosted NeoArchive server to search, organize, and upload documents.",
|
|
439
|
+
icon: "neoarchive",
|
|
440
|
+
apps: [APP],
|
|
441
|
+
connectPrompt:
|
|
442
|
+
"Add the NeoArchive backend URL once, then authorize NeoAgent with OAuth.",
|
|
443
|
+
supportsMultipleAccounts: true,
|
|
444
|
+
connectionMethod: "user_config",
|
|
445
|
+
requiresRefreshToken: true,
|
|
446
|
+
getApp(appId) {
|
|
447
|
+
return text(appId) === APP.id ? APP : null;
|
|
448
|
+
},
|
|
449
|
+
getToolAppId(name) {
|
|
450
|
+
return TOOLS.some((tool) => tool.name === text(name)) ? APP.id : null;
|
|
451
|
+
},
|
|
452
|
+
getEnvStatus(context = {}) {
|
|
453
|
+
const userId = Number(context.userId);
|
|
454
|
+
const stored =
|
|
455
|
+
Number.isInteger(userId) && userId > 0
|
|
456
|
+
? config(getProviderConfig(userId, KEY, context.agentId))
|
|
457
|
+
: { baseUrl: "" };
|
|
458
|
+
const configured = Boolean(stored.baseUrl);
|
|
459
|
+
return {
|
|
460
|
+
configured,
|
|
461
|
+
missing: configured ? [] : ["baseUrl"],
|
|
462
|
+
summary: configured
|
|
463
|
+
? "NeoArchive is ready for account connections."
|
|
464
|
+
: "Add the NeoArchive backend URL to enable account connections.",
|
|
465
|
+
setupMode: "user",
|
|
466
|
+
};
|
|
467
|
+
},
|
|
468
|
+
getToolDefinitions(options = {}) {
|
|
469
|
+
return new Set(options.connectedAppIds || []).has(APP.id)
|
|
470
|
+
? TOOLS.slice()
|
|
471
|
+
: [];
|
|
472
|
+
},
|
|
473
|
+
supportsTool(name) {
|
|
474
|
+
return TOOLS.some((tool) => tool.name === text(name));
|
|
475
|
+
},
|
|
476
|
+
buildSnapshot(rows, context = {}) {
|
|
477
|
+
return snapshot(this, rows, context);
|
|
478
|
+
},
|
|
479
|
+
summarizeForModel(item) {
|
|
480
|
+
return !item?.env?.configured
|
|
481
|
+
? "NeoArchive: setup is not complete yet."
|
|
482
|
+
: !item.connection?.connected
|
|
483
|
+
? "NeoArchive: setup is ready, but no archive account is connected."
|
|
484
|
+
: "NeoArchive: connected with document search, metadata, text, upload, archive, and reprocessing tools.";
|
|
485
|
+
},
|
|
486
|
+
async beginOAuth({ state, codeVerifier, userId, agentId, appKey }) {
|
|
487
|
+
if (text(appKey) !== APP.id) throw new Error("Unknown NeoArchive app.");
|
|
488
|
+
const stored = config(
|
|
489
|
+
getProviderConfig(
|
|
490
|
+
Number(userId),
|
|
491
|
+
KEY,
|
|
492
|
+
resolveAgentId(Number(userId), agentId),
|
|
493
|
+
),
|
|
494
|
+
);
|
|
495
|
+
const baseUrl = normalizeBaseUrl(stored.baseUrl);
|
|
496
|
+
const boot = await bootstrap(baseUrl);
|
|
497
|
+
const challenge = crypto
|
|
498
|
+
.createHash("sha256")
|
|
499
|
+
.update(String(codeVerifier))
|
|
500
|
+
.digest("base64url");
|
|
501
|
+
return {
|
|
502
|
+
url: appendQuery(
|
|
503
|
+
text(boot.authorizationEndpoint) || `${baseUrl}/oauth/authorize`,
|
|
504
|
+
{
|
|
505
|
+
response_type: "code",
|
|
506
|
+
client_id: boot.clientId,
|
|
507
|
+
redirect_uri: text(boot.redirectUri) || callbackUrl(),
|
|
508
|
+
state,
|
|
509
|
+
code_challenge: challenge,
|
|
510
|
+
code_challenge_method: "S256",
|
|
511
|
+
scope: Array.isArray(boot.scopes)
|
|
512
|
+
? boot.scopes.join(" ")
|
|
513
|
+
: SCOPES.join(" "),
|
|
514
|
+
},
|
|
515
|
+
),
|
|
516
|
+
};
|
|
517
|
+
},
|
|
518
|
+
async finishOAuth({ userId, agentId, code, codeVerifier }) {
|
|
519
|
+
const stored = config(
|
|
520
|
+
getProviderConfig(
|
|
521
|
+
Number(userId),
|
|
522
|
+
KEY,
|
|
523
|
+
resolveAgentId(Number(userId), agentId),
|
|
524
|
+
),
|
|
525
|
+
);
|
|
526
|
+
const baseUrl = normalizeBaseUrl(stored.baseUrl);
|
|
527
|
+
const boot = await bootstrap(baseUrl);
|
|
528
|
+
const issued = await token(baseUrl, {
|
|
529
|
+
grant_type: "authorization_code",
|
|
530
|
+
client_id: boot.clientId,
|
|
531
|
+
code: text(code),
|
|
532
|
+
redirect_uri: text(boot.redirectUri) || callbackUrl(),
|
|
533
|
+
code_verifier: text(codeVerifier),
|
|
534
|
+
});
|
|
535
|
+
const accessToken = text(issued.access_token);
|
|
536
|
+
const refreshToken = text(issued.refresh_token);
|
|
537
|
+
if (!accessToken || !refreshToken)
|
|
538
|
+
throw new Error("NeoArchive did not return durable OAuth credentials.");
|
|
539
|
+
const info = await fetchJson(
|
|
540
|
+
`${baseUrl}/oauth/userinfo`,
|
|
541
|
+
{ headers: { Authorization: `Bearer ${accessToken}` } },
|
|
542
|
+
{ serviceName: "NeoArchive userinfo" },
|
|
543
|
+
);
|
|
544
|
+
const host = new URL(baseUrl).host;
|
|
545
|
+
const accountEmail =
|
|
546
|
+
text(info.email) ||
|
|
547
|
+
text(info.preferred_username) ||
|
|
548
|
+
`neoarchive:${host}`;
|
|
549
|
+
return {
|
|
550
|
+
accountEmail,
|
|
551
|
+
scopes: text(issued.scope).split(/\s+/).filter(Boolean),
|
|
552
|
+
credentials: {
|
|
553
|
+
baseUrl,
|
|
554
|
+
client_id: text(boot.clientId),
|
|
555
|
+
access_token: accessToken,
|
|
556
|
+
refresh_token: refreshToken,
|
|
557
|
+
scope: text(issued.scope),
|
|
558
|
+
expires_at_ms:
|
|
559
|
+
Date.now() + Math.max(1, Number(issued.expires_in) || 3600) * 1000,
|
|
560
|
+
},
|
|
561
|
+
metadata: {
|
|
562
|
+
baseUrl,
|
|
563
|
+
username: text(info.preferred_username),
|
|
564
|
+
email: text(info.email) || null,
|
|
565
|
+
},
|
|
566
|
+
};
|
|
567
|
+
},
|
|
568
|
+
async executeTool(toolName, args, connection) {
|
|
569
|
+
return execute(toolName, args || {}, connection);
|
|
570
|
+
},
|
|
571
|
+
getUserConfig({ userId, agentId }) {
|
|
572
|
+
const scoped = resolveAgentId(Number(userId), agentId);
|
|
573
|
+
const stored = config(getProviderConfig(Number(userId), KEY, scoped));
|
|
574
|
+
const accountCount =
|
|
575
|
+
db
|
|
576
|
+
.prepare(
|
|
577
|
+
"SELECT COUNT(*) AS count FROM integration_connections WHERE user_id = ? AND agent_id = ? AND provider_key = ? AND status = 'connected'",
|
|
578
|
+
)
|
|
579
|
+
.get(userId, scoped, KEY)?.count || 0;
|
|
580
|
+
return {
|
|
581
|
+
baseUrl: stored.baseUrl,
|
|
582
|
+
configured: Boolean(stored.baseUrl),
|
|
583
|
+
accountCount,
|
|
584
|
+
hasConnectedAccount: accountCount > 0,
|
|
585
|
+
};
|
|
586
|
+
},
|
|
587
|
+
async saveUserConfig({ userId, agentId, config: input }) {
|
|
588
|
+
const scoped = resolveAgentId(Number(userId), agentId);
|
|
589
|
+
const existing = config(getProviderConfig(Number(userId), KEY, scoped));
|
|
590
|
+
const parsed = config(input, existing);
|
|
591
|
+
const baseUrl = normalizeBaseUrl(parsed.baseUrl);
|
|
592
|
+
await fetchJson(
|
|
593
|
+
`${baseUrl}/api/v1/health`,
|
|
594
|
+
{ method: "GET" },
|
|
595
|
+
{ serviceName: "NeoArchive health check" },
|
|
596
|
+
);
|
|
597
|
+
await bootstrap(baseUrl);
|
|
598
|
+
setProviderConfig(Number(userId), KEY, { baseUrl }, scoped);
|
|
599
|
+
if (existing.baseUrl && existing.baseUrl !== baseUrl)
|
|
600
|
+
db.prepare(
|
|
601
|
+
"DELETE FROM integration_connections WHERE user_id = ? AND agent_id = ? AND provider_key = ?",
|
|
602
|
+
).run(userId, scoped, KEY);
|
|
603
|
+
return this.getUserConfig({ userId, agentId: scoped });
|
|
604
|
+
},
|
|
605
|
+
clearUserConfig({ userId, agentId }) {
|
|
606
|
+
const scoped = resolveAgentId(Number(userId), agentId);
|
|
607
|
+
deleteProviderConfig(Number(userId), KEY, scoped);
|
|
608
|
+
db.prepare(
|
|
609
|
+
"DELETE FROM integration_connections WHERE user_id = ? AND agent_id = ? AND provider_key = ?",
|
|
610
|
+
).run(userId, scoped, KEY);
|
|
611
|
+
return { cleared: true };
|
|
612
|
+
},
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
module.exports = { createNeoArchiveProvider };
|