tuna-agent 0.1.37 → 0.1.39

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,9 +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
- console.log(`[generate_ideas] Received: topic="${topic}" count=${count} ideasInstruction=${ideasInstruction ? ideasInstruction.length + ' chars' : '(none)'} styleName=${styleName || '(none)'}`);
69
- 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()) {
70
71
  const error = 'content-creator agent not found on this machine';
71
72
  console.error(`[generate_ideas] ${error}`);
72
73
  ws.sendExtensionEvent(code, { type: 'ideas_result', ideas: [], error });
@@ -109,21 +110,30 @@ export async function handleGenerateIdeas(ws, code, taskId, topic, styleName, st
109
110
  `Example format: ["title 1","title 2","title 3","title 4","title 5"]`,
110
111
  ].join('\n');
111
112
  try {
112
- const result = await runClaude({
113
- prompt,
114
- systemPrompt,
115
- cwd: CONTENT_CREATOR_DIR,
116
- maxTurns: 1,
117
- outputFormat: 'json',
118
- timeoutMs: 60000,
119
- });
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
+ }
120
130
  let ideas = [];
121
131
  try {
122
- // 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
123
133
  const arrays = [];
124
134
  const re = /\[(?:[^\[\]]*(?:"(?:[^"\\]|\\.)*"[^\[\]]*)*)\]/g;
125
135
  let m;
126
- while ((m = re.exec(result.result)) !== null) {
136
+ while ((m = re.exec(resultText)) !== null) {
127
137
  try {
128
138
  const parsed = JSON.parse(m[0]);
129
139
  if (Array.isArray(parsed) && parsed.every(i => typeof i === 'string')) {
@@ -140,7 +150,7 @@ export async function handleGenerateIdeas(ws, code, taskId, topic, styleName, st
140
150
  catch { /* fall through to fallback */ }
141
151
  // Fallback: split numbered/bulleted lines
142
152
  if (ideas.length === 0) {
143
- ideas = result.result
153
+ ideas = resultText
144
154
  .split('\n')
145
155
  .map(l => l.replace(/^[\d\-\*\.\)\s]+/, '').replace(/^[""]|[""]$/g, '').trim())
146
156
  .filter(l => l.length > 10 && !l.startsWith('[') && !l.toLowerCase().includes('json'))
@@ -168,8 +178,9 @@ export async function handleGenerateIdeas(ws, code, taskId, topic, styleName, st
168
178
  // 2. Run `claude -p <prompt> --append-system-prompt <template>` in lightweight mode
169
179
  // 3. Stream text deltas back to extension
170
180
  // 4. Return full script on completion
171
- export async function handleGenerateScript(ws, code, taskId, idea, topic, style, duration, language, styleName, styleGuidance) {
172
- 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()) {
173
184
  const error = 'content-creator agent not found on this machine';
174
185
  console.error(`[generate_script] ${error}`);
175
186
  ws.sendExtensionDone(code, taskId, { error });
@@ -222,58 +233,76 @@ export async function handleGenerateScript(ws, code, taskId, idea, topic, style,
222
233
  const systemPrompt = expandedTemplate + styleContext + (characterContext
223
234
  ? `\n\n## AVAILABLE CHARACTERS\n${characterContext}`
224
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}`;
225
237
  try {
226
- let streamChunks = 0;
227
- let sentTextLen = 0; // track how much text we've already streamed
228
- const result = await runClaude({
229
- 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}`,
230
- systemPrompt,
231
- cwd: CONTENT_CREATOR_DIR,
232
- maxTurns: 4,
233
- outputFormat: 'stream-json',
234
- includePartialMessages: true,
235
- timeoutMs: 180000,
236
- onStreamLine: (data) => {
237
- if (streamChunks < 5) {
238
- const sub = data.subtype || '';
239
- console.log(`[generate_script] stream #${streamChunks}: type=${data.type} sub=${sub}`);
240
- }
241
- streamChunks++;
242
- // stream_event: raw API events (token-level deltas) — fast, smooth streaming
243
- if (data.type === 'stream_event') {
244
- const evt = data.event;
245
- if (evt?.type === 'content_block_delta') {
246
- const delta = evt.delta;
247
- if (delta?.type === 'text_delta' && typeof delta.text === 'string') {
248
- sentTextLen += delta.text.length;
249
- ws.sendExtensionStream(code, taskId, delta.text);
250
- }
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}`);
251
270
  }
252
- return;
253
- }
254
- // assistant: partial message snapshots (fallback for coarser updates)
255
- if (data.type === 'assistant') {
256
- const msg = data.message;
257
- const content = (msg?.content ?? data.content);
258
- if (Array.isArray(content)) {
259
- let fullText = '';
260
- for (const block of content) {
261
- if (block.type === 'text' && typeof block.text === 'string') {
262
- 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);
263
279
  }
264
280
  }
265
- if (fullText.length > sentTextLen) {
266
- const delta = fullText.slice(sentTextLen);
267
- sentTextLen = fullText.length;
268
- 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
+ }
269
298
  }
270
299
  }
271
- }
272
- },
273
- });
274
- console.log(`[generate_script] Done: ${streamChunks} chunks, streamed=${sentTextLen} chars, result=${result.result?.length || 0} chars`);
275
- const scriptText = result.result;
276
- 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
+ }
277
306
  }
278
307
  catch (err) {
279
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.37",
3
+ "version": "0.1.39",
4
4
  "description": "Tuna Agent - Run AI coding tasks on your machine",
5
5
  "bin": {
6
6
  "tuna-agent": "dist/cli/index.js"