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
|
|
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
|
-
|
|
80
|
-
]
|
|
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
|
-
|
|
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
|
-
|
|
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 —
|
|
368
|
-
|
|
369
|
-
|
|
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:
|
|
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
|
-
|
|
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);
|
package/dist/daemon/index.js
CHANGED
|
@@ -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
|
-
|
|
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;
|
|
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
|
-
*
|
|
15
|
-
*
|
|
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
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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, `${
|
|
45
|
+
args.push(MEM0_SSH_HOST, `${envString} mem0-mcp`);
|
|
36
46
|
return { command: 'ssh', args };
|
|
37
47
|
}
|
|
38
48
|
/**
|