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
|
-
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
118
|
+
// UGC app promo — personal 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 =
|
|
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 +=
|
|
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
|
-
|
|
435
|
-
|
|
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
|