10x-chat 0.4.3 → 0.5.7
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/dist/bin/cli.js +2 -0
- package/dist/bin/cli.js.map +1 -1
- package/dist/browser/daemon.d.ts +39 -0
- package/dist/browser/daemon.d.ts.map +1 -0
- package/dist/browser/daemon.js +114 -0
- package/dist/browser/daemon.js.map +1 -0
- package/dist/browser/index.d.ts +2 -0
- package/dist/browser/index.d.ts.map +1 -1
- package/dist/browser/index.js +2 -0
- package/dist/browser/index.js.map +1 -1
- package/dist/browser/manager.d.ts.map +1 -1
- package/dist/browser/manager.js +25 -12
- package/dist/browser/manager.js.map +1 -1
- package/dist/browser/tabs.d.ts +22 -0
- package/dist/browser/tabs.d.ts.map +1 -0
- package/dist/browser/tabs.js +97 -0
- package/dist/browser/tabs.js.map +1 -0
- package/dist/cli/chat.d.ts.map +1 -1
- package/dist/cli/chat.js +83 -42
- package/dist/cli/chat.js.map +1 -1
- package/dist/cli/login.d.ts.map +1 -1
- package/dist/cli/login.js +96 -3
- package/dist/cli/login.js.map +1 -1
- package/dist/cli/video.d.ts +3 -0
- package/dist/cli/video.d.ts.map +1 -0
- package/dist/cli/video.js +99 -0
- package/dist/cli/video.js.map +1 -0
- package/dist/core/files.d.ts +6 -0
- package/dist/core/files.d.ts.map +1 -0
- package/dist/core/files.js +39 -0
- package/dist/core/files.js.map +1 -0
- package/dist/core/images.d.ts +8 -0
- package/dist/core/images.d.ts.map +1 -0
- package/dist/core/images.js +79 -0
- package/dist/core/images.js.map +1 -0
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +1 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/orchestrator.d.ts +10 -0
- package/dist/core/orchestrator.d.ts.map +1 -1
- package/dist/core/orchestrator.js +19 -106
- package/dist/core/orchestrator.js.map +1 -1
- package/dist/core/polling.d.ts +30 -0
- package/dist/core/polling.d.ts.map +1 -0
- package/dist/core/polling.js +37 -0
- package/dist/core/polling.js.map +1 -0
- package/dist/core/video-orchestrator.d.ts +22 -0
- package/dist/core/video-orchestrator.d.ts.map +1 -0
- package/dist/core/video-orchestrator.js +341 -0
- package/dist/core/video-orchestrator.js.map +1 -0
- package/dist/providers/chatgpt.d.ts.map +1 -1
- package/dist/providers/chatgpt.js +26 -39
- package/dist/providers/chatgpt.js.map +1 -1
- package/dist/providers/claude.d.ts.map +1 -1
- package/dist/providers/claude.js +24 -52
- package/dist/providers/claude.js.map +1 -1
- package/dist/providers/flow.d.ts +55 -0
- package/dist/providers/flow.d.ts.map +1 -0
- package/dist/providers/flow.js +286 -0
- package/dist/providers/flow.js.map +1 -0
- package/dist/providers/gemini.d.ts.map +1 -1
- package/dist/providers/gemini.js +30 -54
- package/dist/providers/gemini.js.map +1 -1
- package/dist/providers/grok.d.ts.map +1 -1
- package/dist/providers/grok.js +23 -42
- package/dist/providers/grok.js.map +1 -1
- package/dist/providers/registry.d.ts.map +1 -1
- package/dist/providers/registry.js +2 -0
- package/dist/providers/registry.js.map +1 -1
- package/dist/providers/utils.d.ts +10 -0
- package/dist/providers/utils.d.ts.map +1 -0
- package/dist/providers/utils.js +28 -0
- package/dist/providers/utils.js.map +1 -0
- package/dist/types.d.ts +24 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +3 -2
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import { mkdir, stat, writeFile } from 'node:fs/promises';
|
|
2
|
-
import path from 'node:path';
|
|
3
1
|
import chalk from 'chalk';
|
|
4
|
-
import fg from 'fast-glob';
|
|
5
2
|
import { launchBrowser } from '../browser/index.js';
|
|
6
3
|
import { loadConfig } from '../config.js';
|
|
7
4
|
import { getProvider } from '../providers/index.js';
|
|
8
5
|
import { createSession, saveBundle, saveResponse, updateSession } from '../session/index.js';
|
|
9
6
|
import { buildBundle } from './bundle.js';
|
|
7
|
+
import { resolveAttachPaths } from './files.js';
|
|
8
|
+
import { downloadImages } from './images.js';
|
|
9
|
+
/** Providers supported by chat --all (excludes special-purpose providers). */
|
|
10
|
+
const CHAT_PROVIDERS = ['chatgpt', 'gemini', 'claude', 'grok'];
|
|
10
11
|
/**
|
|
11
12
|
* Execute a chat interaction with a provider:
|
|
12
13
|
* 1. Build the prompt bundle
|
|
@@ -115,112 +116,24 @@ export async function runChat(options) {
|
|
|
115
116
|
}
|
|
116
117
|
}
|
|
117
118
|
/**
|
|
118
|
-
*
|
|
119
|
-
*
|
|
119
|
+
* Run the same prompt against multiple providers in parallel.
|
|
120
|
+
* Uses the shared browser daemon — all providers reuse one Chromium process.
|
|
120
121
|
*/
|
|
121
|
-
async function
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
const results = [];
|
|
126
|
-
const context = page.context();
|
|
127
|
-
for (let i = 0; i < images.length; i++) {
|
|
128
|
-
const img = images[i];
|
|
122
|
+
export async function runChatAll(options) {
|
|
123
|
+
const targets = options.providers ?? CHAT_PROVIDERS;
|
|
124
|
+
console.log(chalk.bold.blue(`\n🚀 Sending to ${targets.length} providers in parallel...\n`));
|
|
125
|
+
const tasks = targets.map(async (provider) => {
|
|
129
126
|
try {
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
const cookieHeader = cookies.map((c) => `${c.name}=${c.value}`).join('; ');
|
|
133
|
-
let buf;
|
|
134
|
-
let contentType = '';
|
|
135
|
-
// Try Node fetch with cookies first
|
|
136
|
-
const resp = await fetch(url, {
|
|
137
|
-
headers: cookieHeader ? { cookie: cookieHeader } : undefined,
|
|
138
|
-
}).catch(() => null);
|
|
139
|
-
if (resp?.ok) {
|
|
140
|
-
buf = Buffer.from(await resp.arrayBuffer());
|
|
141
|
-
contentType = resp.headers.get('content-type') ?? '';
|
|
142
|
-
}
|
|
143
|
-
else {
|
|
144
|
-
// Fallback: fetch via browser context (handles auth cookies + CORS)
|
|
145
|
-
const dataUrl = await page.evaluate(async (imgUrl) => {
|
|
146
|
-
try {
|
|
147
|
-
const r = await fetch(imgUrl, { credentials: 'include' });
|
|
148
|
-
if (!r.ok)
|
|
149
|
-
return null;
|
|
150
|
-
const blob = await r.blob();
|
|
151
|
-
return new Promise((resolve) => {
|
|
152
|
-
const reader = new FileReader();
|
|
153
|
-
reader.onloadend = () => resolve(reader.result);
|
|
154
|
-
reader.readAsDataURL(blob);
|
|
155
|
-
});
|
|
156
|
-
}
|
|
157
|
-
catch {
|
|
158
|
-
return null;
|
|
159
|
-
}
|
|
160
|
-
}, url);
|
|
161
|
-
if (!dataUrl) {
|
|
162
|
-
console.warn(chalk.yellow(` ⚠ Failed to download image ${i + 1}: HTTP ${resp?.status ?? 'N/A'}`));
|
|
163
|
-
results.push(img);
|
|
164
|
-
continue;
|
|
165
|
-
}
|
|
166
|
-
const match = dataUrl.match(/^data:([^;]+);base64,(.+)$/);
|
|
167
|
-
if (!match) {
|
|
168
|
-
results.push(img);
|
|
169
|
-
continue;
|
|
170
|
-
}
|
|
171
|
-
contentType = match[1];
|
|
172
|
-
buf = Buffer.from(match[2], 'base64');
|
|
173
|
-
}
|
|
174
|
-
const ext = contentType.includes('png')
|
|
175
|
-
? 'png'
|
|
176
|
-
: contentType.includes('webp')
|
|
177
|
-
? 'webp'
|
|
178
|
-
: 'jpg';
|
|
179
|
-
const filename = `image_${i + 1}.${ext}`;
|
|
180
|
-
const filePath = path.join(outputDir, filename);
|
|
181
|
-
await writeFile(filePath, buf);
|
|
182
|
-
console.log(chalk.green(` ✓ Saved: ${filePath}`));
|
|
183
|
-
results.push({ ...img, localPath: filePath });
|
|
184
|
-
}
|
|
185
|
-
catch (err) {
|
|
186
|
-
console.warn(chalk.yellow(` ⚠ Error downloading image ${i + 1}: ${err}`));
|
|
187
|
-
results.push(img);
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
return results;
|
|
191
|
-
}
|
|
192
|
-
/**
|
|
193
|
-
* Resolve --attach paths (supports globs) to absolute file paths.
|
|
194
|
-
* Validates that all resolved paths exist and are files.
|
|
195
|
-
*/
|
|
196
|
-
async function resolveAttachPaths(patterns) {
|
|
197
|
-
const resolved = [];
|
|
198
|
-
for (const pattern of patterns) {
|
|
199
|
-
// Check if it's a glob or a literal path
|
|
200
|
-
if (/[*?{}[\]]/.test(pattern)) {
|
|
201
|
-
const matches = await fg(pattern, { absolute: true, onlyFiles: true });
|
|
202
|
-
if (matches.length === 0) {
|
|
203
|
-
throw new Error(`No files matched attachment pattern: ${pattern}`);
|
|
204
|
-
}
|
|
205
|
-
resolved.push(...matches);
|
|
127
|
+
const result = await runChat({ ...options, provider });
|
|
128
|
+
return { provider, result };
|
|
206
129
|
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
resolved.push(abs);
|
|
213
|
-
}
|
|
214
|
-
else {
|
|
215
|
-
console.warn(chalk.yellow(`Skipping directory: ${pattern}`));
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
catch {
|
|
219
|
-
throw new Error(`Attachment not found: ${pattern}`);
|
|
220
|
-
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
return {
|
|
132
|
+
provider,
|
|
133
|
+
error: error instanceof Error ? error.message : String(error),
|
|
134
|
+
};
|
|
221
135
|
}
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
return [...new Set(resolved)];
|
|
136
|
+
});
|
|
137
|
+
return Promise.allSettled(tasks).then((settled) => settled.map((s) => s.status === 'fulfilled' ? s.value : { provider: 'chatgpt', error: String(s.reason) }));
|
|
225
138
|
}
|
|
226
139
|
//# sourceMappingURL=orchestrator.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"orchestrator.js","sourceRoot":"","sources":["../../src/core/orchestrator.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"orchestrator.js","sourceRoot":"","sources":["../../src/core/orchestrator.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Page } from 'playwright';
|
|
2
|
+
/**
|
|
3
|
+
* Options for the pollUntilStable utility.
|
|
4
|
+
*/
|
|
5
|
+
export interface PollOptions {
|
|
6
|
+
/** Function that extracts current text from the page. */
|
|
7
|
+
getText: (page: Page) => Promise<string>;
|
|
8
|
+
/** Total timeout for polling in milliseconds. */
|
|
9
|
+
timeoutMs: number;
|
|
10
|
+
/** Called with new text delta when content changes. */
|
|
11
|
+
onChunk?: (delta: string) => void;
|
|
12
|
+
/** Number of consecutive stable polls before considering complete. Default: 3 */
|
|
13
|
+
stableThreshold?: number;
|
|
14
|
+
/** Interval between polls in milliseconds. Default: 1000 */
|
|
15
|
+
pollIntervalMs?: number;
|
|
16
|
+
/** Optional function that returns true if streaming is still in progress. */
|
|
17
|
+
isStreaming?: (page: Page) => Promise<boolean>;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Poll a page element until its text content stabilizes (stops changing).
|
|
21
|
+
* Used by multiple providers for captureResponse streaming detection.
|
|
22
|
+
*
|
|
23
|
+
* @returns The final stable text, elapsed time, and whether it was truncated.
|
|
24
|
+
*/
|
|
25
|
+
export declare function pollUntilStable(page: Page, opts: PollOptions): Promise<{
|
|
26
|
+
text: string;
|
|
27
|
+
elapsed: number;
|
|
28
|
+
truncated: boolean;
|
|
29
|
+
}>;
|
|
30
|
+
//# sourceMappingURL=polling.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"polling.d.ts","sourceRoot":"","sources":["../../src/core/polling.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAEvC;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,yDAAyD;IACzD,OAAO,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACzC,iDAAiD;IACjD,SAAS,EAAE,MAAM,CAAC;IAClB,uDAAuD;IACvD,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,iFAAiF;IACjF,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,4DAA4D;IAC5D,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,6EAA6E;IAC7E,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;CAChD;AAED;;;;;GAKG;AACH,wBAAsB,eAAe,CACnC,IAAI,EAAE,IAAI,EACV,IAAI,EAAE,WAAW,GAChB,OAAO,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,OAAO,CAAA;CAAE,CAAC,CAwChE"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Poll a page element until its text content stabilizes (stops changing).
|
|
3
|
+
* Used by multiple providers for captureResponse streaming detection.
|
|
4
|
+
*
|
|
5
|
+
* @returns The final stable text, elapsed time, and whether it was truncated.
|
|
6
|
+
*/
|
|
7
|
+
export async function pollUntilStable(page, opts) {
|
|
8
|
+
const { getText, timeoutMs, onChunk, stableThreshold = 3, pollIntervalMs = 1000, isStreaming, } = opts;
|
|
9
|
+
const startTime = Date.now();
|
|
10
|
+
let lastText = '';
|
|
11
|
+
let stableCount = 0;
|
|
12
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
13
|
+
const streaming = isStreaming ? await isStreaming(page) : false;
|
|
14
|
+
const currentText = await getText(page);
|
|
15
|
+
if (currentText === lastText && !streaming) {
|
|
16
|
+
stableCount++;
|
|
17
|
+
if (stableCount >= stableThreshold && currentText.length > 0) {
|
|
18
|
+
break;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
if (onChunk && currentText.length > lastText.length) {
|
|
23
|
+
onChunk(currentText.slice(lastText.length));
|
|
24
|
+
}
|
|
25
|
+
lastText = currentText;
|
|
26
|
+
stableCount = 0;
|
|
27
|
+
}
|
|
28
|
+
await page.waitForTimeout(pollIntervalMs);
|
|
29
|
+
}
|
|
30
|
+
const elapsed = Date.now() - startTime;
|
|
31
|
+
return {
|
|
32
|
+
text: lastText,
|
|
33
|
+
elapsed,
|
|
34
|
+
truncated: elapsed >= timeoutMs && stableCount < stableThreshold,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=polling.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"polling.js","sourceRoot":"","sources":["../../src/core/polling.ts"],"names":[],"mappings":"AAoBA;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,IAAU,EACV,IAAiB;IAEjB,MAAM,EACJ,OAAO,EACP,SAAS,EACT,OAAO,EACP,eAAe,GAAG,CAAC,EACnB,cAAc,GAAG,IAAI,EACrB,WAAW,GACZ,GAAG,IAAI,CAAC;IAET,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IAC7B,IAAI,QAAQ,GAAG,EAAE,CAAC;IAClB,IAAI,WAAW,GAAG,CAAC,CAAC;IAEpB,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,SAAS,GAAG,SAAS,EAAE,CAAC;QAC1C,MAAM,SAAS,GAAG,WAAW,CAAC,CAAC,CAAC,MAAM,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;QAChE,MAAM,WAAW,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;QAExC,IAAI,WAAW,KAAK,QAAQ,IAAI,CAAC,SAAS,EAAE,CAAC;YAC3C,WAAW,EAAE,CAAC;YACd,IAAI,WAAW,IAAI,eAAe,IAAI,WAAW,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBAC7D,MAAM;YACR,CAAC;QACH,CAAC;aAAM,CAAC;YACN,IAAI,OAAO,IAAI,WAAW,CAAC,MAAM,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC;gBACpD,OAAO,CAAC,WAAW,CAAC,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC;YAC9C,CAAC;YACD,QAAQ,GAAG,WAAW,CAAC;YACvB,WAAW,GAAG,CAAC,CAAC;QAClB,CAAC;QAED,MAAM,IAAI,CAAC,cAAc,CAAC,cAAc,CAAC,CAAC;IAC5C,CAAC;IAED,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,SAAS,CAAC;IACvC,OAAO;QACL,IAAI,EAAE,QAAQ;QACd,OAAO;QACP,SAAS,EAAE,OAAO,IAAI,SAAS,IAAI,WAAW,GAAG,eAAe;KACjE,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { GeneratedVideo, ProviderName, VideoOptions } from '../types.js';
|
|
2
|
+
export interface VideoResult {
|
|
3
|
+
sessionId: string;
|
|
4
|
+
provider: ProviderName;
|
|
5
|
+
message: string;
|
|
6
|
+
videos: GeneratedVideo[];
|
|
7
|
+
truncated: boolean;
|
|
8
|
+
durationMs: number;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Execute a video generation interaction with Google Flow:
|
|
12
|
+
* 1. Launch browser → navigate to Flow
|
|
13
|
+
* 2. Click "New project"
|
|
14
|
+
* 3. Configure video mode (model, orientation, count, sub-mode)
|
|
15
|
+
* 4. If frames mode: upload Start/End keyframe images
|
|
16
|
+
* 5. Enter prompt → click Create
|
|
17
|
+
* 6. Poll for generation progress
|
|
18
|
+
* 7. When complete: download video files
|
|
19
|
+
* 8. Save session
|
|
20
|
+
*/
|
|
21
|
+
export declare function runVideo(options: VideoOptions): Promise<VideoResult>;
|
|
22
|
+
//# sourceMappingURL=video-orchestrator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"video-orchestrator.d.ts","sourceRoot":"","sources":["../../src/core/video-orchestrator.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,cAAc,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE9E,MAAM,WAAW,WAAW;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,YAAY,CAAC;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,cAAc,EAAE,CAAC;IACzB,SAAS,EAAE,OAAO,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,QAAQ,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC,CA0M1E"}
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import { launchBrowser } from '../browser/index.js';
|
|
6
|
+
import { loadConfig } from '../config.js';
|
|
7
|
+
import { configureVideoMode, FLOW_CONFIG, uploadKeyframes, waitForGeneration, } from '../providers/flow.js';
|
|
8
|
+
import { getProvider } from '../providers/index.js';
|
|
9
|
+
import { createSession, saveBundle, saveResponse, updateSession } from '../session/index.js';
|
|
10
|
+
/**
|
|
11
|
+
* Execute a video generation interaction with Google Flow:
|
|
12
|
+
* 1. Launch browser → navigate to Flow
|
|
13
|
+
* 2. Click "New project"
|
|
14
|
+
* 3. Configure video mode (model, orientation, count, sub-mode)
|
|
15
|
+
* 4. If frames mode: upload Start/End keyframe images
|
|
16
|
+
* 5. Enter prompt → click Create
|
|
17
|
+
* 6. Poll for generation progress
|
|
18
|
+
* 7. When complete: download video files
|
|
19
|
+
* 8. Save session
|
|
20
|
+
*/
|
|
21
|
+
export async function runVideo(options) {
|
|
22
|
+
const config = await loadConfig();
|
|
23
|
+
const providerName = 'flow';
|
|
24
|
+
const provider = getProvider(providerName);
|
|
25
|
+
const timeoutMs = options.timeoutMs ?? FLOW_CONFIG.defaultTimeoutMs;
|
|
26
|
+
const headless = options.headed === true ? false : config.headless;
|
|
27
|
+
// Create session
|
|
28
|
+
const session = await createSession(providerName, options.prompt, options.model);
|
|
29
|
+
await saveBundle(session.id, options.prompt);
|
|
30
|
+
console.log(chalk.dim(`Session: ${session.id}`));
|
|
31
|
+
console.log(chalk.blue(`Provider: ${FLOW_CONFIG.displayName}`));
|
|
32
|
+
console.log(chalk.dim(`Model: ${options.model ?? FLOW_CONFIG.defaultModel}`));
|
|
33
|
+
console.log(chalk.dim(`Mode: ${options.mode ?? 'ingredients'}`));
|
|
34
|
+
// Note: Flow always uses shared persistent profile for Google SPA auth
|
|
35
|
+
// Launch browser — Flow needs a persistent browser context for Google SPA auth.
|
|
36
|
+
// Use shared profile (default/) with persistent: true.
|
|
37
|
+
let browser;
|
|
38
|
+
try {
|
|
39
|
+
await updateSession(session.id, { status: 'running' });
|
|
40
|
+
browser = await launchBrowser({
|
|
41
|
+
provider: 'gemini',
|
|
42
|
+
headless,
|
|
43
|
+
url: FLOW_CONFIG.url,
|
|
44
|
+
profileMode: 'shared',
|
|
45
|
+
persistent: true,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
await updateSession(session.id, { status: 'failed' });
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
const startTime = Date.now();
|
|
53
|
+
const page = browser.page;
|
|
54
|
+
try {
|
|
55
|
+
// ── Remove all blocking overlays from DOM ──
|
|
56
|
+
await page.waitForTimeout(5000);
|
|
57
|
+
const dismissOverlays = async () => {
|
|
58
|
+
// Only remove cookie consent bar — nothing else
|
|
59
|
+
await page.evaluate(() => {
|
|
60
|
+
for (const el of Array.from(document.querySelectorAll('.glue-cookie-notification-bar'))) {
|
|
61
|
+
el.remove();
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
await page.waitForTimeout(500);
|
|
65
|
+
// Click "Get started" on onboarding modal if visible
|
|
66
|
+
const getStarted = page.locator('button:has-text("Get started")').first();
|
|
67
|
+
if (await getStarted.isVisible({ timeout: 2000 }).catch(() => false)) {
|
|
68
|
+
await getStarted.click({ force: true });
|
|
69
|
+
console.log(chalk.dim('Dismissed onboarding modal'));
|
|
70
|
+
// The modal dismiss often triggers a navigation — wait for the page
|
|
71
|
+
// to fully settle before doing any page.evaluate() calls.
|
|
72
|
+
await page.waitForLoadState('domcontentloaded').catch(() => { });
|
|
73
|
+
await page.waitForTimeout(3000);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
await dismissOverlays();
|
|
77
|
+
// ── Handle marketing page → studio navigation ──
|
|
78
|
+
let isMarketingPage = false;
|
|
79
|
+
try {
|
|
80
|
+
isMarketingPage = await page.evaluate(() => {
|
|
81
|
+
const body = document.body.textContent ?? '';
|
|
82
|
+
return body.includes('Where the next wave') || body.includes('Scroll to Explore');
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
// Context may have been destroyed by a recent navigation — assume not marketing page
|
|
87
|
+
}
|
|
88
|
+
if (isMarketingPage) {
|
|
89
|
+
console.log(chalk.dim('On marketing page, entering studio...'));
|
|
90
|
+
await page.evaluate(() => {
|
|
91
|
+
for (const el of Array.from(document.querySelectorAll('a, button'))) {
|
|
92
|
+
if (el.textContent?.includes('Create with Flow')) {
|
|
93
|
+
el.click();
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
await page.waitForLoadState('domcontentloaded').catch(() => { });
|
|
99
|
+
await page.waitForTimeout(8000);
|
|
100
|
+
await dismissOverlays();
|
|
101
|
+
}
|
|
102
|
+
// Step 3: Now we should be in the studio. Check if we need to create a new project.
|
|
103
|
+
// Wait for SPA content to render (studio loads asynchronously)
|
|
104
|
+
await page.waitForTimeout(5000);
|
|
105
|
+
let currentUrl = page.url();
|
|
106
|
+
console.log(chalk.dim(`Studio URL: ${currentUrl}`));
|
|
107
|
+
const isProjectPage = currentUrl.includes('/project/');
|
|
108
|
+
if (!isProjectPage) {
|
|
109
|
+
console.log(chalk.dim('Creating new project...'));
|
|
110
|
+
// Try JS click on "New project" since overlays may still intercept
|
|
111
|
+
const clicked = await page.evaluate(() => {
|
|
112
|
+
for (const btn of Array.from(document.querySelectorAll('button'))) {
|
|
113
|
+
if (btn.textContent?.includes('New project') || btn.textContent?.includes('新建项目')) {
|
|
114
|
+
btn.click();
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return false;
|
|
119
|
+
});
|
|
120
|
+
if (clicked) {
|
|
121
|
+
console.log(chalk.dim('Clicked New project, waiting for project to load...'));
|
|
122
|
+
// Wait for URL to change to /project/
|
|
123
|
+
try {
|
|
124
|
+
await page.waitForURL('**/project/**', { timeout: 15_000 });
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
await page.waitForTimeout(5000);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
console.log(chalk.dim('New project button not found in DOM'));
|
|
132
|
+
}
|
|
133
|
+
currentUrl = page.url();
|
|
134
|
+
console.log(chalk.dim(`After new project: ${currentUrl}`));
|
|
135
|
+
}
|
|
136
|
+
// Configure video mode
|
|
137
|
+
console.log(chalk.dim('Configuring video mode...'));
|
|
138
|
+
await configureVideoMode(page, {
|
|
139
|
+
mode: options.mode,
|
|
140
|
+
model: options.model,
|
|
141
|
+
orientation: options.orientation,
|
|
142
|
+
count: options.count,
|
|
143
|
+
});
|
|
144
|
+
// Upload keyframes if in frames mode
|
|
145
|
+
if (options.mode === 'frames' && (options.startFrame || options.endFrame)) {
|
|
146
|
+
console.log(chalk.dim('Uploading keyframes...'));
|
|
147
|
+
await uploadKeyframes(page, {
|
|
148
|
+
startFrame: options.startFrame,
|
|
149
|
+
endFrame: options.endFrame,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
// Enter prompt and submit
|
|
153
|
+
console.log(chalk.dim('Submitting prompt...'));
|
|
154
|
+
await provider.actions.submitPrompt(page, options.prompt);
|
|
155
|
+
// Wait for generation
|
|
156
|
+
console.log(chalk.dim('Generating video...'));
|
|
157
|
+
let lastPct = -1;
|
|
158
|
+
await waitForGeneration(page, {
|
|
159
|
+
timeoutMs,
|
|
160
|
+
onProgress: (pct) => {
|
|
161
|
+
if (pct !== lastPct) {
|
|
162
|
+
lastPct = pct;
|
|
163
|
+
process.stdout.write(`\r${chalk.blue('▸')} Generating... ${chalk.bold(`${pct}%`)}`);
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
process.stdout.write('\n');
|
|
168
|
+
const durationMs = Date.now() - startTime;
|
|
169
|
+
const timedOut = durationMs >= timeoutMs;
|
|
170
|
+
// Download generated videos
|
|
171
|
+
const videos = await downloadVideos(page, session.id, options.saveDir);
|
|
172
|
+
const savedVideos = videos.filter((v) => v.localPath);
|
|
173
|
+
const count = savedVideos.length;
|
|
174
|
+
const message = count > 0
|
|
175
|
+
? `Generated ${count} video(s) in ${Math.round(durationMs / 1000)}s`
|
|
176
|
+
: timedOut
|
|
177
|
+
? 'Video generation timed out'
|
|
178
|
+
: 'No videos detected';
|
|
179
|
+
await saveResponse(session.id, message);
|
|
180
|
+
await updateSession(session.id, {
|
|
181
|
+
status: timedOut ? 'timeout' : count > 0 ? 'completed' : 'failed',
|
|
182
|
+
durationMs,
|
|
183
|
+
});
|
|
184
|
+
return {
|
|
185
|
+
sessionId: session.id,
|
|
186
|
+
provider: providerName,
|
|
187
|
+
message,
|
|
188
|
+
videos,
|
|
189
|
+
truncated: timedOut,
|
|
190
|
+
durationMs,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
const durationMs = Date.now() - startTime;
|
|
195
|
+
const isTimeout = error instanceof Error && error.message.toLowerCase().includes('timeout');
|
|
196
|
+
await updateSession(session.id, {
|
|
197
|
+
status: isTimeout ? 'timeout' : 'failed',
|
|
198
|
+
durationMs,
|
|
199
|
+
});
|
|
200
|
+
throw error;
|
|
201
|
+
}
|
|
202
|
+
finally {
|
|
203
|
+
await browser.close();
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Download generated videos from the browser.
|
|
208
|
+
* Finds <video> elements, fetches their src blobs via the browser context, and saves locally.
|
|
209
|
+
* Handles both blob: URLs and regular HTTPS URLs.
|
|
210
|
+
*/
|
|
211
|
+
async function downloadVideos(page, sessionId, saveDir) {
|
|
212
|
+
const outputDir = saveDir ?? path.join(os.homedir(), '.10x-chat', 'sessions', sessionId, 'videos');
|
|
213
|
+
await mkdir(outputDir, { recursive: true });
|
|
214
|
+
// Get video source URLs from the page
|
|
215
|
+
const videoSources = await page.evaluate(() => {
|
|
216
|
+
const videos = document.querySelectorAll('video');
|
|
217
|
+
const sources = [];
|
|
218
|
+
for (const v of Array.from(videos)) {
|
|
219
|
+
const src = v.src || v.querySelector('source')?.src || '';
|
|
220
|
+
if (src)
|
|
221
|
+
sources.push(src);
|
|
222
|
+
}
|
|
223
|
+
return sources;
|
|
224
|
+
});
|
|
225
|
+
if (videoSources.length === 0) {
|
|
226
|
+
console.log(chalk.yellow(' No downloadable videos found on page.'));
|
|
227
|
+
// Fallback: try to find download buttons and click them
|
|
228
|
+
const downloadBtns = await page.locator('a[download], button:has-text("Download")').count();
|
|
229
|
+
if (downloadBtns > 0) {
|
|
230
|
+
console.log(chalk.dim(` Found ${downloadBtns} download button(s), attempting click...`));
|
|
231
|
+
const dlPromise = page.waitForEvent('download', { timeout: 15_000 }).catch(() => null);
|
|
232
|
+
await page.locator('a[download], button:has-text("Download")').first().click();
|
|
233
|
+
const download = await dlPromise;
|
|
234
|
+
if (download) {
|
|
235
|
+
const filePath = path.join(outputDir, download.suggestedFilename() || 'video_1.mp4');
|
|
236
|
+
await download.saveAs(filePath);
|
|
237
|
+
console.log(chalk.green(` ✓ Saved: ${filePath}`));
|
|
238
|
+
return [{ localPath: filePath }];
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return [];
|
|
242
|
+
}
|
|
243
|
+
const results = [];
|
|
244
|
+
for (let i = 0; i < videoSources.length; i++) {
|
|
245
|
+
const src = videoSources[i];
|
|
246
|
+
try {
|
|
247
|
+
let buf = null;
|
|
248
|
+
let contentType = '';
|
|
249
|
+
if (src.startsWith('blob:')) {
|
|
250
|
+
// For blob: URLs, use XMLHttpRequest inside the browser context
|
|
251
|
+
const dataUrl = await page.evaluate(async (videoUrl) => {
|
|
252
|
+
return new Promise((resolve) => {
|
|
253
|
+
const xhr = new XMLHttpRequest();
|
|
254
|
+
xhr.open('GET', videoUrl, true);
|
|
255
|
+
xhr.responseType = 'blob';
|
|
256
|
+
xhr.onload = () => {
|
|
257
|
+
if (xhr.status === 200) {
|
|
258
|
+
const reader = new FileReader();
|
|
259
|
+
reader.onloadend = () => resolve(reader.result);
|
|
260
|
+
reader.readAsDataURL(xhr.response);
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
resolve(null);
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
xhr.onerror = () => resolve(null);
|
|
267
|
+
xhr.send();
|
|
268
|
+
});
|
|
269
|
+
}, src);
|
|
270
|
+
if (dataUrl) {
|
|
271
|
+
const match = dataUrl.match(/^data:([^;]+);base64,(.+)$/);
|
|
272
|
+
if (match) {
|
|
273
|
+
contentType = match[1];
|
|
274
|
+
buf = Buffer.from(match[2], 'base64');
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
// HTTPS URLs (including tRPC redirects) — fetch server-side with cookies
|
|
280
|
+
const context = page.context();
|
|
281
|
+
const cookies = await context.cookies([src]).catch(() => []);
|
|
282
|
+
const cookieHeader = cookies.map((c) => `${c.name}=${c.value}`).join('; ');
|
|
283
|
+
const resp = await fetch(src, {
|
|
284
|
+
headers: cookieHeader ? { cookie: cookieHeader } : undefined,
|
|
285
|
+
redirect: 'follow',
|
|
286
|
+
}).catch(() => null);
|
|
287
|
+
if (resp?.ok) {
|
|
288
|
+
buf = Buffer.from(await resp.arrayBuffer());
|
|
289
|
+
contentType = resp.headers.get('content-type') ?? '';
|
|
290
|
+
}
|
|
291
|
+
else {
|
|
292
|
+
// Fallback: try in-browser fetch
|
|
293
|
+
const dataUrl = await page.evaluate(async (videoUrl) => {
|
|
294
|
+
try {
|
|
295
|
+
const r = await fetch(videoUrl, { credentials: 'include', redirect: 'follow' });
|
|
296
|
+
if (!r.ok)
|
|
297
|
+
return null;
|
|
298
|
+
const blob = await r.blob();
|
|
299
|
+
return new Promise((resolve) => {
|
|
300
|
+
const reader = new FileReader();
|
|
301
|
+
reader.onloadend = () => resolve(reader.result);
|
|
302
|
+
reader.readAsDataURL(blob);
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
}, src);
|
|
309
|
+
if (dataUrl) {
|
|
310
|
+
const match = dataUrl.match(/^data:([^;]+);base64,(.+)$/);
|
|
311
|
+
if (match) {
|
|
312
|
+
contentType = match[1];
|
|
313
|
+
buf = Buffer.from(match[2], 'base64');
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
if (!buf) {
|
|
319
|
+
console.warn(chalk.yellow(` ⚠ Failed to download video ${i + 1} (src: ${src.slice(0, 80)})`));
|
|
320
|
+
results.push({});
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
const ext = contentType.includes('mp4')
|
|
324
|
+
? 'mp4'
|
|
325
|
+
: contentType.includes('webm')
|
|
326
|
+
? 'webm'
|
|
327
|
+
: 'mp4';
|
|
328
|
+
const filename = `video_${i + 1}.${ext}`;
|
|
329
|
+
const filePath = path.join(outputDir, filename);
|
|
330
|
+
await writeFile(filePath, buf);
|
|
331
|
+
console.log(chalk.green(` ✓ Saved: ${filePath}`));
|
|
332
|
+
results.push({ localPath: filePath });
|
|
333
|
+
}
|
|
334
|
+
catch (err) {
|
|
335
|
+
console.warn(chalk.yellow(` ⚠ Error downloading video ${i + 1}: ${err}`));
|
|
336
|
+
results.push({});
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return results;
|
|
340
|
+
}
|
|
341
|
+
//# sourceMappingURL=video-orchestrator.js.map
|