neoagent 2.3.1-beta.95 → 2.3.1-beta.97

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neoagent",
3
- "version": "2.3.1-beta.95",
3
+ "version": "2.3.1-beta.97",
4
4
  "description": "Proactive personal AI agent with no limits",
5
5
  "license": "MIT",
6
6
  "main": "server/index.js",
@@ -1 +1 @@
1
- e36f6379ecffed1205851d8afac54166
1
+ e299764c07bc83e074bc914963bc6b6a
@@ -37,6 +37,6 @@ _flutter.buildConfig = {"engineRevision":"42d3d75a56efe1a2e9902f52dc8006099c45d9
37
37
 
38
38
  _flutter.loader.load({
39
39
  serviceWorkerSettings: {
40
- serviceWorkerVersion: "1191180070" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
40
+ serviceWorkerVersion: "818064985" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
41
41
  }
42
42
  });
@@ -129338,7 +129338,7 @@ r===$&&A.b()
129338
129338
  o.push(A.ii(p,A.iY(!1,new A.a3(B.tM,A.dT(new A.cI(B.he,new A.a5V(r,p),p),p,p),p),!1,B.I,!0),p,p,0,0,0,p))}r=!1
129339
129339
  if(!s.ay)if(!s.ch){r=s.e
129340
129340
  r===$&&A.b()
129341
- r=B.b.t("mp8lujhh-8aedfd7").length!==0&&r.b}if(r){r=s.d
129341
+ r=B.b.t("mp9vo0j5-aadf22e").length!==0&&r.b}if(r){r=s.d
129342
129342
  r===$&&A.b()
129343
129343
  r=r.ag&&!r.V?84:0
129344
129344
  q=s.e
@@ -134146,7 +134146,7 @@ $S:236}
134146
134146
  A.Ys.prototype={}
134147
134147
  A.Rr.prototype={
134148
134148
  mT(a){var s=this
134149
- if(B.b.t("mp8lujhh-8aedfd7").length===0||s.a!=null)return
134149
+ if(B.b.t("mp9vo0j5-aadf22e").length===0||s.a!=null)return
134150
134150
  s.A5()
134151
134151
  s.a=A.q1(B.PP,new A.b58(s))},
134152
134152
  A5(){var s=0,r=A.l(t.H),q,p=2,o=[],n=this,m,l,k,j,i,h,g,f
@@ -134164,7 +134164,7 @@ if(!t.f.b(k)){s=1
134164
134164
  break}i=J.Z(k,"buildId")
134165
134165
  h=i==null?null:B.b.t(J.r(i))
134166
134166
  j=h==null?"":h
134167
- if(J.bm(j)===0||J.d(j,"mp8lujhh-8aedfd7")){s=1
134167
+ if(J.bm(j)===0||J.d(j,"mp9vo0j5-aadf22e")){s=1
134168
134168
  break}n.b=!0
134169
134169
  n.D()
134170
134170
  p=2
@@ -134181,7 +134181,7 @@ case 2:return A.i(o.at(-1),r)}})
134181
134181
  return A.k($async$A5,r)},
134182
134182
  vb(){var s=0,r=A.l(t.H),q,p=2,o=[],n=this,m,l,k,j,i,h,g,f,e,d,c,b,a,a0,a1
134183
134183
  var $async$vb=A.h(function(a2,a3){if(a2===1){o.push(a3)
134184
- s=p}for(;;)switch(s){case 0:if(B.b.t("mp8lujhh-8aedfd7").length===0||n.c){s=1
134184
+ s=p}for(;;)switch(s){case 0:if(B.b.t("mp9vo0j5-aadf22e").length===0||n.c){s=1
134185
134185
  break}n.c=!0
134186
134186
  n.D()
134187
134187
  p=4
@@ -66,6 +66,19 @@ router.get('/status', async (req, res) => {
66
66
  }
67
67
  });
68
68
 
69
+ router.get('/cookies', async (req, res) => {
70
+ try {
71
+ const bc = await getBrowserController(req);
72
+ if (typeof bc.getCookies !== 'function') {
73
+ return res.status(501).json({ error: 'Cookie export is unavailable for this browser provider.' });
74
+ }
75
+ const result = await bc.getCookies();
76
+ res.json(result);
77
+ } catch (err) {
78
+ res.status(500).json({ error: sanitizeError(err) });
79
+ }
80
+ });
81
+
69
82
  // Launch browser
70
83
  router.post('/launch', async (req, res) => {
71
84
  try {
@@ -310,8 +310,8 @@ function getCommandHealth(userId, app, engine) {
310
310
  configured: Boolean(runtimeManager),
311
311
  healthy: Boolean(runtimeManager),
312
312
  summary: runtimeManager
313
- ? 'Shell command execution is available through the per-user runtime capsule.'
314
- : 'Shell executor is not available.',
313
+ ? 'Shell command execution is available.'
314
+ : 'Shell command execution is not available in this environment.',
315
315
  });
316
316
  }
317
317
 
@@ -31,14 +31,14 @@ const STATIC_MODELS = [
31
31
  purpose: 'coding'
32
32
  },
33
33
  {
34
- id: 'gpt-5.3-codex',
35
- label: 'GPT-5.3 (Codex Default)',
34
+ id: 'gpt-5.4',
35
+ label: 'GPT-5.4 (Codex Default)',
36
36
  provider: 'openai-codex',
37
37
  purpose: 'general'
38
38
  },
39
39
  {
40
- id: 'gpt-4.1-codex',
41
- label: 'GPT-4.1 (Codex Fast)',
40
+ id: 'gpt-5.4-mini',
41
+ label: 'GPT-5.4 mini (Codex Fast)',
42
42
  provider: 'openai-codex',
43
43
  purpose: 'coding'
44
44
  },
@@ -1,31 +1,330 @@
1
- const { OpenAIProvider } = require('./openai');
1
+ const OpenAI = require('openai');
2
+ const { BaseProvider } = require('./base');
2
3
 
3
- class OpenAICodexProvider extends OpenAIProvider {
4
+ const DEFAULT_BASE_URL = 'https://chatgpt.com/backend-api/codex';
5
+
6
+ function normalizeContent(content) {
7
+ if (content == null) return '';
8
+ if (typeof content === 'string') return content;
9
+ if (Array.isArray(content)) {
10
+ return content.map((part) => {
11
+ if (!part) return '';
12
+ if (typeof part === 'string') return part;
13
+ if (part.type === 'text' && typeof part.text === 'string') return part.text;
14
+ if (part.type === 'input_text' && typeof part.text === 'string') return part.text;
15
+ return '';
16
+ }).join('');
17
+ }
18
+ return String(content);
19
+ }
20
+
21
+ function normalizeInputContent(content) {
22
+ if (content == null) return [];
23
+
24
+ if (typeof content === 'string') {
25
+ return [{ type: 'input_text', text: content }];
26
+ }
27
+
28
+ if (!Array.isArray(content)) {
29
+ const text = String(content);
30
+ return text ? [{ type: 'input_text', text }] : [];
31
+ }
32
+
33
+ const parts = [];
34
+ for (const part of content) {
35
+ if (!part) continue;
36
+ if (typeof part === 'string') {
37
+ if (part.trim()) parts.push({ type: 'input_text', text: part });
38
+ continue;
39
+ }
40
+ if (part.type === 'text' && typeof part.text === 'string') {
41
+ parts.push({ type: 'input_text', text: part.text });
42
+ continue;
43
+ }
44
+ if (part.type === 'input_text' && typeof part.text === 'string') {
45
+ parts.push({ type: 'input_text', text: part.text });
46
+ continue;
47
+ }
48
+ if (part.type === 'image_url') {
49
+ const imageUrl = typeof part.image_url === 'string' ? part.image_url : part.image_url?.url;
50
+ if (imageUrl) {
51
+ parts.push({
52
+ type: 'input_image',
53
+ image_url: imageUrl,
54
+ detail: part.detail || 'auto',
55
+ });
56
+ }
57
+ continue;
58
+ }
59
+ if (part.type === 'input_image') {
60
+ parts.push({
61
+ type: 'input_image',
62
+ image_url: part.image_url || null,
63
+ file_id: part.file_id || null,
64
+ detail: part.detail || 'auto',
65
+ });
66
+ }
67
+ }
68
+
69
+ return parts;
70
+ }
71
+
72
+ function toFunctionCallOutput(toolCallId, content) {
73
+ return {
74
+ type: 'function_call_output',
75
+ call_id: toolCallId,
76
+ output: normalizeContent(content),
77
+ };
78
+ }
79
+
80
+ function extractResponseText(response) {
81
+ if (typeof response?.output_text === 'string' && response.output_text.length > 0) {
82
+ return response.output_text;
83
+ }
84
+
85
+ const parts = [];
86
+ for (const item of response?.output || []) {
87
+ if (item?.type !== 'message') continue;
88
+ for (const content of item.content || []) {
89
+ if (content?.type === 'output_text' && typeof content.text === 'string') {
90
+ parts.push(content.text);
91
+ }
92
+ }
93
+ }
94
+ return parts.join('');
95
+ }
96
+
97
+ function extractToolCalls(response) {
98
+ const toolCalls = [];
99
+ for (const item of response?.output || []) {
100
+ if (item?.type !== 'function_call') continue;
101
+ toolCalls.push({
102
+ id: item.call_id || item.id || '',
103
+ type: 'function',
104
+ function: {
105
+ name: item.name || '',
106
+ arguments: item.arguments || '',
107
+ },
108
+ });
109
+ }
110
+ return toolCalls.filter((toolCall) => toolCall.id && toolCall.function.name);
111
+ }
112
+
113
+ function formatOpenAIError(err) {
114
+ if (!err || typeof err !== 'object') return 'Unknown OpenAI error';
115
+ const parts = [];
116
+ if (typeof err.status === 'number') parts.push(`HTTP ${err.status}`);
117
+ if (err.type) parts.push(`type=${err.type}`);
118
+ if (err.code) parts.push(`code=${err.code}`);
119
+ if (err.param) parts.push(`param=${err.param}`);
120
+ if (err.request_id) parts.push(`request_id=${err.request_id}`);
121
+ const message = err.message || 'Unknown OpenAI error';
122
+ return parts.length > 0 ? `${message} (${parts.join(', ')})` : message;
123
+ }
124
+
125
+ class OpenAICodexProvider extends BaseProvider {
4
126
  constructor(config = {}) {
5
- const officialBaseUrl = 'https://api.openai.com/v1';
6
- const baseUrl = config.baseUrl || process.env.OPENAI_CODEX_BASE_URL || 'https://chatgpt.com/backend-api/codex';
7
-
8
- if (!baseUrl.includes('api.openai.com') && !baseUrl.includes('chatgpt.com')) {
9
- console.warn(`[OpenAICodex] Using non-official base URL: ${baseUrl}`);
10
- } else if (baseUrl.includes('chatgpt.com')) {
11
- console.info(`[OpenAICodex] Using ChatGPT subscription endpoint: ${baseUrl}`);
127
+ super(config);
128
+
129
+ const baseURL = config.baseUrl || process.env.OPENAI_CODEX_BASE_URL || DEFAULT_BASE_URL;
130
+
131
+ if (!baseURL.includes('chatgpt.com/backend-api/codex') && !baseURL.includes('api.openai.com')) {
132
+ console.warn(`[OpenAICodex] Using non-official base URL: ${baseURL}`);
133
+ } else if (baseURL.includes('chatgpt.com/backend-api/codex')) {
134
+ console.info(`[OpenAICodex] Using ChatGPT Codex endpoint: ${baseURL}`);
12
135
  }
13
136
 
14
- super({
15
- ...config,
137
+ this.name = 'openai-codex';
138
+ this.models = [
139
+ 'gpt-5.4',
140
+ 'gpt-5.4-mini',
141
+ ];
142
+ this.reasoningModels = new Set([
143
+ 'gpt-5.4',
144
+ 'gpt-5.4-mini',
145
+ 'gpt-5.4-nano',
146
+ ]);
147
+ this.client = new OpenAI({
16
148
  apiKey: config.apiKey || process.env.OPENAI_CODEX_ACCESS_TOKEN,
17
- baseUrl,
149
+ baseURL,
18
150
  defaultHeaders: {
19
151
  'Editor-Version': process.env.OPENAI_CODEX_EDITOR_VERSION || 'vscode/1.99.0',
20
152
  'Editor-Plugin-Version': process.env.OPENAI_CODEX_EDITOR_PLUGIN_VERSION || 'neoagent/1.0.0',
21
- 'User-Agent': process.env.OPENAI_CODEX_USER_AGENT || 'NeoAgent/1.0.0'
22
- }
153
+ 'User-Agent': process.env.OPENAI_CODEX_USER_AGENT || 'NeoAgent/1.0.0',
154
+ },
23
155
  });
24
- this.name = 'openai-codex';
25
156
  }
26
157
 
27
- // OpenAI Codex (subscription-based) uses the OAuth token directly as the API key.
28
- // The base URL routes it through the ChatGPT backend-api.
158
+ _isReasoningModel(model) {
159
+ if (!model) return false;
160
+ for (const id of this.reasoningModels) {
161
+ if (model === id || model.startsWith(`${id}-`)) return true;
162
+ }
163
+ return false;
164
+ }
165
+
166
+ _buildRequest(messages = [], tools = [], options = {}, model = '') {
167
+ const instructions = [];
168
+ const input = [];
169
+
170
+ for (const msg of messages || []) {
171
+ if (!msg || !msg.role) continue;
172
+
173
+ if ((msg.role === 'system' || msg.role === 'developer') && msg.content != null) {
174
+ const text = normalizeContent(msg.content).trim();
175
+ if (text) instructions.push(text);
176
+ continue;
177
+ }
178
+
179
+ if (msg.role === 'tool') {
180
+ const toolCallId = String(msg.tool_call_id || '').trim();
181
+ if (!toolCallId) continue;
182
+ input.push(toFunctionCallOutput(toolCallId, msg.content));
183
+ continue;
184
+ }
185
+
186
+ const content = normalizeInputContent(msg.content);
187
+ if (content.length > 0) {
188
+ input.push({
189
+ type: 'message',
190
+ role: msg.role === 'assistant' ? 'assistant' : 'user',
191
+ content,
192
+ });
193
+ }
194
+
195
+ if (msg.role === 'assistant' && Array.isArray(msg.tool_calls) && msg.tool_calls.length > 0) {
196
+ for (const toolCall of msg.tool_calls) {
197
+ const name = String(toolCall?.function?.name || '').trim();
198
+ const argumentsText = String(toolCall?.function?.arguments || '');
199
+ const callId = String(toolCall?.id || toolCall?.call_id || '').trim();
200
+ if (!name || !callId) continue;
201
+ input.push({
202
+ type: 'function_call',
203
+ id: callId,
204
+ call_id: callId,
205
+ name,
206
+ arguments: argumentsText,
207
+ });
208
+ }
209
+ }
210
+ }
211
+
212
+ const request = {
213
+ input,
214
+ };
215
+
216
+ if (instructions.length > 0) {
217
+ request.instructions = instructions.join('\n\n');
218
+ }
219
+
220
+ if (tools && tools.length > 0) {
221
+ request.tools = this.formatTools(tools);
222
+ request.tool_choice = options.toolChoice || 'auto';
223
+ }
224
+
225
+ request.max_output_tokens = options.maxTokens || 16384;
226
+
227
+ if (options.temperature !== undefined && options.temperature !== null) {
228
+ request.temperature = options.temperature;
229
+ }
230
+
231
+ const reasoningEffort = options.reasoningEffort || options.reasoning_effort;
232
+ if (reasoningEffort || this._isReasoningModel(model)) {
233
+ request.reasoning = {
234
+ effort: reasoningEffort || 'medium',
235
+ };
236
+ }
237
+
238
+ return request;
239
+ }
240
+
241
+ async chat(messages, tools = [], options = {}) {
242
+ const model = options.model || this.config.model || this.getDefaultModel();
243
+ const request = this._buildRequest(messages, tools, options, model);
244
+ let response;
245
+ try {
246
+ response = await this.client.responses.create({
247
+ model,
248
+ ...request,
249
+ });
250
+ } catch (err) {
251
+ throw new Error(`OpenAI Codex request failed: ${formatOpenAIError(err)}`);
252
+ }
253
+
254
+ const toolCalls = extractToolCalls(response);
255
+
256
+ return {
257
+ content: extractResponseText(response),
258
+ toolCalls,
259
+ finishReason: toolCalls.length > 0 ? 'tool_calls' : 'stop',
260
+ usage: response.usage ? {
261
+ promptTokens: response.usage.input_tokens,
262
+ completionTokens: response.usage.output_tokens,
263
+ totalTokens: response.usage.total_tokens,
264
+ } : null,
265
+ model: response.model,
266
+ };
267
+ }
268
+
269
+ async *stream(messages, tools = [], options = {}) {
270
+ const model = options.model || this.config.model || this.getDefaultModel();
271
+ const request = this._buildRequest(messages, tools, options, model);
272
+ let stream;
273
+ try {
274
+ stream = await this.client.responses.create({
275
+ model,
276
+ ...request,
277
+ stream: true,
278
+ });
279
+ } catch (err) {
280
+ throw new Error(`OpenAI Codex request failed: ${formatOpenAIError(err)}`);
281
+ }
282
+
283
+ let content = '';
284
+ let finalResponse = null;
285
+
286
+ for await (const event of stream) {
287
+ if (event.type === 'response.output_text.delta' && typeof event.delta === 'string') {
288
+ content += event.delta;
289
+ yield { type: 'content', content: event.delta };
290
+ continue;
291
+ }
292
+
293
+ if (event.type === 'response.completed') {
294
+ finalResponse = event.response;
295
+ }
296
+ }
297
+
298
+ const response = finalResponse || {};
299
+ const toolCalls = extractToolCalls(response);
300
+ const finalContent = extractResponseText(response) || content;
301
+
302
+ if (toolCalls.length > 0) {
303
+ yield {
304
+ type: 'tool_calls',
305
+ content: finalContent,
306
+ toolCalls,
307
+ usage: response.usage ? {
308
+ promptTokens: response.usage.input_tokens,
309
+ completionTokens: response.usage.output_tokens,
310
+ totalTokens: response.usage.total_tokens,
311
+ } : null,
312
+ };
313
+ return;
314
+ }
315
+
316
+ yield {
317
+ type: 'done',
318
+ content: finalContent,
319
+ toolCalls: [],
320
+ finishReason: 'stop',
321
+ usage: response.usage ? {
322
+ promptTokens: response.usage.input_tokens,
323
+ completionTokens: response.usage.output_tokens,
324
+ totalTokens: response.usage.total_tokens,
325
+ } : null,
326
+ };
327
+ }
29
328
  }
30
329
 
31
330
  module.exports = { OpenAICodexProvider };
@@ -77,7 +77,7 @@ const AI_PROVIDER_DEFINITIONS = Object.freeze({
77
77
  supportsApiKey: true,
78
78
  supportsBaseUrl: true,
79
79
  defaultEnabled: false,
80
- defaultBaseUrl: 'https://api.openai.com/v1'
80
+ defaultBaseUrl: 'https://chatgpt.com/backend-api/codex'
81
81
  },
82
82
  ollama: {
83
83
  id: 'ollama',
@@ -779,6 +779,17 @@ class BrowserController {
779
779
  };
780
780
  }
781
781
 
782
+ async getCookies() {
783
+ await this.ensureBrowser();
784
+ if (!this.context || typeof this.context.cookies !== 'function') {
785
+ return { cookies: [] };
786
+ }
787
+ const cookies = await this.context.cookies().catch(() => []);
788
+ return {
789
+ cookies: Array.isArray(cookies) ? cookies : [],
790
+ };
791
+ }
792
+
782
793
  async close() {
783
794
  if (this.page && !this.page.isClosed()) {
784
795
  await this.page.close().catch(() => { });
@@ -272,6 +272,9 @@ class VmBrowserProvider {
272
272
  const status = await this.client.request('GET', '/browser/status');
273
273
  return Number(status?.pages || 0);
274
274
  }
275
+ async getCookies() {
276
+ return this.client.request('GET', '/browser/cookies');
277
+ }
275
278
  async setHeadless(value) {
276
279
  this.headless = true;
277
280
  return { success: true };
@@ -126,6 +126,35 @@ function resolveVoiceSttConfigFromSettings(settings = {}) {
126
126
  };
127
127
  }
128
128
 
129
+ function serializeCookiesForNetscapeJar(cookies = []) {
130
+ const lines = ['# Netscape HTTP Cookie File'];
131
+ for (const cookie of Array.isArray(cookies) ? cookies : []) {
132
+ if (!cookie || typeof cookie !== 'object') continue;
133
+ const domain = String(cookie.domain || '').trim();
134
+ const name = String(cookie.name || '').trim();
135
+ const value = String(cookie.value || '').replace(/[\r\n\t]/g, ' ');
136
+ if (!domain || !name) continue;
137
+ const cookieDomain = domain.startsWith('.') ? domain : domain;
138
+ const includeSubdomains = domain.startsWith('.') ? 'TRUE' : 'FALSE';
139
+ const pathValue = String(cookie.path || '/').trim() || '/';
140
+ const secure = cookie.secure ? 'TRUE' : 'FALSE';
141
+ const expires = Number.isFinite(Number(cookie.expires)) && Number(cookie.expires) > 0
142
+ ? String(Math.floor(Number(cookie.expires)))
143
+ : '0';
144
+ const httpOnlyPrefix = cookie.httpOnly ? '#HttpOnly_' : '';
145
+ lines.push([
146
+ `${httpOnlyPrefix}${cookieDomain}`,
147
+ includeSubdomains,
148
+ pathValue,
149
+ secure,
150
+ expires,
151
+ name,
152
+ value,
153
+ ].join('\t'));
154
+ }
155
+ return `${lines.join('\n')}\n`;
156
+ }
157
+
129
158
  function fileExists(filePath) {
130
159
  try {
131
160
  return fs.statSync(filePath).isFile();
@@ -240,8 +269,14 @@ class SocialVideoService {
240
269
 
241
270
  const pageMetadata = await this.#resolvePageMetadata(userId, normalizedUrl, warnings);
242
271
  jobDir = await fsp.mkdtemp(path.join(SOCIAL_VIDEO_TMP_DIR, `${platform}-${Date.now()}-`));
272
+ const cookieFilePath = await this.#resolveCookieFile({
273
+ userId,
274
+ platform,
275
+ jobDir,
276
+ warnings,
277
+ });
243
278
 
244
- const mediaInfo = await this.#readMediaInfo(normalizedUrl, jobDir);
279
+ const mediaInfo = await this.#readMediaInfo(normalizedUrl, jobDir, cookieFilePath);
245
280
  const baseTitle = String(pageMetadata.title || mediaInfo.title || '').trim();
246
281
  const baseDescription = String(pageMetadata.description || mediaInfo.description || '').trim();
247
282
  const resolvedUrl = String(pageMetadata.resolvedUrl || mediaInfo.webpage_url || normalizedUrl).trim();
@@ -264,6 +299,7 @@ class SocialVideoService {
264
299
  captionTrack,
265
300
  transcriptDecision,
266
301
  jobDir,
302
+ cookieFilePath,
267
303
  userId,
268
304
  agentId,
269
305
  warnings,
@@ -276,6 +312,7 @@ class SocialVideoService {
276
312
  sourceUrl: normalizedUrl,
277
313
  mediaInfo,
278
314
  jobDir,
315
+ cookieFilePath,
279
316
  warnings,
280
317
  });
281
318
 
@@ -432,10 +469,11 @@ class SocialVideoService {
432
469
  };
433
470
  }
434
471
 
435
- async #readMediaInfo(normalizedUrl, jobDir) {
472
+ async #readMediaInfo(normalizedUrl, jobDir, cookieFilePath = null) {
436
473
  const infoTemplate = path.join(jobDir, 'media.%(ext)s');
437
474
  const infoPath = path.join(jobDir, 'media.info.json');
438
- const command = `${shellEscape(this.ytDlpBin)} --quiet --no-warnings --no-playlist --skip-download --write-info-json --no-clean-infojson -o ${shellEscape(infoTemplate)} -- ${shellEscape(normalizedUrl)}`;
475
+ const cookieArg = cookieFilePath ? ` --cookies ${shellEscape(cookieFilePath)}` : '';
476
+ const command = `${shellEscape(this.ytDlpBin)} --quiet --no-warnings --no-playlist --skip-download --write-info-json --no-clean-infojson${cookieArg} -o ${shellEscape(infoTemplate)} -- ${shellEscape(normalizedUrl)}`;
439
477
  await this.#runCommand(command, { cwd: jobDir, timeout: 4 * 60 * 1000 });
440
478
  if (!fileExists(infoPath)) {
441
479
  throw new Error('yt-dlp did not produce an info JSON artifact.');
@@ -486,7 +524,8 @@ class SocialVideoService {
486
524
 
487
525
  async #transcribeViaStt(context) {
488
526
  const template = path.join(context.jobDir, 'audio.%(ext)s');
489
- const command = `${shellEscape(this.ytDlpBin)} --quiet --no-warnings --no-playlist -o ${shellEscape(template)} -f bestaudio -- ${shellEscape(context.sourceUrl)}`;
527
+ const cookieArg = context.cookieFilePath ? ` --cookies ${shellEscape(context.cookieFilePath)}` : '';
528
+ const command = `${shellEscape(this.ytDlpBin)} --quiet --no-warnings --no-playlist${cookieArg} -o ${shellEscape(template)} -f bestaudio -- ${shellEscape(context.sourceUrl)}`;
490
529
  await this.#runCommand(command, { cwd: context.jobDir, timeout: 10 * 60 * 1000 });
491
530
 
492
531
  const audioPath = firstFileMatching(context.jobDir, 'audio.');
@@ -521,6 +560,36 @@ class SocialVideoService {
521
560
  });
522
561
  }
523
562
 
563
+ async #resolveCookieFile(context) {
564
+ if (context.platform !== 'instagram') {
565
+ return null;
566
+ }
567
+ if (!this.runtimeManager || typeof this.runtimeManager.getBrowserProviderForUser !== 'function') {
568
+ return null;
569
+ }
570
+
571
+ const browser = await Promise.resolve(
572
+ this.runtimeManager.getBrowserProviderForUser(context.userId),
573
+ ).catch(() => null);
574
+ if (!browser || typeof browser.getCookies !== 'function') {
575
+ return null;
576
+ }
577
+
578
+ const payload = await browser.getCookies().catch((error) => {
579
+ context.warnings.push(`Browser cookie export failed: ${error.message}`);
580
+ return null;
581
+ });
582
+ const cookies = Array.isArray(payload?.cookies) ? payload.cookies : [];
583
+ if (cookies.length === 0) {
584
+ context.warnings.push('Browser cookie export returned no cookies for Instagram.');
585
+ return null;
586
+ }
587
+
588
+ const cookieFilePath = path.join(context.jobDir, 'browser.cookies.txt');
589
+ await fsp.writeFile(cookieFilePath, serializeCookiesForNetscapeJar(cookies), 'utf8');
590
+ return cookieFilePath;
591
+ }
592
+
524
593
  async #resolveFrameImage(context) {
525
594
  const downloadedFrame = await this.#extractFrameFromVideo(context).catch((error) => {
526
595
  context.warnings.push(`Frame extraction failed: ${error.message}`);
@@ -540,7 +609,8 @@ class SocialVideoService {
540
609
 
541
610
  async #extractFrameFromVideo(context) {
542
611
  const template = path.join(context.jobDir, 'video.%(ext)s');
543
- const downloadCommand = `${shellEscape(this.ytDlpBin)} --quiet --no-warnings --no-playlist -o ${shellEscape(template)} -f "bv*[ext=mp4]+ba[ext=m4a]/b[ext=mp4]/best" --merge-output-format mp4 -- ${shellEscape(context.sourceUrl)}`;
612
+ const cookieArg = context.cookieFilePath ? ` --cookies ${shellEscape(context.cookieFilePath)}` : '';
613
+ const downloadCommand = `${shellEscape(this.ytDlpBin)} --quiet --no-warnings --no-playlist${cookieArg} -o ${shellEscape(template)} -f "bv*[ext=mp4]+ba[ext=m4a]/b[ext=mp4]/best" --merge-output-format mp4 -- ${shellEscape(context.sourceUrl)}`;
544
614
  await this.#runCommand(downloadCommand, { cwd: context.jobDir, timeout: 14 * 60 * 1000 });
545
615
 
546
616
  const videoPath = firstFileMatching(context.jobDir, 'video.');