tuna-agent 0.1.3 → 0.1.5
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.
|
@@ -27,7 +27,7 @@ export interface VideoRecord {
|
|
|
27
27
|
}
|
|
28
28
|
export declare function handleGetHistory(ws: AgentWebSocketClient, code: string, taskId: string): void;
|
|
29
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): 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>;
|
|
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<{
|
|
@@ -11,7 +11,11 @@
|
|
|
11
11
|
import fs from 'fs';
|
|
12
12
|
import path from 'path';
|
|
13
13
|
import os from 'os';
|
|
14
|
+
import { execFile } from 'child_process';
|
|
15
|
+
import { promisify } from 'util';
|
|
16
|
+
import { pipeline } from 'stream/promises';
|
|
14
17
|
import { runClaude } from '../utils/claude-cli.js';
|
|
18
|
+
const execFileAsync = promisify(execFile);
|
|
15
19
|
// ─── Storage ──────────────────────────────────────────────────────────────────
|
|
16
20
|
const VIDEOS_DIR = path.join(os.homedir(), '.tuna-content', 'videos');
|
|
17
21
|
const VIDEOS_DB = path.join(VIDEOS_DIR, 'history.json');
|
|
@@ -146,7 +150,7 @@ export async function handleGenerateIdeas(ws, code, taskId, topic) {
|
|
|
146
150
|
// 2. Run `claude -p <prompt> --append-system-prompt <template>` in lightweight mode
|
|
147
151
|
// 3. Stream text deltas back to extension
|
|
148
152
|
// 4. Return full script on completion
|
|
149
|
-
export async function handleGenerateScript(ws, code, taskId, idea, topic, style) {
|
|
153
|
+
export async function handleGenerateScript(ws, code, taskId, idea, topic, style, duration, language) {
|
|
150
154
|
if (!hasContentCreator()) {
|
|
151
155
|
const error = 'content-creator agent not found on this machine';
|
|
152
156
|
console.error(`[generate_script] ${error}`);
|
|
@@ -165,9 +169,16 @@ export async function handleGenerateScript(ws, code, taskId, idea, topic, style)
|
|
|
165
169
|
ws.sendExtensionDone(code, taskId, { error });
|
|
166
170
|
return;
|
|
167
171
|
}
|
|
168
|
-
const
|
|
169
|
-
const
|
|
170
|
-
const
|
|
172
|
+
const resolvedStyle = style || 'short-form';
|
|
173
|
+
const resolvedDuration = duration || 60;
|
|
174
|
+
const resolvedLanguage = language || 'vi';
|
|
175
|
+
console.log(`[generate_script] Received: style=${style} duration=${duration} (resolved=${resolvedDuration}) language=${language}`);
|
|
176
|
+
const expandedTemplate = template
|
|
177
|
+
.replace(/\$ARGUMENTS/g, idea)
|
|
178
|
+
.replace(/\$IDEA/g, idea)
|
|
179
|
+
.replace(/\$STYLE/g, resolvedStyle)
|
|
180
|
+
.replace(/\$DURATION/g, String(resolvedDuration))
|
|
181
|
+
.replace(/\$LANGUAGE/g, resolvedLanguage);
|
|
171
182
|
// Also read character profiles so Claude can match characters
|
|
172
183
|
let characterContext = '';
|
|
173
184
|
try {
|
|
@@ -192,7 +203,7 @@ export async function handleGenerateScript(ws, code, taskId, idea, topic, style)
|
|
|
192
203
|
let streamChunks = 0;
|
|
193
204
|
let sentTextLen = 0; // track how much text we've already streamed
|
|
194
205
|
const result = await runClaude({
|
|
195
|
-
prompt: `Viết script video theo hướng dẫn trên
|
|
206
|
+
prompt: `Viết script video theo hướng dẫn trên.\n\nIdea: ${idea}\nStyle: ${resolvedStyle}\nDuration: ${resolvedDuration}s\nLanguage: ${resolvedLanguage}`,
|
|
196
207
|
systemPrompt,
|
|
197
208
|
cwd: CONTENT_CREATOR_DIR,
|
|
198
209
|
maxTurns: 4,
|
|
@@ -320,19 +331,121 @@ async function _callVeo3(prompt, aspectRatio, onProgress) {
|
|
|
320
331
|
const outFile = path.join(VIDEOS_DIR, `scene-${Date.now()}.mp4`);
|
|
321
332
|
return 'file://' + outFile;
|
|
322
333
|
}
|
|
323
|
-
// ─── Internal: Render
|
|
334
|
+
// ─── Internal: Render (ffmpeg concat) ─────────────────────────────────────────
|
|
324
335
|
async function _doRender(ws, code, taskId, record, payload) {
|
|
336
|
+
const tmpDir = path.join(os.tmpdir(), `tuna-render-${record.id}`);
|
|
325
337
|
try {
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
338
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
339
|
+
ensureDir();
|
|
340
|
+
const scenes = payload?.scenes || record.scenes || [];
|
|
341
|
+
// ── Step 1: Collect scene video files (0% → 30%) ─────────────────────
|
|
342
|
+
_emitProgress(ws, code, record, 5, 'Collecting scene videos...');
|
|
343
|
+
const videoFiles = [];
|
|
344
|
+
for (let i = 0; i < scenes.length; i++) {
|
|
345
|
+
const scene = scenes[i];
|
|
346
|
+
if (!scene.videoUrl) {
|
|
347
|
+
console.warn(`[render] Scene ${scene.idx} has no videoUrl, skipping`);
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
let localPath;
|
|
351
|
+
const destFile = path.join(tmpDir, `scene_${String(scene.idx).padStart(2, '0')}.mp4`);
|
|
352
|
+
if (scene.videoUrl.startsWith('file://')) {
|
|
353
|
+
localPath = scene.videoUrl.replace('file://', '');
|
|
354
|
+
}
|
|
355
|
+
else if (scene.videoUrl.startsWith('/')) {
|
|
356
|
+
localPath = scene.videoUrl;
|
|
357
|
+
}
|
|
358
|
+
else if (scene.videoUrl.startsWith('data:')) {
|
|
359
|
+
// Base64 data URL — decode and save
|
|
360
|
+
const match = scene.videoUrl.match(/^data:video\/\w+;base64,(.+)$/);
|
|
361
|
+
if (!match)
|
|
362
|
+
throw new Error(`Invalid data URL for scene ${scene.idx}`);
|
|
363
|
+
fs.writeFileSync(destFile, Buffer.from(match[1], 'base64'));
|
|
364
|
+
localPath = destFile;
|
|
365
|
+
}
|
|
366
|
+
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;
|
|
370
|
+
}
|
|
371
|
+
else if (scene.videoUrl.startsWith('blob:')) {
|
|
372
|
+
// Blob URL — ask extension to extract via flow command
|
|
373
|
+
try {
|
|
374
|
+
const result = await flowCmd(ws, code, {
|
|
375
|
+
type: 'getVideoBlob',
|
|
376
|
+
videoUrl: scene.videoUrl,
|
|
377
|
+
}, 60000);
|
|
378
|
+
const base64 = result.base64;
|
|
379
|
+
if (!base64)
|
|
380
|
+
throw new Error('No base64 data returned');
|
|
381
|
+
fs.writeFileSync(destFile, Buffer.from(base64, 'base64'));
|
|
382
|
+
localPath = destFile;
|
|
383
|
+
}
|
|
384
|
+
catch (err) {
|
|
385
|
+
throw new Error(`Failed to extract blob video for scene ${scene.idx}: ${err instanceof Error ? err.message : err}`);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
else {
|
|
389
|
+
throw new Error(`Unsupported videoUrl format for scene ${scene.idx}: ${scene.videoUrl.substring(0, 30)}...`);
|
|
390
|
+
}
|
|
391
|
+
if (!fs.existsSync(localPath)) {
|
|
392
|
+
throw new Error(`Video file not found for scene ${scene.idx}: ${localPath}`);
|
|
393
|
+
}
|
|
394
|
+
videoFiles.push(localPath);
|
|
395
|
+
const pct = 5 + Math.round(((i + 1) / scenes.length) * 25);
|
|
396
|
+
_emitProgress(ws, code, record, pct, `Collected ${i + 1}/${scenes.length} videos`);
|
|
332
397
|
}
|
|
333
|
-
|
|
334
|
-
|
|
398
|
+
if (videoFiles.length === 0) {
|
|
399
|
+
throw new Error('No scene videos available to render');
|
|
400
|
+
}
|
|
401
|
+
console.log(`[render] Collected ${videoFiles.length} video files`);
|
|
402
|
+
// ── Step 2: Create ffmpeg concat list (30%) ──────────────────────────
|
|
403
|
+
_emitProgress(ws, code, record, 30, 'Preparing video merge...');
|
|
404
|
+
const concatList = path.join(tmpDir, 'concat.txt');
|
|
405
|
+
const concatContent = videoFiles
|
|
406
|
+
.map(f => `file '${f.replace(/'/g, "'\\''")}'`)
|
|
407
|
+
.join('\n');
|
|
408
|
+
fs.writeFileSync(concatList, concatContent);
|
|
409
|
+
// ── Step 3: Run ffmpeg concat (35% → 80%) ───────────────────────────
|
|
335
410
|
const outFile = path.join(VIDEOS_DIR, `${record.id}.mp4`);
|
|
411
|
+
_emitProgress(ws, code, record, 35, 'Merging videos with ffmpeg...');
|
|
412
|
+
// Try stream-copy first (fast, no re-encoding)
|
|
413
|
+
try {
|
|
414
|
+
await _runFfmpeg([
|
|
415
|
+
'-y', '-f', 'concat', '-safe', '0',
|
|
416
|
+
'-i', concatList,
|
|
417
|
+
'-c', 'copy',
|
|
418
|
+
'-movflags', '+faststart',
|
|
419
|
+
outFile,
|
|
420
|
+
]);
|
|
421
|
+
}
|
|
422
|
+
catch (copyErr) {
|
|
423
|
+
// If copy fails (codec mismatch), re-encode
|
|
424
|
+
console.warn(`[render] -c copy failed, re-encoding: ${copyErr instanceof Error ? copyErr.message : copyErr}`);
|
|
425
|
+
_emitProgress(ws, code, record, 40, 'Re-encoding (codec mismatch)...');
|
|
426
|
+
await _runFfmpeg([
|
|
427
|
+
'-y', '-f', 'concat', '-safe', '0',
|
|
428
|
+
'-i', concatList,
|
|
429
|
+
'-c:v', 'libx264', '-preset', 'fast', '-crf', '23',
|
|
430
|
+
'-c:a', 'aac', '-b:a', '128k',
|
|
431
|
+
'-movflags', '+faststart',
|
|
432
|
+
outFile,
|
|
433
|
+
]);
|
|
434
|
+
}
|
|
435
|
+
_emitProgress(ws, code, record, 85, 'Video merged');
|
|
436
|
+
// ── Step 4: Get output metadata ─────────────────────────────────────
|
|
437
|
+
let duration = '0:00';
|
|
438
|
+
let resolution = '1080×1920';
|
|
439
|
+
try {
|
|
440
|
+
const probe = await _runFfprobe(outFile);
|
|
441
|
+
duration = probe.duration;
|
|
442
|
+
resolution = probe.resolution;
|
|
443
|
+
}
|
|
444
|
+
catch (e) {
|
|
445
|
+
console.warn(`[render] ffprobe failed (non-fatal):`, e);
|
|
446
|
+
}
|
|
447
|
+
_emitProgress(ws, code, record, 95, 'Finalizing...');
|
|
448
|
+
// ── Step 5: Update record and notify ─────────────────────────────────
|
|
336
449
|
record.status = 'rendered';
|
|
337
450
|
record.progress = 100;
|
|
338
451
|
record.outputPath = outFile;
|
|
@@ -341,18 +454,83 @@ async function _doRender(ws, code, taskId, record, payload) {
|
|
|
341
454
|
type: 'render_done',
|
|
342
455
|
outputPath: outFile,
|
|
343
456
|
title: record.title,
|
|
457
|
+
duration,
|
|
458
|
+
resolution,
|
|
344
459
|
video: record,
|
|
345
460
|
});
|
|
346
|
-
ws.sendExtensionDone(code, taskId, {
|
|
461
|
+
ws.sendExtensionDone(code, taskId, {
|
|
462
|
+
ok: true,
|
|
463
|
+
videoId: record.id,
|
|
464
|
+
outputPath: outFile,
|
|
465
|
+
duration,
|
|
466
|
+
resolution,
|
|
467
|
+
});
|
|
468
|
+
console.log(`[render] Done: ${outFile} (${duration}, ${resolution})`);
|
|
347
469
|
}
|
|
348
470
|
catch (err) {
|
|
349
471
|
const error = err instanceof Error ? err.message : String(err);
|
|
472
|
+
console.error(`[render] Failed:`, error);
|
|
350
473
|
record.status = 'failed';
|
|
351
474
|
record.error = error;
|
|
352
475
|
upsertVideo(record);
|
|
353
476
|
ws.sendExtensionEvent(code, { type: 'render_failed', error });
|
|
354
477
|
ws.sendExtensionDone(code, taskId, { error });
|
|
355
478
|
}
|
|
479
|
+
finally {
|
|
480
|
+
// Cleanup temp directory
|
|
481
|
+
try {
|
|
482
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
483
|
+
}
|
|
484
|
+
catch { /* ignore cleanup errors */ }
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
/** Emit render progress and persist to history */
|
|
488
|
+
function _emitProgress(ws, code, record, progress, phase) {
|
|
489
|
+
record.progress = progress;
|
|
490
|
+
upsertVideo(record);
|
|
491
|
+
ws.sendExtensionEvent(code, { type: 'render_progress', progress, phase });
|
|
492
|
+
}
|
|
493
|
+
/** Download a file from HTTP(S) URL to a local path */
|
|
494
|
+
async function _downloadFile(url, dest) {
|
|
495
|
+
const res = await fetch(url);
|
|
496
|
+
if (!res.ok)
|
|
497
|
+
throw new Error(`Download failed: ${res.status} ${res.statusText}`);
|
|
498
|
+
if (!res.body)
|
|
499
|
+
throw new Error('No response body');
|
|
500
|
+
const fileStream = fs.createWriteStream(dest);
|
|
501
|
+
// @ts-ignore — Node ReadableStream → pipeline
|
|
502
|
+
await pipeline(res.body, fileStream);
|
|
503
|
+
}
|
|
504
|
+
/** Run ffmpeg with the given arguments */
|
|
505
|
+
async function _runFfmpeg(args) {
|
|
506
|
+
const { stdout, stderr } = await execFileAsync('ffmpeg', args, {
|
|
507
|
+
timeout: 300000, // 5 minutes
|
|
508
|
+
maxBuffer: 10 * 1024 * 1024, // 10MB
|
|
509
|
+
});
|
|
510
|
+
return stdout || stderr;
|
|
511
|
+
}
|
|
512
|
+
/** Probe a video file for duration and resolution */
|
|
513
|
+
async function _runFfprobe(filePath) {
|
|
514
|
+
const { stdout } = await execFileAsync('ffprobe', [
|
|
515
|
+
'-v', 'quiet',
|
|
516
|
+
'-print_format', 'json',
|
|
517
|
+
'-show_format', '-show_streams',
|
|
518
|
+
filePath,
|
|
519
|
+
], { timeout: 10000 });
|
|
520
|
+
const info = JSON.parse(stdout);
|
|
521
|
+
const videoStream = info.streams?.find((s) => s.codec_type === 'video');
|
|
522
|
+
let duration = '0:00';
|
|
523
|
+
const totalSec = parseFloat(info.format?.duration || '0');
|
|
524
|
+
if (totalSec > 0) {
|
|
525
|
+
const min = Math.floor(totalSec / 60);
|
|
526
|
+
const sec = Math.round(totalSec % 60);
|
|
527
|
+
duration = `${min}:${String(sec).padStart(2, '0')}`;
|
|
528
|
+
}
|
|
529
|
+
let resolution = '1080×1920';
|
|
530
|
+
if (videoStream) {
|
|
531
|
+
resolution = `${videoStream.width}×${videoStream.height}`;
|
|
532
|
+
}
|
|
533
|
+
return { duration, resolution };
|
|
356
534
|
}
|
|
357
535
|
// ─── Character profile interface ─────────────────────────────────────────────
|
|
358
536
|
const CHARACTERS_DIR = path.join(os.homedir(), '.tuna', 'characters');
|
package/dist/daemon/index.js
CHANGED
|
@@ -155,6 +155,43 @@ export async function startDaemon(config) {
|
|
|
155
155
|
ws.send({ action: 'agent_skills_scanned', agent_id: agentId, skills });
|
|
156
156
|
console.log(`[Daemon] Scanned ${skills.length} skill(s) for agent ${agentId}`);
|
|
157
157
|
}
|
|
158
|
+
else if (command === 'create_agent') {
|
|
159
|
+
const agentId = msg.agent_id;
|
|
160
|
+
const agentName = msg.agent_name || 'agent';
|
|
161
|
+
const rawFolder = msg.folder_path || '';
|
|
162
|
+
const roleDesc = msg.role_description || '';
|
|
163
|
+
if (!rawFolder) {
|
|
164
|
+
console.error('[Daemon] create_agent: missing folder_path');
|
|
165
|
+
ws.send({ action: 'agent_folder_created', agent_id: agentId, success: false, error: 'Missing folder_path' });
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
const folderPath = rawFolder.startsWith('~')
|
|
169
|
+
? path.join(os.homedir(), rawFolder.slice(1))
|
|
170
|
+
: rawFolder;
|
|
171
|
+
console.log(`[Daemon] Creating agent folder: ${folderPath} for "${agentName}"`);
|
|
172
|
+
try {
|
|
173
|
+
// Create folder structure
|
|
174
|
+
fs.mkdirSync(path.join(folderPath, '.claude', 'commands'), { recursive: true });
|
|
175
|
+
fs.mkdirSync(path.join(folderPath, 'knowledge'), { recursive: true });
|
|
176
|
+
// Write CLAUDE.md only if it doesn't exist
|
|
177
|
+
const claudeMdPath = path.join(folderPath, 'CLAUDE.md');
|
|
178
|
+
if (!fs.existsSync(claudeMdPath)) {
|
|
179
|
+
const claudeContent = `# ${agentName}\n\n${roleDesc || `You are ${agentName}, an AI agent.`}\n`;
|
|
180
|
+
fs.writeFileSync(claudeMdPath, claudeContent, 'utf-8');
|
|
181
|
+
console.log(`[Daemon] Created CLAUDE.md for "${agentName}"`);
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
console.log(`[Daemon] CLAUDE.md already exists, skipping`);
|
|
185
|
+
}
|
|
186
|
+
ws.send({ action: 'agent_folder_created', agent_id: agentId, success: true });
|
|
187
|
+
console.log(`[Daemon] Agent folder created: ${folderPath}`);
|
|
188
|
+
}
|
|
189
|
+
catch (err) {
|
|
190
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
191
|
+
console.error(`[Daemon] Failed to create agent folder: ${errMsg}`);
|
|
192
|
+
ws.send({ action: 'agent_folder_created', agent_id: agentId, success: false, error: errMsg });
|
|
193
|
+
}
|
|
194
|
+
}
|
|
158
195
|
else if (command === 'analyze_skill') {
|
|
159
196
|
const skillId = msg.skill_id;
|
|
160
197
|
const sourcePath = msg.source_path;
|
|
@@ -314,7 +351,7 @@ ${skillContent.slice(0, 15000)}`;
|
|
|
314
351
|
}
|
|
315
352
|
if (extTask === 'generate_script') {
|
|
316
353
|
(async () => {
|
|
317
|
-
await handleGenerateScript(ws, extCode, extTaskId, msg.idea, msg.topic, msg.style);
|
|
354
|
+
await handleGenerateScript(ws, extCode, extTaskId, msg.idea, msg.topic, msg.style, msg.duration, msg.language);
|
|
318
355
|
})();
|
|
319
356
|
break;
|
|
320
357
|
}
|
package/dist/mcp/setup.js
CHANGED
|
@@ -5,6 +5,36 @@ const __filename = fileURLToPath(import.meta.url);
|
|
|
5
5
|
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
|
+
// Mem0 config from environment variables
|
|
9
|
+
const MEM0_SSH_HOST = process.env.MEM0_SSH_HOST; // e.g. "root@redrop.ddns.net"
|
|
10
|
+
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
|
+
/**
|
|
13
|
+
* 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.
|
|
16
|
+
*/
|
|
17
|
+
function buildMem0McpConfig(agentName) {
|
|
18
|
+
if (!MEM0_SSH_HOST)
|
|
19
|
+
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(' ');
|
|
31
|
+
const args = ['-p', MEM0_SSH_PORT, '-o', 'StrictHostKeyChecking=no'];
|
|
32
|
+
if (MEM0_SSH_KEY) {
|
|
33
|
+
args.push('-i', MEM0_SSH_KEY);
|
|
34
|
+
}
|
|
35
|
+
args.push(MEM0_SSH_HOST, `${envVars} mem0-mcp`);
|
|
36
|
+
return { command: 'ssh', args };
|
|
37
|
+
}
|
|
8
38
|
/**
|
|
9
39
|
* Generate MCP server config file for Claude Code.
|
|
10
40
|
* This file is auto-detected by runClaude and passed via --mcp-config.
|
|
@@ -12,23 +42,26 @@ const MCP_CONFIG_PATH = path.join(MCP_CONFIG_DIR, 'mcp-config.json');
|
|
|
12
42
|
export function setupMcpConfig(config) {
|
|
13
43
|
const knowledgeServerPath = path.join(__dirname, 'knowledge-server.js');
|
|
14
44
|
const browserServerPath = path.join(__dirname, 'browser-server.js');
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
args: [browserServerPath, '--user-data-dir', path.join(process.env.HOME || '', '.config', 'tuna-browser', 'chrome-profile')],
|
|
29
|
-
},
|
|
45
|
+
const mcpServers = {
|
|
46
|
+
'tuna-knowledge': {
|
|
47
|
+
command: process.execPath, // full path to current node binary
|
|
48
|
+
args: [
|
|
49
|
+
knowledgeServerPath,
|
|
50
|
+
'--api-url', config.apiUrl,
|
|
51
|
+
'--token', config.agentToken,
|
|
52
|
+
'--agent-id', config.agentId,
|
|
53
|
+
],
|
|
54
|
+
},
|
|
55
|
+
'tuna-browser': {
|
|
56
|
+
command: process.execPath,
|
|
57
|
+
args: [browserServerPath, '--user-data-dir', path.join(process.env.HOME || '', '.config', 'tuna-browser', 'chrome-profile')],
|
|
30
58
|
},
|
|
31
59
|
};
|
|
60
|
+
const mem0Config = buildMem0McpConfig(config.name);
|
|
61
|
+
if (mem0Config) {
|
|
62
|
+
mcpServers['mem0'] = mem0Config;
|
|
63
|
+
}
|
|
64
|
+
const mcpConfig = { mcpServers };
|
|
32
65
|
if (!fs.existsSync(MCP_CONFIG_DIR)) {
|
|
33
66
|
fs.mkdirSync(MCP_CONFIG_DIR, { recursive: true });
|
|
34
67
|
}
|
|
@@ -61,7 +94,7 @@ export function writeAgentFolderMcpConfig(agentFolderPath, config) {
|
|
|
61
94
|
existing = JSON.parse(fs.readFileSync(mcpJsonPath, 'utf-8'));
|
|
62
95
|
}
|
|
63
96
|
const existingServers = existing.mcpServers ?? {};
|
|
64
|
-
|
|
97
|
+
const newServers = {
|
|
65
98
|
...existingServers,
|
|
66
99
|
'tuna-knowledge': {
|
|
67
100
|
command: process.execPath,
|
|
@@ -77,6 +110,11 @@ export function writeAgentFolderMcpConfig(agentFolderPath, config) {
|
|
|
77
110
|
args: [browserServerPath, '--user-data-dir', path.join(process.env.HOME || '', '.config', 'tuna-browser', 'chrome-profile')],
|
|
78
111
|
},
|
|
79
112
|
};
|
|
113
|
+
const mem0Config = buildMem0McpConfig(config.name);
|
|
114
|
+
if (mem0Config) {
|
|
115
|
+
newServers['mem0'] = mem0Config;
|
|
116
|
+
}
|
|
117
|
+
existing.mcpServers = newServers;
|
|
80
118
|
fs.writeFileSync(mcpJsonPath, JSON.stringify(existing, null, 2));
|
|
81
119
|
console.log(`[MCP] Agent folder .mcp.json written to ${mcpJsonPath}`);
|
|
82
120
|
}
|
|
@@ -32,6 +32,8 @@ export declare const CommandMessageSchema: z.ZodObject<{
|
|
|
32
32
|
agent_folders: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
33
33
|
agent_id: z.ZodOptional<z.ZodString>;
|
|
34
34
|
folder_path: z.ZodOptional<z.ZodString>;
|
|
35
|
+
agent_name: z.ZodOptional<z.ZodString>;
|
|
36
|
+
role_description: z.ZodOptional<z.ZodString>;
|
|
35
37
|
skill_id: z.ZodOptional<z.ZodString>;
|
|
36
38
|
source_path: z.ZodOptional<z.ZodString>;
|
|
37
39
|
skill_name: z.ZodOptional<z.ZodString>;
|
|
@@ -26,6 +26,9 @@ export const CommandMessageSchema = z.object({
|
|
|
26
26
|
// For rescan_agent_skills command
|
|
27
27
|
agent_id: z.string().optional(),
|
|
28
28
|
folder_path: z.string().optional(),
|
|
29
|
+
// For create_agent command
|
|
30
|
+
agent_name: z.string().optional(),
|
|
31
|
+
role_description: z.string().optional(),
|
|
29
32
|
// For analyze_skill command
|
|
30
33
|
skill_id: z.string().optional(),
|
|
31
34
|
source_path: z.string().optional(),
|