tuna-agent 0.1.36 → 0.1.38

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.
@@ -26,8 +26,8 @@ export interface VideoRecord {
26
26
  aspectRatio?: string;
27
27
  }
28
28
  export declare function handleGetHistory(ws: AgentWebSocketClient, code: string, taskId: string): void;
29
- export declare function handleGenerateIdeas(ws: AgentWebSocketClient, code: string, taskId: string, topic: string, styleName?: string, styleDesc?: string, language?: string, count?: number, ideasInstruction?: string): Promise<void>;
30
- export declare function handleGenerateScript(ws: AgentWebSocketClient, code: string, taskId: string, idea: string, topic: string, style?: string, duration?: number, language?: string, styleName?: string, styleGuidance?: string): Promise<void>;
29
+ export declare function handleGenerateIdeas(ws: AgentWebSocketClient, code: string, taskId: string, topic: string, styleName?: string, styleDesc?: string, language?: string, count?: number, ideasInstruction?: string, provider?: string): Promise<void>;
30
+ export declare function handleGenerateScript(ws: AgentWebSocketClient, code: string, taskId: string, idea: string, topic: string, style?: string, duration?: number, language?: string, styleName?: string, styleGuidance?: string, provider?: string): Promise<void>;
31
31
  export declare function handleRetryVideo(ws: AgentWebSocketClient, code: string, taskId: string, videoId: string): void;
32
32
  export declare function handleGenerateScene(ws: AgentWebSocketClient, code: string, taskId: string, sceneIdx: number, prompt: string, aspectRatio: string): Promise<void>;
33
33
  export declare function handleGenerateScenes(ws: AgentWebSocketClient, code: string, taskId: string, scenes: Array<{
@@ -15,6 +15,7 @@ import { execFile } from 'child_process';
15
15
  import { promisify } from 'util';
16
16
  import { pipeline } from 'stream/promises';
17
17
  import { runClaude } from '../utils/claude-cli.js';
18
+ import { runOpenAI } from '../utils/openai-api.js';
18
19
  const execFileAsync = promisify(execFile);
19
20
  // ─── Storage ──────────────────────────────────────────────────────────────────
20
21
  const VIDEOS_DIR = path.join(os.homedir(), '.tuna-content', 'videos');
@@ -64,8 +65,9 @@ function hasContentCreator() {
64
65
  }
65
66
  }
66
67
  // ─── Handler: generate_ideas ──────────────────────────────────────────────────
67
- export async function handleGenerateIdeas(ws, code, taskId, topic, styleName, styleDesc, language, count, ideasInstruction) {
68
- if (!hasContentCreator()) {
68
+ export async function handleGenerateIdeas(ws, code, taskId, topic, styleName, styleDesc, language, count, ideasInstruction, provider) {
69
+ console.log(`[generate_ideas] Received: topic="${topic}" count=${count} provider=${provider || 'claude-cli'} ideasInstruction=${ideasInstruction ? ideasInstruction.length + ' chars' : '(none)'} styleName=${styleName || '(none)'}`);
70
+ if (provider !== 'openai' && !hasContentCreator()) {
69
71
  const error = 'content-creator agent not found on this machine';
70
72
  console.error(`[generate_ideas] ${error}`);
71
73
  ws.sendExtensionEvent(code, { type: 'ideas_result', ideas: [], error });
@@ -108,21 +110,30 @@ export async function handleGenerateIdeas(ws, code, taskId, topic, styleName, st
108
110
  `Example format: ["title 1","title 2","title 3","title 4","title 5"]`,
109
111
  ].join('\n');
110
112
  try {
111
- const result = await runClaude({
112
- prompt,
113
- systemPrompt,
114
- cwd: CONTENT_CREATOR_DIR,
115
- maxTurns: 1,
116
- outputFormat: 'json',
117
- timeoutMs: 60000,
118
- });
113
+ let resultText;
114
+ if (provider === 'openai') {
115
+ console.log(`[generate_ideas] Using OpenAI provider`);
116
+ const oaiResult = await runOpenAI({ prompt, systemPrompt, timeoutMs: 60000 });
117
+ resultText = oaiResult.result;
118
+ }
119
+ else {
120
+ const result = await runClaude({
121
+ prompt,
122
+ systemPrompt,
123
+ cwd: CONTENT_CREATOR_DIR,
124
+ maxTurns: 1,
125
+ outputFormat: 'json',
126
+ timeoutMs: 60000,
127
+ });
128
+ resultText = result.result;
129
+ }
119
130
  let ideas = [];
120
131
  try {
121
- // Try all JSON arrays in the response and pick the one with 5 items
132
+ // Try all JSON arrays in the response and pick the one closest to n items
122
133
  const arrays = [];
123
134
  const re = /\[(?:[^\[\]]*(?:"(?:[^"\\]|\\.)*"[^\[\]]*)*)\]/g;
124
135
  let m;
125
- while ((m = re.exec(result.result)) !== null) {
136
+ while ((m = re.exec(resultText)) !== null) {
126
137
  try {
127
138
  const parsed = JSON.parse(m[0]);
128
139
  if (Array.isArray(parsed) && parsed.every(i => typeof i === 'string')) {
@@ -139,7 +150,7 @@ export async function handleGenerateIdeas(ws, code, taskId, topic, styleName, st
139
150
  catch { /* fall through to fallback */ }
140
151
  // Fallback: split numbered/bulleted lines
141
152
  if (ideas.length === 0) {
142
- ideas = result.result
153
+ ideas = resultText
143
154
  .split('\n')
144
155
  .map(l => l.replace(/^[\d\-\*\.\)\s]+/, '').replace(/^[""]|[""]$/g, '').trim())
145
156
  .filter(l => l.length > 10 && !l.startsWith('[') && !l.toLowerCase().includes('json'))
@@ -167,8 +178,9 @@ export async function handleGenerateIdeas(ws, code, taskId, topic, styleName, st
167
178
  // 2. Run `claude -p <prompt> --append-system-prompt <template>` in lightweight mode
168
179
  // 3. Stream text deltas back to extension
169
180
  // 4. Return full script on completion
170
- export async function handleGenerateScript(ws, code, taskId, idea, topic, style, duration, language, styleName, styleGuidance) {
171
- if (!hasContentCreator()) {
181
+ export async function handleGenerateScript(ws, code, taskId, idea, topic, style, duration, language, styleName, styleGuidance, provider) {
182
+ console.log(`[generate_script] provider=${provider || 'claude-cli'}`);
183
+ if (provider !== 'openai' && !hasContentCreator()) {
172
184
  const error = 'content-creator agent not found on this machine';
173
185
  console.error(`[generate_script] ${error}`);
174
186
  ws.sendExtensionDone(code, taskId, { error });
@@ -221,58 +233,76 @@ export async function handleGenerateScript(ws, code, taskId, idea, topic, style,
221
233
  const systemPrompt = expandedTemplate + styleContext + (characterContext
222
234
  ? `\n\n## AVAILABLE CHARACTERS\n${characterContext}`
223
235
  : '');
236
+ const userPrompt = `Viết script video theo hướng dẫn trên.\n\nIdea: ${idea}\nStyle: ${resolvedStyle}\nClips: ${Math.round(resolvedDuration / 8)} (each exactly 8s, total ~${resolvedDuration}s)\nLanguage: ${resolvedLanguage}`;
224
237
  try {
225
- let streamChunks = 0;
226
- let sentTextLen = 0; // track how much text we've already streamed
227
- const result = await runClaude({
228
- prompt: `Viết script video theo hướng dẫn trên.\n\nIdea: ${idea}\nStyle: ${resolvedStyle}\nClips: ${Math.round(resolvedDuration / 8)} (each exactly 8s, total ~${resolvedDuration}s)\nLanguage: ${resolvedLanguage}`,
229
- systemPrompt,
230
- cwd: CONTENT_CREATOR_DIR,
231
- maxTurns: 4,
232
- outputFormat: 'stream-json',
233
- includePartialMessages: true,
234
- timeoutMs: 180000,
235
- onStreamLine: (data) => {
236
- if (streamChunks < 5) {
237
- const sub = data.subtype || '';
238
- console.log(`[generate_script] stream #${streamChunks}: type=${data.type} sub=${sub}`);
239
- }
240
- streamChunks++;
241
- // stream_event: raw API events (token-level deltas) — fast, smooth streaming
242
- if (data.type === 'stream_event') {
243
- const evt = data.event;
244
- if (evt?.type === 'content_block_delta') {
245
- const delta = evt.delta;
246
- if (delta?.type === 'text_delta' && typeof delta.text === 'string') {
247
- sentTextLen += delta.text.length;
248
- ws.sendExtensionStream(code, taskId, delta.text);
249
- }
238
+ if (provider === 'openai') {
239
+ console.log(`[generate_script] Using OpenAI provider (streaming)`);
240
+ let sentTextLen = 0;
241
+ const oaiResult = await runOpenAI({
242
+ prompt: userPrompt,
243
+ systemPrompt,
244
+ timeoutMs: 180000,
245
+ stream: true,
246
+ onChunk: (chunk) => {
247
+ sentTextLen += chunk.length;
248
+ ws.sendExtensionStream(code, taskId, chunk);
249
+ },
250
+ });
251
+ console.log(`[generate_script] OpenAI done: streamed=${sentTextLen} chars`);
252
+ const scriptText = oaiResult.result;
253
+ ws.sendExtensionDone(code, taskId, { script: scriptText, text: scriptText });
254
+ }
255
+ else {
256
+ let streamChunks = 0;
257
+ let sentTextLen = 0;
258
+ const result = await runClaude({
259
+ prompt: userPrompt,
260
+ systemPrompt,
261
+ cwd: CONTENT_CREATOR_DIR,
262
+ maxTurns: 4,
263
+ outputFormat: 'stream-json',
264
+ includePartialMessages: true,
265
+ timeoutMs: 180000,
266
+ onStreamLine: (data) => {
267
+ if (streamChunks < 5) {
268
+ const sub = data.subtype || '';
269
+ console.log(`[generate_script] stream #${streamChunks}: type=${data.type} sub=${sub}`);
250
270
  }
251
- return;
252
- }
253
- // assistant: partial message snapshots (fallback for coarser updates)
254
- if (data.type === 'assistant') {
255
- const msg = data.message;
256
- const content = (msg?.content ?? data.content);
257
- if (Array.isArray(content)) {
258
- let fullText = '';
259
- for (const block of content) {
260
- if (block.type === 'text' && typeof block.text === 'string') {
261
- fullText += block.text;
271
+ streamChunks++;
272
+ if (data.type === 'stream_event') {
273
+ const evt = data.event;
274
+ if (evt?.type === 'content_block_delta') {
275
+ const delta = evt.delta;
276
+ if (delta?.type === 'text_delta' && typeof delta.text === 'string') {
277
+ sentTextLen += delta.text.length;
278
+ ws.sendExtensionStream(code, taskId, delta.text);
262
279
  }
263
280
  }
264
- if (fullText.length > sentTextLen) {
265
- const delta = fullText.slice(sentTextLen);
266
- sentTextLen = fullText.length;
267
- ws.sendExtensionStream(code, taskId, delta);
281
+ return;
282
+ }
283
+ if (data.type === 'assistant') {
284
+ const msg = data.message;
285
+ const content = (msg?.content ?? data.content);
286
+ if (Array.isArray(content)) {
287
+ let fullText = '';
288
+ for (const block of content) {
289
+ if (block.type === 'text' && typeof block.text === 'string') {
290
+ fullText += block.text;
291
+ }
292
+ }
293
+ if (fullText.length > sentTextLen) {
294
+ const delta = fullText.slice(sentTextLen);
295
+ sentTextLen = fullText.length;
296
+ ws.sendExtensionStream(code, taskId, delta);
297
+ }
268
298
  }
269
299
  }
270
- }
271
- },
272
- });
273
- console.log(`[generate_script] Done: ${streamChunks} chunks, streamed=${sentTextLen} chars, result=${result.result?.length || 0} chars`);
274
- const scriptText = result.result;
275
- ws.sendExtensionDone(code, taskId, { script: scriptText, text: scriptText });
300
+ },
301
+ });
302
+ console.log(`[generate_script] Done: ${streamChunks} chunks, streamed=${sentTextLen} chars, result=${result.result?.length || 0} chars`);
303
+ const scriptText = result.result;
304
+ ws.sendExtensionDone(code, taskId, { script: scriptText, text: scriptText });
305
+ }
276
306
  }
277
307
  catch (err) {
278
308
  const error = err instanceof Error ? err.message : String(err);
@@ -367,13 +367,13 @@ ${skillContent.slice(0, 15000)}`;
367
367
  }
368
368
  if (extTask === 'generate_ideas') {
369
369
  (async () => {
370
- await handleGenerateIdeas(ws, extCode, extTaskId, msg.topic, msg.styleName, msg.styleDesc, msg.language, msg.count, msg.ideasInstruction);
370
+ await handleGenerateIdeas(ws, extCode, extTaskId, msg.topic, msg.styleName, msg.styleDesc, msg.language, msg.count, msg.ideasInstruction, msg.provider);
371
371
  })();
372
372
  break;
373
373
  }
374
374
  if (extTask === 'generate_script') {
375
375
  (async () => {
376
- await handleGenerateScript(ws, extCode, extTaskId, msg.idea, msg.topic, msg.style, msg.duration, msg.language, msg.styleName, (msg.scriptInstruction || msg.styleGuidance));
376
+ await handleGenerateScript(ws, extCode, extTaskId, msg.idea, msg.topic, msg.style, msg.duration, msg.language, msg.styleName, (msg.scriptInstruction || msg.styleGuidance), msg.provider);
377
377
  })();
378
378
  break;
379
379
  }
@@ -0,0 +1,19 @@
1
+ export interface OpenAIRequest {
2
+ prompt: string;
3
+ systemPrompt?: string;
4
+ model?: string;
5
+ maxTokens?: number;
6
+ timeoutMs?: number;
7
+ stream?: boolean;
8
+ onChunk?: (chunk: string) => void;
9
+ }
10
+ export interface OpenAIResult {
11
+ result: string;
12
+ model: string;
13
+ usage?: {
14
+ prompt_tokens: number;
15
+ completion_tokens: number;
16
+ total_tokens: number;
17
+ };
18
+ }
19
+ export declare function runOpenAI(opts: OpenAIRequest): Promise<OpenAIResult>;
@@ -0,0 +1,94 @@
1
+ // OpenAI API utility — direct fetch, no SDK needed
2
+ const OPENAI_API_KEY = process.env.OPENAI_API_KEY || '';
3
+ const OPENAI_MODEL = process.env.OPENAI_MODEL || 'gpt-4o';
4
+ export async function runOpenAI(opts) {
5
+ const { prompt, systemPrompt, model = OPENAI_MODEL, maxTokens = 4096, timeoutMs = 120000, stream = false, onChunk, } = opts;
6
+ if (!OPENAI_API_KEY) {
7
+ throw new Error('OPENAI_API_KEY environment variable is not set');
8
+ }
9
+ const messages = [];
10
+ if (systemPrompt) {
11
+ messages.push({ role: 'system', content: systemPrompt });
12
+ }
13
+ messages.push({ role: 'user', content: prompt });
14
+ const controller = new AbortController();
15
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
16
+ try {
17
+ if (stream && onChunk) {
18
+ return await _streamRequest(messages, model, maxTokens, controller, onChunk);
19
+ }
20
+ else {
21
+ return await _normalRequest(messages, model, maxTokens, controller);
22
+ }
23
+ }
24
+ finally {
25
+ clearTimeout(timer);
26
+ }
27
+ }
28
+ async function _normalRequest(messages, model, maxTokens, controller) {
29
+ const resp = await fetch('https://api.openai.com/v1/chat/completions', {
30
+ method: 'POST',
31
+ headers: {
32
+ 'Content-Type': 'application/json',
33
+ 'Authorization': `Bearer ${OPENAI_API_KEY}`,
34
+ },
35
+ body: JSON.stringify({ model, messages, max_tokens: maxTokens }),
36
+ signal: controller.signal,
37
+ });
38
+ if (!resp.ok) {
39
+ const body = await resp.text();
40
+ throw new Error(`OpenAI API error ${resp.status}: ${body}`);
41
+ }
42
+ const data = await resp.json();
43
+ const choice = data.choices?.[0];
44
+ return {
45
+ result: choice?.message?.content || '',
46
+ model: data.model,
47
+ usage: data.usage,
48
+ };
49
+ }
50
+ async function _streamRequest(messages, model, maxTokens, controller, onChunk) {
51
+ const resp = await fetch('https://api.openai.com/v1/chat/completions', {
52
+ method: 'POST',
53
+ headers: {
54
+ 'Content-Type': 'application/json',
55
+ 'Authorization': `Bearer ${OPENAI_API_KEY}`,
56
+ },
57
+ body: JSON.stringify({ model, messages, max_tokens: maxTokens, stream: true }),
58
+ signal: controller.signal,
59
+ });
60
+ if (!resp.ok) {
61
+ const body = await resp.text();
62
+ throw new Error(`OpenAI API error ${resp.status}: ${body}`);
63
+ }
64
+ let fullText = '';
65
+ const reader = resp.body.getReader();
66
+ const decoder = new TextDecoder();
67
+ let buffer = '';
68
+ while (true) {
69
+ const { done, value } = await reader.read();
70
+ if (done)
71
+ break;
72
+ buffer += decoder.decode(value, { stream: true });
73
+ const lines = buffer.split('\n');
74
+ buffer = lines.pop() || '';
75
+ for (const line of lines) {
76
+ const trimmed = line.trim();
77
+ if (!trimmed || !trimmed.startsWith('data: '))
78
+ continue;
79
+ const payload = trimmed.slice(6);
80
+ if (payload === '[DONE]')
81
+ continue;
82
+ try {
83
+ const parsed = JSON.parse(payload);
84
+ const delta = parsed.choices?.[0]?.delta?.content;
85
+ if (delta) {
86
+ fullText += delta;
87
+ onChunk(delta);
88
+ }
89
+ }
90
+ catch { /* skip parse errors */ }
91
+ }
92
+ }
93
+ return { result: fullText, model };
94
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tuna-agent",
3
- "version": "0.1.36",
3
+ "version": "0.1.38",
4
4
  "description": "Tuna Agent - Run AI coding tasks on your machine",
5
5
  "bin": {
6
6
  "tuna-agent": "dist/cli/index.js"