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 styleFlag = style ? `--style ${style} ` : '';
169
- const args = `${styleFlag}${idea}`.trim();
170
- const expandedTemplate = template.replace(/\$ARGUMENTS/g, args);
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. Arguments: ${args}`,
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
- // Progress: 0 → 100
327
- for (let p = 10; p <= 90; p += 10) {
328
- await _sleep(600);
329
- record.progress = p;
330
- upsertVideo(record);
331
- ws.sendExtensionEvent(code, { type: 'render_progress', progress: p });
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
- // TODO: integrate real Remotion render
334
- // Real impl: call /render skill or Remotion CLI with scene clips + voiceover
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, { ok: true, videoId: record.id });
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');
@@ -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 mcpConfig = {
16
- mcpServers: {
17
- 'tuna-knowledge': {
18
- command: process.execPath, // full path to current node binary
19
- args: [
20
- knowledgeServerPath,
21
- '--api-url', config.apiUrl,
22
- '--token', config.agentToken,
23
- '--agent-id', config.agentId,
24
- ],
25
- },
26
- 'tuna-browser': {
27
- command: process.execPath,
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
- existing.mcpServers = {
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(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tuna-agent",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Tuna Agent - Run AI coding tasks on your machine",
5
5
  "bin": {
6
6
  "tuna-agent": "dist/cli/index.js"