tuna-agent 0.1.79 → 0.1.81

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.
package/dist/cli/index.js CHANGED
File without changes
@@ -113,21 +113,47 @@ export async function handleGenerateIdeas(ws, code, taskId, topic, styleName, st
113
113
  ? `- Titles must be in Vietnamese, natural and engaging`
114
114
  : `- Titles must be in English, natural and engaging`;
115
115
  const n = count && count > 0 ? count : 10;
116
- const promptParts = [
117
- `Generate exactly ${n} viral YouTube Shorts video ideas for the topic: "${topic}".`,
118
- ``,
119
- `Requirements:`,
120
- `- Each idea is a catchy, scroll-stopping video title`,
121
- `- Use proven viral patterns: POV, "X điều...", plot twist, emotional hook, controversial take`,
122
- langReq,
123
- `- Mix different angles/formats for variety`,
124
- ...(styleName ? [`- Ideas must fit the "${styleName}" video style${styleDesc ? ` (${styleDesc})` : ''}`] : []),
125
- ];
116
+ let promptParts;
126
117
  if (appContext) {
127
- promptParts.push(`- Each idea must naturally promote "${appContext.appName}" feel like a real user sharing their experience`);
128
- if (!appContext.isWholeApp && appContext.features.length > 0) {
129
- promptParts.push(`- Focus ideas on: ${appContext.features.join(', ')}`);
130
- }
118
+ // UGC app promopersonal story ideas, NOT generic viral titles
119
+ const featureFocus = appContext.isWholeApp
120
+ ? `the entire app "${appContext.appName}"`
121
+ : appContext.features.length > 0
122
+ ? `these features of "${appContext.appName}": ${appContext.features.join(', ')}`
123
+ : `the app "${appContext.appName}"`;
124
+ promptParts = [
125
+ `Generate exactly ${n} UGC (User Generated Content) video ideas promoting ${featureFocus}.`,
126
+ ``,
127
+ `Each idea must be a SHORT SCENARIO DESCRIPTION (1-2 sentences) that includes:`,
128
+ `- A relatable problem/situation the user faces`,
129
+ `- How the app/feature solves it`,
130
+ `- The emotional angle (surprise, relief, excitement)`,
131
+ ``,
132
+ `Example ideas:`,
133
+ `- "Mình đang diet mà không biết tô phở bao nhiêu calo, scan thử và bất ngờ vì kết quả"`,
134
+ `- "Mỗi ngày ghi lại bữa ăn bằng app này, sau 30 ngày nhìn lại thói quen ăn uống thay đổi hẳn"`,
135
+ ``,
136
+ `Rules:`,
137
+ langReq,
138
+ `- Frame as a PERSONAL STORY — "I was struggling with X until..." NOT "App Y has feature Z"`,
139
+ `- Each idea = ONE specific use case, not a feature list`,
140
+ `- Tone: conversational, like telling a friend — NOT a product pitch`,
141
+ `- Mix different angles: problem→solution, before→after, surprise discovery, daily routine`,
142
+ ...(styleName ? [`- Style: "${styleName}"${styleDesc ? ` (${styleDesc})` : ''}`] : []),
143
+ ];
144
+ }
145
+ else {
146
+ // Generic viral ideas
147
+ promptParts = [
148
+ `Generate exactly ${n} viral YouTube Shorts video ideas for the topic: "${topic}".`,
149
+ ``,
150
+ `Requirements:`,
151
+ `- Each idea is a catchy, scroll-stopping video title`,
152
+ `- Use proven viral patterns: POV, "X điều...", plot twist, emotional hook, controversial take`,
153
+ langReq,
154
+ `- Mix different angles/formats for variety`,
155
+ ...(styleName ? [`- Ideas must fit the "${styleName}" video style${styleDesc ? ` (${styleDesc})` : ''}`] : []),
156
+ ];
131
157
  }
132
158
  promptParts.push(``, `Respond with ONLY a JSON array of ${n} strings. No explanation, no markdown, no wrapping.`, `Example format: ["title 1","title 2","title 3","title 4","title 5"]`);
133
159
  const prompt = promptParts.join('\n');
@@ -1,7 +1,6 @@
1
1
  import path from 'path';
2
2
  import os from 'os';
3
3
  import { spawn } from 'child_process';
4
- import { StringDecoder } from 'string_decoder';
5
4
  import { runClaude } from '../utils/claude-cli.js';
6
5
  import { validatePath } from '../utils/validate-path.js';
7
6
  const NEEDS_INPUT_MARKER = '"status":"NEEDS_INPUT"';
@@ -23,9 +22,6 @@ export async function runTask(task, onProgress, signal, confirmBeforeEdit) {
23
22
  if (confirmBeforeEdit) {
24
23
  args.push('--permission-mode', 'default');
25
24
  }
26
- else {
27
- args.push('--permission-mode', 'bypassPermissions');
28
- }
29
25
  const env = {
30
26
  ...process.env,
31
27
  HOME: process.env.HOME || '',
@@ -83,9 +79,8 @@ export async function runTask(task, onProgress, signal, confirmBeforeEdit) {
83
79
  let stdout = '';
84
80
  let stderr = '';
85
81
  let buffer = '';
86
- const stdoutDecoder = new StringDecoder('utf8');
87
82
  proc.stdout.on('data', (chunk) => {
88
- const text = stdoutDecoder.write(chunk);
83
+ const text = chunk.toString();
89
84
  stdout += text;
90
85
  buffer += text;
91
86
  const lines = buffer.split('\n');
@@ -102,9 +97,8 @@ export async function runTask(task, onProgress, signal, confirmBeforeEdit) {
102
97
  }
103
98
  }
104
99
  });
105
- const stderrDecoder = new StringDecoder('utf8');
106
100
  proc.stderr.on('data', (chunk) => {
107
- stderr += stderrDecoder.write(chunk);
101
+ stderr += chunk.toString();
108
102
  });
109
103
  proc.on('close', (code) => {
110
104
  clearTimeout(timeoutTimer);
@@ -431,15 +425,8 @@ export async function executeSubtask(subtask, repoPath, contracts, callbacks, si
431
425
  }
432
426
  else {
433
427
  // Fallback: detect if AI just wrote questions as text without using NEEDS_INPUT
434
- // Only trigger for very short outputs (< 500 chars) that completed quickly (< 15s)
435
- // This avoids false positives when a completed task output contains sentences with "?"
436
- const isShortOutput = result.result.length < 500;
437
- const isQuickRun = result.durationMs != null && result.durationMs < 15000;
438
- const fallbackQuestion = (isShortOutput && isQuickRun)
439
- ? parseQuestionFromOutput(result.result, subtask.id)
440
- : null;
441
- const seemsLikeQuestion = !!fallbackQuestion;
442
- if (seemsLikeQuestion && fallbackQuestion && callbacks?.onSubtaskNeedsInput) {
428
+ const fallbackQuestion = parseQuestionFromOutput(result.result, subtask.id);
429
+ if (fallbackQuestion && callbacks?.onSubtaskNeedsInput) {
443
430
  info.status = 'waiting_input';
444
431
  info.pendingQuestion = fallbackQuestion;
445
432
  log(info, 'thinking', `Fallback: detected question in output (no NEEDS_INPUT marker): ${fallbackQuestion.question}`);
@@ -490,6 +477,15 @@ export async function executeSubtask(subtask, repoPath, contracts, callbacks, si
490
477
  log(info, 'thinking', 'No answer received — task paused');
491
478
  }
492
479
  }
480
+ else if (looksIncomplete(result.result)) {
481
+ info.status = 'failed';
482
+ info.result = result.result;
483
+ log(info, 'error', `Detected incomplete output — marking as failed`);
484
+ callbacks?.onSubtaskLog?.(subtask.id, {
485
+ type: 'error',
486
+ message: `❌ Task did not complete: AI could not fulfill the request`,
487
+ });
488
+ }
493
489
  else {
494
490
  info.status = 'done';
495
491
  info.result = result.result;
@@ -586,6 +582,26 @@ export async function executeTaskWithPlan(task, plan, onProgress, callbacks, sig
586
582
  console.log(`[Executor] Total time: ${(totalTime / 1000).toFixed(1)}s`);
587
583
  return { sessions: allSessions, status: 'done' };
588
584
  }
585
+ /**
586
+ * Detect if Claude's output indicates it could NOT complete the task.
587
+ * Checks the last ~500 chars for common failure/inability patterns.
588
+ */
589
+ function looksIncomplete(output) {
590
+ // Check only the tail — that's where the conclusion lives
591
+ const tail = output.slice(-500).toLowerCase();
592
+ const patterns = [
593
+ /i (?:was |am )?(?:unable|not able) to/,
594
+ /(?:could|can)(?:n't|not) (?:find|locate|access|complete|proceed|continue)/,
595
+ /(?:doesn't|does not|don't|do not) (?:exist|have access)/,
596
+ /(?:no (?:such|matching) (?:file|directory|path|repo))/,
597
+ /(?:not found|file not found|directory not found)/,
598
+ /(?:i need (?:you to|more information|the .* path|access))/,
599
+ /(?:please (?:provide|specify|confirm|check))/,
600
+ /(?:unfortunately|i apologize).{0,50}(?:cannot|couldn't|unable|can't)/,
601
+ /(?:blocked|stuck).{0,30}(?:because|due to|cannot)/,
602
+ ];
603
+ return patterns.some((p) => p.test(tail));
604
+ }
589
605
  /**
590
606
  * Parse question from Claude output when question.json is missing.
591
607
  * Fallback for when Claude outputs NEEDS_INPUT but didn't create the file.
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tuna-agent",
3
- "version": "0.1.79",
3
+ "version": "0.1.81",
4
4
  "description": "Tuna Agent - Run AI coding tasks on your machine",
5
5
  "bin": {
6
6
  "tuna-agent": "dist/cli/index.js"