tuna-agent 0.1.5 → 0.1.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.
@@ -51,6 +51,7 @@ export class ClaudeCodeAdapter {
51
51
  if (task.mode === 'agent_team') {
52
52
  console.log(`[ClaudeCode] Agent Team mode — direct chat with Claude CLI`);
53
53
  ws.sendProgress(task.id, 'executing', { startedAt: new Date().toISOString() });
54
+ ws.sendSubtaskStart(task.id, { id: 'agent-team', role: 'agent', description: task.description });
54
55
  const MAX_ROUNDS = 50;
55
56
  let sessionId;
56
57
  let totalDurationMs = 0;
@@ -202,6 +203,11 @@ export class ClaudeCodeAdapter {
202
203
  durationMs: totalDurationMs,
203
204
  sessionId,
204
205
  });
206
+ // Workflow tasks: no follow-up needed — workflow engine handles next step
207
+ if (task.source === 'workflow') {
208
+ console.log(`[ClaudeCode] Workflow task done — skipping follow-up wait`);
209
+ return;
210
+ }
205
211
  // Save state for resume after potential timeout or daemon restart
206
212
  savePMState({
207
213
  taskId: task.id,
@@ -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): Promise<void>;
30
- export declare function handleGenerateScript(ws: AgentWebSocketClient, code: string, taskId: string, idea: string, topic: string, style?: string, duration?: number, language?: string): Promise<void>;
29
+ export declare function handleGenerateIdeas(ws: AgentWebSocketClient, code: string, taskId: string, topic: string, styleName?: string, styleDesc?: string, language?: 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>;
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<{
@@ -64,7 +64,7 @@ function hasContentCreator() {
64
64
  }
65
65
  }
66
66
  // ─── Handler: generate_ideas ──────────────────────────────────────────────────
67
- export async function handleGenerateIdeas(ws, code, taskId, topic) {
67
+ export async function handleGenerateIdeas(ws, code, taskId, topic, styleName, styleDesc, language) {
68
68
  if (!hasContentCreator()) {
69
69
  const error = 'content-creator agent not found on this machine';
70
70
  console.error(`[generate_ideas] ${error}`);
@@ -72,20 +72,33 @@ export async function handleGenerateIdeas(ws, code, taskId, topic) {
72
72
  ws.sendExtensionDone(code, taskId, { error });
73
73
  return;
74
74
  }
75
- const systemPrompt = [
75
+ const resolvedLang = language || 'vi';
76
+ const langInstruction = resolvedLang === 'vi'
77
+ ? `You always write titles in Vietnamese that feel native, not translated.`
78
+ : `You always write titles in English that feel natural and engaging.`;
79
+ const systemParts = [
76
80
  `You are a top-tier content strategist and viral video expert specializing in YouTube Shorts and TikTok.`,
77
81
  `You deeply understand viral hooks, storytelling patterns, and audience psychology.`,
78
82
  `You know what makes people stop scrolling: curiosity gaps, emotional triggers, POV format, plot twists, listicles, and controversial takes.`,
79
- `You always write titles in Vietnamese that feel native, not translated.`,
80
- ].join(' ');
83
+ langInstruction,
84
+ ];
85
+ if (styleName) {
86
+ systemParts.push(`The video style is "${styleName}"${styleDesc ? `: ${styleDesc}` : ''}.`);
87
+ systemParts.push(`Generate ideas that are specifically suited for this style.`);
88
+ }
89
+ const systemPrompt = systemParts.join(' ');
90
+ const langReq = resolvedLang === 'vi'
91
+ ? `- Titles must be in Vietnamese, natural and engaging`
92
+ : `- Titles must be in English, natural and engaging`;
81
93
  const prompt = [
82
94
  `Generate exactly 5 viral YouTube Shorts video ideas for the topic: "${topic}".`,
83
95
  ``,
84
96
  `Requirements:`,
85
97
  `- Each idea is a catchy, scroll-stopping video title`,
86
98
  `- Use proven viral patterns: POV, "X điều...", plot twist, emotional hook, controversial take`,
87
- `- Titles must be in Vietnamese, natural and engaging`,
99
+ langReq,
88
100
  `- Mix different angles/formats for variety`,
101
+ ...(styleName ? [`- Ideas must fit the "${styleName}" video style${styleDesc ? ` (${styleDesc})` : ''}`] : []),
89
102
  ``,
90
103
  `Respond with ONLY a JSON array of 5 strings. No explanation, no markdown, no wrapping.`,
91
104
  `Example format: ["title 1","title 2","title 3","title 4","title 5"]`,
@@ -150,7 +163,7 @@ export async function handleGenerateIdeas(ws, code, taskId, topic) {
150
163
  // 2. Run `claude -p <prompt> --append-system-prompt <template>` in lightweight mode
151
164
  // 3. Stream text deltas back to extension
152
165
  // 4. Return full script on completion
153
- export async function handleGenerateScript(ws, code, taskId, idea, topic, style, duration, language) {
166
+ export async function handleGenerateScript(ws, code, taskId, idea, topic, style, duration, language, styleName, styleGuidance) {
154
167
  if (!hasContentCreator()) {
155
168
  const error = 'content-creator agent not found on this machine';
156
169
  console.error(`[generate_script] ${error}`);
@@ -196,7 +209,12 @@ export async function handleGenerateScript(ws, code, taskId, idea, topic, style,
196
209
  catch {
197
210
  // Non-fatal — Claude will use N/A for characters
198
211
  }
199
- const systemPrompt = expandedTemplate + (characterContext
212
+ // Build style guidance section
213
+ let styleContext = '';
214
+ if (styleGuidance && styleGuidance.trim()) {
215
+ styleContext = `\n\n## VIDEO STYLE GUIDANCE (${styleName || resolvedStyle})\n\nFollow these style-specific instructions for tone, pacing, camera work, and visual direction. The output format MUST remain exactly as specified above — only the CONTENT within each section should reflect this guidance.\n\n${styleGuidance.trim()}`;
216
+ }
217
+ const systemPrompt = expandedTemplate + styleContext + (characterContext
200
218
  ? `\n\n## AVAILABLE CHARACTERS\n${characterContext}`
201
219
  : '');
202
220
  try {
@@ -364,9 +382,28 @@ async function _doRender(ws, code, taskId, record, payload) {
364
382
  localPath = destFile;
365
383
  }
366
384
  else if (scene.videoUrl.startsWith('http://') || scene.videoUrl.startsWith('https://')) {
367
- // HTTP(S) URL — download directly
368
- await _downloadFile(scene.videoUrl, destFile);
369
- localPath = destFile;
385
+ // HTTP(S) URL — ask extension to fetch (needs auth cookies for Google Flow)
386
+ try {
387
+ const result = await flowCmd(ws, code, {
388
+ type: 'fetchVideoAsBase64',
389
+ videoUrl: scene.videoUrl,
390
+ }, 120000);
391
+ const base64 = result.base64;
392
+ if (!base64)
393
+ throw new Error('No base64 data returned from extension');
394
+ fs.writeFileSync(destFile, Buffer.from(base64, 'base64'));
395
+ localPath = destFile;
396
+ }
397
+ catch (err) {
398
+ // Fallback: try direct download (works for non-auth URLs)
399
+ try {
400
+ await _downloadFile(scene.videoUrl, destFile);
401
+ localPath = destFile;
402
+ }
403
+ catch (dlErr) {
404
+ throw new Error(`Failed to download video for scene ${scene.idx}: ${dlErr instanceof Error ? dlErr.message : dlErr}`);
405
+ }
406
+ }
370
407
  }
371
408
  else if (scene.videoUrl.startsWith('blob:')) {
372
409
  // Blob URL — ask extension to extract via flow command
@@ -445,27 +482,40 @@ async function _doRender(ws, code, taskId, record, payload) {
445
482
  console.warn(`[render] ffprobe failed (non-fatal):`, e);
446
483
  }
447
484
  _emitProgress(ws, code, record, 95, 'Finalizing...');
448
- // ── Step 5: Update record and notify ─────────────────────────────────
485
+ // ── Step 5: Read video file as base64 for extension playback ─────────
486
+ let videoBase64;
487
+ try {
488
+ const buf = fs.readFileSync(outFile);
489
+ videoBase64 = buf.toString('base64');
490
+ console.log(`[render] Video base64 size: ${(videoBase64.length / 1024 / 1024).toFixed(1)}MB`);
491
+ }
492
+ catch (e) {
493
+ console.warn(`[render] Could not read video for base64 (non-fatal):`, e);
494
+ }
495
+ // ── Step 6: Update record and notify ─────────────────────────────────
449
496
  record.status = 'rendered';
450
497
  record.progress = 100;
451
498
  record.outputPath = outFile;
452
499
  upsertVideo(record);
453
500
  ws.sendExtensionEvent(code, {
454
501
  type: 'render_done',
455
- outputPath: outFile,
502
+ videoBase64,
456
503
  title: record.title,
457
504
  duration,
458
505
  resolution,
459
- video: record,
460
506
  });
461
507
  ws.sendExtensionDone(code, taskId, {
462
508
  ok: true,
463
509
  videoId: record.id,
464
- outputPath: outFile,
465
510
  duration,
466
511
  resolution,
467
512
  });
468
513
  console.log(`[render] Done: ${outFile} (${duration}, ${resolution})`);
514
+ // Clean up rendered video file — extension has the base64 data
515
+ try {
516
+ fs.unlinkSync(outFile);
517
+ }
518
+ catch { /* ignore */ }
469
519
  }
470
520
  catch (err) {
471
521
  const error = err instanceof Error ? err.message : String(err);
@@ -345,13 +345,13 @@ ${skillContent.slice(0, 15000)}`;
345
345
  }
346
346
  if (extTask === 'generate_ideas') {
347
347
  (async () => {
348
- await handleGenerateIdeas(ws, extCode, extTaskId, msg.topic);
348
+ await handleGenerateIdeas(ws, extCode, extTaskId, msg.topic, msg.styleName, msg.styleDesc, msg.language);
349
349
  })();
350
350
  break;
351
351
  }
352
352
  if (extTask === 'generate_script') {
353
353
  (async () => {
354
- await handleGenerateScript(ws, extCode, extTaskId, msg.idea, msg.topic, msg.style, msg.duration, msg.language);
354
+ await handleGenerateScript(ws, extCode, extTaskId, msg.idea, msg.topic, msg.style, msg.duration, msg.language, msg.styleName, msg.styleGuidance);
355
355
  })();
356
356
  break;
357
357
  }
package/dist/mcp/setup.js CHANGED
@@ -6,33 +6,43 @@ const __dirname = path.dirname(__filename);
6
6
  const MCP_CONFIG_DIR = path.join(process.env.HOME || '', '.tuna-agent');
7
7
  const MCP_CONFIG_PATH = path.join(MCP_CONFIG_DIR, 'mcp-config.json');
8
8
  // Mem0 config from environment variables
9
- const MEM0_SSH_HOST = process.env.MEM0_SSH_HOST; // e.g. "root@redrop.ddns.net"
9
+ // MEM0_SSH_HOST: "local" = run mem0-mcp directly, "user@host" = run via SSH, unset = disabled
10
+ const MEM0_SSH_HOST = process.env.MEM0_SSH_HOST;
10
11
  const MEM0_SSH_PORT = process.env.MEM0_SSH_PORT || '22';
11
- const MEM0_SSH_KEY = process.env.MEM0_SSH_KEY; // optional: path to SSH key
12
+ const MEM0_SSH_KEY = process.env.MEM0_SSH_KEY;
13
+ const MEM0_ENV_VARS = {
14
+ MEM0_API_BASE: 'http://127.0.0.1:8765',
15
+ MEM0_QDRANT_URL: 'http://127.0.0.1:6333',
16
+ MEM0_OLLAMA_URL: 'http://127.0.0.1:11434',
17
+ MEM0_EMBED_MODEL: 'mxbai-embed-large:latest',
18
+ MEM0_COLLECTION: 'openmemory',
19
+ MEM0_NEO4J_URL: 'bolt://127.0.0.1:7687',
20
+ MEM0_NEO4J_USER: 'neo4j',
21
+ MEM0_NEO4J_PASSWORD: 'mem0graph',
22
+ };
12
23
  /**
13
24
  * Build Mem0 MCP server config for an agent.
14
- * Runs mem0-mcp on the Mem0 infra machine via SSH stdio.
15
- * Returns null if MEM0_SSH_HOST is not configured.
25
+ * - MEM0_SSH_HOST="local": run mem0-mcp directly (Mem0 infra on same machine)
26
+ * - MEM0_SSH_HOST="user@host": run mem0-mcp via SSH stdio
27
+ * - MEM0_SSH_HOST unset: disabled
16
28
  */
17
29
  function buildMem0McpConfig(agentName) {
18
30
  if (!MEM0_SSH_HOST)
19
31
  return null;
20
- const envVars = [
21
- 'MEM0_API_BASE=http://127.0.0.1:8765',
22
- 'MEM0_QDRANT_URL=http://127.0.0.1:6333',
23
- 'MEM0_OLLAMA_URL=http://127.0.0.1:11434',
24
- 'MEM0_EMBED_MODEL=mxbai-embed-large:latest',
25
- 'MEM0_COLLECTION=openmemory',
26
- `MEM0_USER_ID=${agentName}`,
27
- 'MEM0_NEO4J_URL=bolt://127.0.0.1:7687',
28
- 'MEM0_NEO4J_USER=neo4j',
29
- 'MEM0_NEO4J_PASSWORD=mem0graph',
30
- ].join(' ');
32
+ const envWithUser = { ...MEM0_ENV_VARS, MEM0_USER_ID: agentName };
33
+ if (MEM0_SSH_HOST === 'local') {
34
+ return {
35
+ command: 'mem0-mcp',
36
+ args: [],
37
+ env: envWithUser,
38
+ };
39
+ }
40
+ const envString = Object.entries(envWithUser).map(([k, v]) => `${k}=${v}`).join(' ');
31
41
  const args = ['-p', MEM0_SSH_PORT, '-o', 'StrictHostKeyChecking=no'];
32
42
  if (MEM0_SSH_KEY) {
33
43
  args.push('-i', MEM0_SSH_KEY);
34
44
  }
35
- args.push(MEM0_SSH_HOST, `${envVars} mem0-mcp`);
45
+ args.push(MEM0_SSH_HOST, `${envString} mem0-mcp`);
36
46
  return { command: 'ssh', args };
37
47
  }
38
48
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tuna-agent",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Tuna Agent - Run AI coding tasks on your machine",
5
5
  "bin": {
6
6
  "tuna-agent": "dist/cli/index.js"