neoagent 3.2.1-beta.1 → 3.2.1-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. package/extensions/chrome-browser/background.mjs +318 -88
  2. package/extensions/chrome-browser/http.mjs +136 -0
  3. package/extensions/chrome-browser/protocol.mjs +511 -89
  4. package/flutter_app/lib/main_chat.dart +118 -739
  5. package/flutter_app/lib/main_controller.dart +29 -20
  6. package/flutter_app/lib/main_models.dart +3 -0
  7. package/flutter_app/lib/main_operations.dart +334 -321
  8. package/flutter_app/lib/main_settings.dart +4 -3
  9. package/flutter_app/lib/main_shared.dart +14 -11
  10. package/flutter_app/lib/src/desktop_companion_actions.dart +185 -31
  11. package/flutter_app/lib/src/desktop_companion_io.dart +319 -86
  12. package/flutter_app/windows/runner/flutter_window.cpp +143 -32
  13. package/lib/manager.js +106 -89
  14. package/lib/schema_migrations.js +67 -13
  15. package/package.json +20 -13
  16. package/runtime/paths.js +49 -5
  17. package/server/guest_agent.js +52 -30
  18. package/server/http/middleware.js +24 -0
  19. package/server/http/routes.js +4 -6
  20. package/server/public/.last_build_id +1 -1
  21. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  22. package/server/public/flutter_bootstrap.js +1 -1
  23. package/server/public/main.dart.js +30803 -30805
  24. package/server/routes/admin.js +1 -1
  25. package/server/routes/android.js +30 -34
  26. package/server/routes/browser.js +23 -15
  27. package/server/routes/desktop.js +18 -1
  28. package/server/routes/integrations.js +5 -1
  29. package/server/routes/memory.js +1 -0
  30. package/server/routes/settings.js +16 -5
  31. package/server/routes/social_reach.js +12 -3
  32. package/server/routes/social_video.js +4 -0
  33. package/server/services/ai/compaction.js +7 -2
  34. package/server/services/ai/history.js +44 -5
  35. package/server/services/ai/integrated_tools/http_request.js +8 -0
  36. package/server/services/ai/loop/agent_engine_core.js +358 -162
  37. package/server/services/ai/loop/callbacks.js +1 -0
  38. package/server/services/ai/loop/completion_judge.js +12 -0
  39. package/server/services/ai/loop/conversation_loop.js +348 -242
  40. package/server/services/ai/loop/messaging_delivery.js +129 -57
  41. package/server/services/ai/loop/model_call_guard.js +91 -0
  42. package/server/services/ai/loop/model_io.js +20 -45
  43. package/server/services/ai/loop/tool_dispatch.js +19 -8
  44. package/server/services/ai/loopPolicy.js +24 -19
  45. package/server/services/ai/model_discovery.js +227 -0
  46. package/server/services/ai/model_identity.js +71 -0
  47. package/server/services/ai/models.js +67 -162
  48. package/server/services/ai/providerRetry.js +17 -59
  49. package/server/services/ai/provider_selector.js +111 -0
  50. package/server/services/ai/providers/anthropic.js +2 -2
  51. package/server/services/ai/providers/claudeCode.js +21 -33
  52. package/server/services/ai/providers/githubCopilot.js +41 -20
  53. package/server/services/ai/providers/google.js +23 -5
  54. package/server/services/ai/providers/grok.js +4 -3
  55. package/server/services/ai/providers/grokOauth.js +19 -27
  56. package/server/services/ai/providers/nvidia.js +10 -5
  57. package/server/services/ai/providers/ollama.js +111 -84
  58. package/server/services/ai/providers/ollama_stream.js +142 -0
  59. package/server/services/ai/providers/openai.js +39 -5
  60. package/server/services/ai/providers/openaiCodex.js +11 -4
  61. package/server/services/ai/providers/openrouter.js +29 -7
  62. package/server/services/ai/providers/provider_error.js +36 -0
  63. package/server/services/ai/settings.js +26 -2
  64. package/server/services/ai/taskAnalysis.js +5 -1
  65. package/server/services/ai/terminal_reply.js +45 -0
  66. package/server/services/ai/toolEvidence.js +58 -29
  67. package/server/services/ai/tools.js +124 -111
  68. package/server/services/android/controller.js +770 -237
  69. package/server/services/android/process.js +140 -0
  70. package/server/services/android/sdk_download.js +143 -0
  71. package/server/services/android/uia.js +6 -5
  72. package/server/services/artifacts/store.js +24 -0
  73. package/server/services/browser/controller.js +736 -385
  74. package/server/services/browser/extension/gateway.js +40 -16
  75. package/server/services/browser/extension/protocol.js +12 -1
  76. package/server/services/browser/extension/provider.js +59 -47
  77. package/server/services/browser/extension/registry.js +155 -34
  78. package/server/services/cli/executor.js +62 -9
  79. package/server/services/desktop/gateway.js +41 -4
  80. package/server/services/desktop/protocol.js +3 -0
  81. package/server/services/desktop/provider.js +39 -42
  82. package/server/services/desktop/registry.js +137 -52
  83. package/server/services/integrations/figma/provider.js +78 -12
  84. package/server/services/integrations/github/common.js +11 -6
  85. package/server/services/integrations/github/provider.js +52 -53
  86. package/server/services/integrations/google/provider.js +55 -19
  87. package/server/services/integrations/home_assistant/network.js +17 -20
  88. package/server/services/integrations/home_assistant/provider.js +7 -5
  89. package/server/services/integrations/home_assistant/tools.js +17 -5
  90. package/server/services/integrations/http.js +51 -0
  91. package/server/services/integrations/manager.js +158 -53
  92. package/server/services/integrations/microsoft/provider.js +80 -13
  93. package/server/services/integrations/neoarchive/provider.js +55 -29
  94. package/server/services/integrations/neorecall/client.js +17 -10
  95. package/server/services/integrations/neorecall/provider.js +20 -11
  96. package/server/services/integrations/notion/provider.js +16 -13
  97. package/server/services/integrations/oauth_provider.js +115 -51
  98. package/server/services/integrations/slack/provider.js +98 -9
  99. package/server/services/integrations/spotify/provider.js +67 -71
  100. package/server/services/integrations/trello/provider.js +21 -7
  101. package/server/services/integrations/weather/provider.js +18 -12
  102. package/server/services/integrations/whatsapp/provider.js +76 -16
  103. package/server/services/manager.js +87 -1
  104. package/server/services/memory/embedding_index.js +20 -8
  105. package/server/services/memory/embeddings.js +151 -90
  106. package/server/services/memory/ingestion.js +50 -9
  107. package/server/services/memory/ingestion_documents.js +13 -3
  108. package/server/services/memory/manager.js +52 -19
  109. package/server/services/messaging/automation.js +84 -9
  110. package/server/services/messaging/http_platforms.js +33 -13
  111. package/server/services/messaging/inbound_queue.js +78 -24
  112. package/server/services/messaging/inbound_store.js +224 -0
  113. package/server/services/messaging/manager.js +326 -51
  114. package/server/services/messaging/typing_keepalive.js +5 -2
  115. package/server/services/network/http.js +210 -0
  116. package/server/services/network/safe_request.js +307 -0
  117. package/server/services/runtime/backends/local-vm.js +214 -66
  118. package/server/services/runtime/manager.js +17 -12
  119. package/server/services/social_reach/channels/github.js +10 -4
  120. package/server/services/social_reach/channels/reddit.js +4 -4
  121. package/server/services/social_reach/channels/rss.js +2 -2
  122. package/server/services/social_reach/channels/social_video.js +12 -7
  123. package/server/services/social_reach/channels/v2ex.js +21 -8
  124. package/server/services/social_reach/channels/x.js +2 -2
  125. package/server/services/social_reach/channels/xueqiu.js +5 -5
  126. package/server/services/social_reach/service.js +9 -6
  127. package/server/services/social_reach/utils.js +65 -14
  128. package/server/services/social_video/service.js +160 -50
  129. package/server/services/tasks/integration_runtime.js +18 -8
  130. package/server/services/tasks/runtime.js +39 -4
  131. package/server/services/voice/agentBridge.js +17 -4
  132. package/server/services/voice/bufferedLiveRelayAdapter.js +5 -0
  133. package/server/services/voice/liveSession.js +31 -0
  134. package/server/services/voice/openaiSpeech.js +33 -8
  135. package/server/services/voice/providers.js +233 -151
  136. package/server/services/voice/runtimeManager.js +118 -20
  137. package/server/services/voice/turnRunner.js +6 -0
  138. package/server/services/wearable/firmware_manifest.js +51 -13
  139. package/server/services/wearable/service.js +1 -0
  140. package/server/utils/abort.js +96 -0
  141. package/server/utils/cloud-security.js +110 -3
  142. package/server/utils/files.js +31 -0
  143. package/server/utils/image_payload.js +95 -0
  144. package/server/utils/retry.js +107 -0
@@ -0,0 +1,140 @@
1
+ 'use strict';
2
+
3
+ const { spawn } = require('node:child_process');
4
+
5
+ const DEFAULT_TIMEOUT_MS = 20_000;
6
+ const DEFAULT_MAX_OUTPUT_BYTES = 4 * 1024 * 1024;
7
+ const MAX_TIMEOUT_MS = 30 * 60 * 1000;
8
+
9
+ function clampNumber(value, fallback, min, max) {
10
+ const parsed = Number(value);
11
+ if (!Number.isFinite(parsed)) return fallback;
12
+ return Math.min(max, Math.max(min, Math.round(parsed)));
13
+ }
14
+
15
+ function createProcessError(message, code, details = {}) {
16
+ const error = new Error(message);
17
+ error.code = code;
18
+ Object.assign(error, details);
19
+ return error;
20
+ }
21
+
22
+ function runProcess(command, args = [], options = {}) {
23
+ const timeoutMs = clampNumber(options.timeoutMs, DEFAULT_TIMEOUT_MS, 100, MAX_TIMEOUT_MS);
24
+ const maxOutputBytes = clampNumber(
25
+ options.maxOutputBytes,
26
+ DEFAULT_MAX_OUTPUT_BYTES,
27
+ 1024,
28
+ 1024 * 1024 * 1024,
29
+ );
30
+ const encoding = options.encoding === null ? null : (options.encoding || 'utf8');
31
+
32
+ return new Promise((resolve, reject) => {
33
+ const child = spawn(command, args, {
34
+ cwd: options.cwd,
35
+ env: options.env,
36
+ stdio: ['pipe', 'pipe', 'pipe'],
37
+ windowsHide: true,
38
+ });
39
+ const stdout = [];
40
+ const stderr = [];
41
+ let stdoutBytes = 0;
42
+ let stderrBytes = 0;
43
+ let settled = false;
44
+ let forceKillTimer = null;
45
+
46
+ const decode = (chunks) => {
47
+ const buffer = Buffer.concat(chunks);
48
+ return encoding === null ? buffer : buffer.toString(encoding);
49
+ };
50
+ const cleanup = () => {
51
+ clearTimeout(timeout);
52
+ options.signal?.removeEventListener('abort', onAbort);
53
+ };
54
+ const settle = (error, result = null) => {
55
+ if (settled) return;
56
+ settled = true;
57
+ cleanup();
58
+ if (error) reject(error);
59
+ else resolve(result);
60
+ };
61
+ const terminate = (error) => {
62
+ try { child.kill('SIGTERM'); } catch {}
63
+ forceKillTimer = setTimeout(() => {
64
+ try { child.kill('SIGKILL'); } catch {}
65
+ }, 1000);
66
+ forceKillTimer.unref?.();
67
+ settle(error);
68
+ };
69
+ const onAbort = () => {
70
+ const error = options.signal?.reason instanceof Error
71
+ ? options.signal.reason
72
+ : createProcessError('Command was aborted.', 'ABORT_ERR');
73
+ if (!(options.signal?.reason instanceof Error)) error.name = 'AbortError';
74
+ terminate(error);
75
+ };
76
+ const collect = (target, chunk, streamName) => {
77
+ if (settled) return;
78
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
79
+ if (streamName === 'stdout') stdoutBytes += buffer.length;
80
+ else stderrBytes += buffer.length;
81
+ if (stdoutBytes + stderrBytes > maxOutputBytes) {
82
+ terminate(createProcessError(
83
+ `Command exceeded the ${maxOutputBytes}-byte output limit.`,
84
+ 'PROCESS_OUTPUT_LIMIT',
85
+ ));
86
+ return;
87
+ }
88
+ target.push(buffer);
89
+ };
90
+
91
+ const timeout = setTimeout(() => {
92
+ terminate(createProcessError(
93
+ `Command timed out after ${timeoutMs} ms.`,
94
+ 'PROCESS_TIMEOUT',
95
+ { timeoutMs },
96
+ ));
97
+ }, timeoutMs);
98
+ timeout.unref?.();
99
+
100
+ child.stdout?.on('data', (chunk) => collect(stdout, chunk, 'stdout'));
101
+ child.stderr?.on('data', (chunk) => collect(stderr, chunk, 'stderr'));
102
+ child.once('error', (error) => settle(error));
103
+ child.once('close', (exitCode, signal) => {
104
+ if (forceKillTimer) clearTimeout(forceKillTimer);
105
+ if (settled) return;
106
+ const result = {
107
+ stdout: decode(stdout),
108
+ stderr: decode(stderr),
109
+ exitCode,
110
+ signal: signal || null,
111
+ };
112
+ if (exitCode === 0) {
113
+ settle(null, result);
114
+ return;
115
+ }
116
+ const stderrText = Buffer.concat(stderr).toString('utf8').trim();
117
+ const stdoutText = Buffer.concat(stdout).toString('utf8').trim();
118
+ settle(createProcessError(
119
+ stderrText || stdoutText || `Command exited with code ${exitCode}.`,
120
+ 'PROCESS_EXIT',
121
+ result,
122
+ ));
123
+ });
124
+
125
+ if (options.signal?.aborted) {
126
+ onAbort();
127
+ return;
128
+ }
129
+ options.signal?.addEventListener('abort', onAbort, { once: true });
130
+ if (options.input != null) child.stdin?.end(options.input);
131
+ else child.stdin?.end();
132
+ });
133
+ }
134
+
135
+ module.exports = {
136
+ DEFAULT_MAX_OUTPUT_BYTES,
137
+ DEFAULT_TIMEOUT_MS,
138
+ clampNumber,
139
+ runProcess,
140
+ };
@@ -0,0 +1,143 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('node:crypto');
4
+ const fs = require('node:fs');
5
+ const https = require('node:https');
6
+ const os = require('node:os');
7
+ const { Transform } = require('node:stream');
8
+ const { pipeline } = require('node:stream/promises');
9
+ const { createAbortError } = require('../../utils/abort');
10
+ const { clampNumber } = require('./process');
11
+
12
+ // Google publishes the current filenames and checksums at:
13
+ // https://developer.android.com/studio#command-line-tools-only
14
+ const COMMAND_LINE_TOOLS_VERSION = '15859902';
15
+ const COMMAND_LINE_TOOLS_RELEASES = {
16
+ darwin: {
17
+ arm64: {
18
+ url: `https://dl.google.com/android/repository/commandlinetools-mac_arm64-${COMMAND_LINE_TOOLS_VERSION}_latest.zip`,
19
+ sha256: '835b62a26162b229b441d1f6d4680383815a270809eb33522c0d480fa5002c4e',
20
+ },
21
+ x64: {
22
+ url: `https://dl.google.com/android/repository/commandlinetools-mac_x86_64-${COMMAND_LINE_TOOLS_VERSION}_latest.zip`,
23
+ sha256: 'c5a6378ab5cf7e0d5701921405115befff13e9ff7417fb588389338f8bd050f3',
24
+ },
25
+ },
26
+ linux: {
27
+ url: `https://dl.google.com/android/repository/commandlinetools-linux-${COMMAND_LINE_TOOLS_VERSION}_latest.zip`,
28
+ sha256: '4e4c464f145a7512b57d088ac6c278c03c9eea610886b35a5e0804e74eedf583',
29
+ },
30
+ win32: {
31
+ url: `https://dl.google.com/android/repository/commandlinetools-win-${COMMAND_LINE_TOOLS_VERSION}_latest.zip`,
32
+ sha256: '90ae805d20434428bffcb699c290860f19bb5f66a67e6b330067e3de801fb04a',
33
+ },
34
+ };
35
+
36
+ function resolveCommandLineToolsRelease(platform = process.platform, arch = os.arch()) {
37
+ const release = COMMAND_LINE_TOOLS_RELEASES[platform];
38
+ if (!release) throw new Error(`No Android command-line-tools download for platform: ${platform}`);
39
+ if (platform !== 'darwin') return release;
40
+ if (arch !== 'arm64' && arch !== 'x64') {
41
+ throw new Error(`No Android command-line-tools download for macOS architecture: ${arch}`);
42
+ }
43
+ return release[arch];
44
+ }
45
+
46
+ function assertExpectedDigest(actualDigest, expectedDigest) {
47
+ const expected = String(expectedDigest || '').trim().toLowerCase();
48
+ if (!/^[a-f0-9]{64}$/.test(expected)) {
49
+ throw new Error('Android SDK download is missing a valid SHA-256 checksum.');
50
+ }
51
+ const actualBuffer = Buffer.from(actualDigest, 'hex');
52
+ const expectedBuffer = Buffer.from(expected, 'hex');
53
+ if (!crypto.timingSafeEqual(actualBuffer, expectedBuffer)) {
54
+ const error = new Error('Android SDK download failed SHA-256 verification.');
55
+ error.code = 'ANDROID_SDK_CHECKSUM_MISMATCH';
56
+ throw error;
57
+ }
58
+ }
59
+
60
+ async function downloadFile(url, destination, options = {}) {
61
+ const maxRedirects = clampNumber(options.maxRedirects, 5, 0, 10);
62
+ const timeoutMs = clampNumber(options.timeoutMs, 60_000, 1000, 10 * 60 * 1000);
63
+ const maxBytes = clampNumber(options.maxBytes, 500 * 1024 * 1024, 1024, 1024 * 1024 * 1024);
64
+ const temporary = `${destination}.${process.pid}.${Math.random().toString(16).slice(2)}.part`;
65
+
66
+ const openResponse = (target, redirectsLeft) => new Promise((resolve, reject) => {
67
+ if (options.signal?.aborted) {
68
+ reject(createAbortError(options.signal, 'Android SDK download was aborted.'));
69
+ return;
70
+ }
71
+ const parsed = new URL(target);
72
+ if (parsed.protocol !== 'https:') {
73
+ reject(new Error(`Refusing non-HTTPS Android SDK download: ${parsed.protocol}`));
74
+ return;
75
+ }
76
+ const request = https.get(parsed, (response) => {
77
+ options.signal?.removeEventListener('abort', onAbort);
78
+ const status = Number(response.statusCode || 0);
79
+ if (status >= 300 && status < 400 && response.headers.location) {
80
+ response.resume();
81
+ if (redirectsLeft <= 0) {
82
+ reject(new Error('Android SDK download exceeded its redirect limit.'));
83
+ return;
84
+ }
85
+ resolve(openResponse(new URL(response.headers.location, parsed).toString(), redirectsLeft - 1));
86
+ return;
87
+ }
88
+ if (status !== 200) {
89
+ response.resume();
90
+ reject(new Error(`Android SDK download failed with HTTP ${status}.`));
91
+ return;
92
+ }
93
+ const contentLength = Number(response.headers['content-length'] || 0);
94
+ if (contentLength > maxBytes) {
95
+ response.destroy();
96
+ reject(new Error(`Android SDK download exceeds the ${maxBytes}-byte limit.`));
97
+ return;
98
+ }
99
+ resolve(response);
100
+ });
101
+ const onAbort = () => request.destroy(createAbortError(options.signal, 'Android SDK download was aborted.'));
102
+ options.signal?.addEventListener('abort', onAbort, { once: true });
103
+ request.setTimeout(timeoutMs, () => request.destroy(new Error('Android SDK download timed out.')));
104
+ request.once('error', (error) => {
105
+ options.signal?.removeEventListener('abort', onAbort);
106
+ reject(error);
107
+ });
108
+ });
109
+
110
+ try {
111
+ const response = await openResponse(url, maxRedirects);
112
+ const hash = crypto.createHash('sha256');
113
+ let received = 0;
114
+ const verifier = new Transform({
115
+ transform(chunk, _encoding, callback) {
116
+ received += chunk.length;
117
+ if (received > maxBytes) {
118
+ callback(new Error(`Android SDK download exceeds the ${maxBytes}-byte limit.`));
119
+ return;
120
+ }
121
+ hash.update(chunk);
122
+ callback(null, chunk);
123
+ },
124
+ });
125
+ await pipeline(response, verifier, fs.createWriteStream(temporary, { mode: 0o600 }), {
126
+ signal: options.signal,
127
+ });
128
+ assertExpectedDigest(hash.digest('hex'), options.expectedSha256);
129
+ fs.renameSync(temporary, destination);
130
+ } catch (error) {
131
+ if (options.signal?.aborted) {
132
+ throw createAbortError(options.signal, 'Android SDK download was aborted.');
133
+ }
134
+ throw error;
135
+ } finally {
136
+ try { fs.unlinkSync(temporary); } catch {}
137
+ }
138
+ }
139
+
140
+ module.exports = {
141
+ downloadFile,
142
+ resolveCommandLineToolsRelease,
143
+ };
@@ -12,12 +12,13 @@ const DEFAULT_BOUNDS = Object.freeze({
12
12
  function decodeXml(value) {
13
13
  return String(value || '')
14
14
  .replace(/&quot;/g, '"')
15
- .replace(/&amp;/g, '&')
15
+ .replace(/&apos;/g, "'")
16
16
  .replace(/&lt;/g, '<')
17
17
  .replace(/&gt;/g, '>')
18
18
  .replace(/&#10;/g, '\n')
19
19
  .replace(/&#13;/g, '\r')
20
- .replace(/&#39;/g, "'");
20
+ .replace(/&#39;/g, "'")
21
+ .replace(/&amp;/g, '&');
21
22
  }
22
23
 
23
24
  function parseBounds(raw) {
@@ -54,7 +55,7 @@ function parseNodeAttributes(raw) {
54
55
 
55
56
  function parseUiDump(xml) {
56
57
  const nodes = [];
57
- const nodeRe = /<node\b([^>]*)\/>/g;
58
+ const nodeRe = /<node\b([^>]*)>/g;
58
59
  let match = nodeRe.exec(String(xml || ''));
59
60
 
60
61
  while (match) {
@@ -123,8 +124,8 @@ function scoreNode(node, selector = {}) {
123
124
  else return -1;
124
125
  }
125
126
 
126
- if (selector.clickable === true && node.clickable) score += 40;
127
- if (selector.clickable === true && !node.clickable) score -= 60;
127
+ if (selector.clickable === true && !node.clickable) return -1;
128
+ if (selector.clickable === true) score += 40;
128
129
  if (node.enabled) score += 10;
129
130
  if (node.bounds.width > 0 && node.bounds.height > 0) score += 10;
130
131
 
@@ -3,6 +3,7 @@ const path = require('path');
3
3
  const { v4: uuidv4 } = require('uuid');
4
4
  const db = require('../../db/database');
5
5
  const { DATA_DIR } = require('../../../runtime/paths');
6
+ const { writeBufferAtomic } = require('../../utils/files');
6
7
 
7
8
  const ARTIFACTS_ROOT = path.join(DATA_DIR, 'artifacts');
8
9
  fs.mkdirSync(ARTIFACTS_ROOT, { recursive: true });
@@ -121,6 +122,29 @@ class ArtifactStore {
121
122
  };
122
123
  }
123
124
 
125
+ async createBufferArtifact(userId, options = {}) {
126
+ const buffer = Buffer.isBuffer(options.content)
127
+ ? options.content
128
+ : Buffer.from(options.content || []);
129
+ const allocation = this.allocateFile(userId, options);
130
+ try {
131
+ await writeBufferAtomic(allocation.storagePath, buffer, {
132
+ signal: options.signal,
133
+ });
134
+ db.prepare('UPDATE artifacts SET byte_size = ? WHERE id = ?')
135
+ .run(buffer.length, allocation.artifactId);
136
+ return {
137
+ ...allocation,
138
+ filePath: allocation.storagePath,
139
+ byteSize: buffer.length,
140
+ };
141
+ } catch (error) {
142
+ await fs.promises.rm(allocation.storagePath, { force: true }).catch(() => {});
143
+ db.prepare('DELETE FROM artifacts WHERE id = ?').run(allocation.artifactId);
144
+ throw error;
145
+ }
146
+ }
147
+
124
148
  getArtifactForUser(userId, artifactId) {
125
149
  const row = db.prepare(`
126
150
  SELECT id, user_id, kind, backend, content_type, storage_path, original_filename, byte_size, metadata_json, created_at