prompt-language-shell 0.9.6 → 1.0.0
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/README.md +134 -46
- package/dist/components/Component.js +5 -3
- package/dist/components/controllers/Command.js +3 -19
- package/dist/components/controllers/Config.js +109 -75
- package/dist/components/controllers/Execute.js +17 -14
- package/dist/components/controllers/Introspect.js +2 -4
- package/dist/components/controllers/Validate.js +3 -2
- package/dist/components/views/Execute.js +1 -1
- package/dist/components/views/Output.js +89 -35
- package/dist/components/views/Subtask.js +12 -7
- package/dist/components/views/Task.js +4 -5
- package/dist/components/views/Upcoming.js +1 -1
- package/dist/configuration/io.js +10 -0
- package/dist/configuration/schema.js +6 -0
- package/dist/configuration/validation.js +5 -0
- package/dist/execution/processing.js +45 -14
- package/dist/execution/runner.js +46 -30
- package/dist/index.js +2 -0
- package/dist/services/anthropic.js +5 -4
- package/dist/services/filesystem.js +13 -1
- package/dist/services/logger.js +176 -28
- package/dist/services/messages.js +7 -4
- package/dist/services/monitor.js +304 -0
- package/dist/services/performance.js +14 -0
- package/dist/services/refinement.js +7 -16
- package/dist/services/router.js +223 -95
- package/dist/services/shell.js +49 -16
- package/dist/services/utils.js +11 -0
- package/dist/skills/execute.md +82 -3
- package/dist/skills/schedule.md +7 -0
- package/package.json +11 -10
|
@@ -37,7 +37,10 @@ export function Execute({ tasks: inputTasks, status, service, upcoming, label, r
|
|
|
37
37
|
// Track working directory across commands (persists cd changes)
|
|
38
38
|
const workdirRef = useRef(undefined);
|
|
39
39
|
// Ref to collect live output during execution (dispatched every second)
|
|
40
|
-
const outputRef = useRef({
|
|
40
|
+
const outputRef = useRef({
|
|
41
|
+
chunks: [],
|
|
42
|
+
currentMemory: undefined,
|
|
43
|
+
});
|
|
41
44
|
// Ref to track if current task execution is cancelled
|
|
42
45
|
const cancelledRef = useRef(false);
|
|
43
46
|
const { error, tasks, message, hasProcessed, completionMessage, summary } = localState;
|
|
@@ -62,8 +65,8 @@ export function Execute({ tasks: inputTasks, status, service, upcoming, label, r
|
|
|
62
65
|
index: currentTaskIndex,
|
|
63
66
|
elapsed,
|
|
64
67
|
output: {
|
|
65
|
-
|
|
66
|
-
|
|
68
|
+
chunks: outputRef.current.chunks,
|
|
69
|
+
currentMemory: outputRef.current.currentMemory,
|
|
67
70
|
},
|
|
68
71
|
},
|
|
69
72
|
});
|
|
@@ -83,8 +86,7 @@ export function Execute({ tasks: inputTasks, status, service, upcoming, label, r
|
|
|
83
86
|
...task,
|
|
84
87
|
status: ExecutionStatus.Aborted,
|
|
85
88
|
output: {
|
|
86
|
-
|
|
87
|
-
stderr: outputRef.current.stderr,
|
|
89
|
+
chunks: outputRef.current.chunks,
|
|
88
90
|
},
|
|
89
91
|
};
|
|
90
92
|
}
|
|
@@ -139,9 +141,9 @@ export function Execute({ tasks: inputTasks, status, service, upcoming, label, r
|
|
|
139
141
|
lifecycleHandlers.completeActive();
|
|
140
142
|
return;
|
|
141
143
|
}
|
|
142
|
-
// Create task data from commands
|
|
143
|
-
const tasks = result.commands.map((cmd
|
|
144
|
-
label:
|
|
144
|
+
// Create task data from commands - use descriptions from execute response
|
|
145
|
+
const tasks = result.commands.map((cmd) => ({
|
|
146
|
+
label: cmd.description,
|
|
145
147
|
command: cmd,
|
|
146
148
|
status: ExecutionStatus.Pending,
|
|
147
149
|
elapsed: 0,
|
|
@@ -207,7 +209,7 @@ export function Execute({ tasks: inputTasks, status, service, upcoming, label, r
|
|
|
207
209
|
payload: { index: currentTaskIndex, startTime: Date.now() },
|
|
208
210
|
});
|
|
209
211
|
// Reset output ref for new task
|
|
210
|
-
outputRef.current = {
|
|
212
|
+
outputRef.current = { chunks: [], currentMemory: undefined };
|
|
211
213
|
// Merge workdir into command
|
|
212
214
|
const command = workdirRef.current
|
|
213
215
|
? { ...currentTask.command, workdir: workdirRef.current }
|
|
@@ -215,7 +217,10 @@ export function Execute({ tasks: inputTasks, status, service, upcoming, label, r
|
|
|
215
217
|
void executeTask(command, currentTaskIndex, {
|
|
216
218
|
onUpdate: (output) => {
|
|
217
219
|
if (!cancelledRef.current) {
|
|
218
|
-
outputRef.current = {
|
|
220
|
+
outputRef.current = {
|
|
221
|
+
chunks: output.chunks,
|
|
222
|
+
currentMemory: output.currentMemory,
|
|
223
|
+
};
|
|
219
224
|
}
|
|
220
225
|
},
|
|
221
226
|
onComplete: (elapsed, execOutput) => {
|
|
@@ -229,8 +234,7 @@ export function Execute({ tasks: inputTasks, status, service, upcoming, label, r
|
|
|
229
234
|
? {
|
|
230
235
|
...task,
|
|
231
236
|
output: {
|
|
232
|
-
|
|
233
|
-
stderr: execOutput.stderr,
|
|
237
|
+
chunks: execOutput.chunks,
|
|
234
238
|
},
|
|
235
239
|
}
|
|
236
240
|
: task);
|
|
@@ -256,8 +260,7 @@ export function Execute({ tasks: inputTasks, status, service, upcoming, label, r
|
|
|
256
260
|
? {
|
|
257
261
|
...task,
|
|
258
262
|
output: {
|
|
259
|
-
|
|
260
|
-
stderr: execOutput.stderr,
|
|
263
|
+
chunks: execOutput.chunks,
|
|
261
264
|
},
|
|
262
265
|
error: execOutput.error || undefined,
|
|
263
266
|
}
|
|
@@ -54,10 +54,8 @@ export function Introspect({ tasks, status, service, children, debug = DebugLeve
|
|
|
54
54
|
message,
|
|
55
55
|
};
|
|
56
56
|
requestHandlers.onCompleted(finalState);
|
|
57
|
-
//
|
|
58
|
-
|
|
59
|
-
// Signal completion
|
|
60
|
-
lifecycleHandlers.completeActive();
|
|
57
|
+
// Signal completion and add Report to timeline (not queue, to preserve order)
|
|
58
|
+
lifecycleHandlers.completeActive(createReport({ message, capabilities }));
|
|
61
59
|
}
|
|
62
60
|
}
|
|
63
61
|
catch (err) {
|
|
@@ -90,8 +90,9 @@ export function Validate({ missingConfig, userRequest, status, service, onError,
|
|
|
90
90
|
},
|
|
91
91
|
});
|
|
92
92
|
// Override descriptions with LLM-generated ones
|
|
93
|
-
|
|
94
|
-
|
|
93
|
+
const configProps = configDef.props;
|
|
94
|
+
if (configProps.steps) {
|
|
95
|
+
configProps.steps = configProps.steps.map((step, index) => ({
|
|
95
96
|
...step,
|
|
96
97
|
description: withDescriptions[index].description ||
|
|
97
98
|
withDescriptions[index].path,
|
|
@@ -56,5 +56,5 @@ export const ExecuteView = ({ isLoading, isExecuting, isActive, error, message,
|
|
|
56
56
|
const isTerminated = upcomingStatus !== ExecutionStatus.Pending;
|
|
57
57
|
// Show upcoming during active execution or when terminated (to show skipped tasks)
|
|
58
58
|
const showUpcoming = upcoming && upcoming.length > 0 && (isActive || isTerminated);
|
|
59
|
-
return (_jsxs(Box, { alignSelf: "flex-start", flexDirection: "column", children: [isLoading && (_jsxs(Box, { flexDirection: "column", marginLeft: 1, children: [label && (_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: getTextColor(isActive), children: label }) })), _jsxs(Box, { marginLeft: label ? 2 : 0, children: [_jsx(Text, { color: Palette.Gray, children: "Preparing commands. " }), _jsx(Spinner, {})] })] })), (isExecuting || showTasks) && (_jsxs(Box, { flexDirection: "column", marginLeft: 1, children: [message && (_jsxs(Box, { marginBottom: 1, gap: 1, children: [_jsx(Text, { color: getTextColor(isActive), children: message }), isExecuting && _jsx(Spinner, {})] })), tasks.map((task, index) => (_jsx(Box, { marginBottom: index < tasks.length - 1 ? 1 : 0, children: _jsx(TaskView, { label: task.label, command: task.command, status: task.status, elapsed: task.elapsed, output: task.output, isFinished: isTaskFinished(task), isActive: isActive }) }, index)))] })), showUpcoming && (_jsx(Box, { marginTop: 1, children: _jsx(Upcoming, { items: upcoming, status: upcomingStatus }) })), completionMessage && !isActive && (_jsx(Box, { marginTop: 1, marginLeft: 1, children: _jsx(Text, { color: getTextColor(false), children: completionMessage }) })), error && (_jsx(Box, { marginLeft: 1, children: _jsx(Text, { children: error }) }))] }));
|
|
59
|
+
return (_jsxs(Box, { alignSelf: "flex-start", flexDirection: "column", children: [isLoading && (_jsxs(Box, { flexDirection: "column", marginLeft: 1, children: [label && (_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: getTextColor(isActive), children: label }) })), _jsxs(Box, { marginLeft: label ? 2 : 0, children: [_jsx(Text, { color: Palette.Gray, children: "Preparing commands. " }), _jsx(Spinner, {})] })] })), (isExecuting || showTasks) && (_jsxs(Box, { flexDirection: "column", marginLeft: 1, children: [message && (_jsxs(Box, { marginBottom: 1, gap: 1, children: [_jsx(Text, { color: getTextColor(isActive), children: message }), isExecuting && _jsx(Spinner, {})] })), tasks.map((task, index) => (_jsx(Box, { marginBottom: index < tasks.length - 1 ? 1 : 0, children: _jsx(TaskView, { label: task.label, command: task.command, status: task.status, elapsed: task.elapsed, currentMemory: task.output?.currentMemory, output: task.output, isFinished: isTaskFinished(task), isActive: isActive }) }, index)))] })), showUpcoming && (_jsx(Box, { marginTop: 1, children: _jsx(Upcoming, { items: upcoming, status: upcomingStatus }) })), completionMessage && !isActive && (_jsx(Box, { marginTop: 1, marginLeft: 1, children: _jsx(Text, { color: getTextColor(false), children: completionMessage }) })), error && (_jsx(Box, { marginLeft: 1, children: _jsx(Text, { children: error }) }))] }));
|
|
60
60
|
};
|
|
@@ -1,54 +1,108 @@
|
|
|
1
|
-
import { jsx as _jsx
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
3
|
import { Palette } from '../../services/colors.js';
|
|
4
4
|
import { ExecutionStatus } from '../../services/shell.js';
|
|
5
5
|
const MAX_LINES = 8;
|
|
6
6
|
const MAX_WIDTH = 75;
|
|
7
|
-
const
|
|
8
|
-
const MINIMAL_INFO_THRESHOLD = 2;
|
|
7
|
+
const INCOMPLETE_LINE_DELAY = 3000;
|
|
9
8
|
/**
|
|
10
|
-
*
|
|
9
|
+
* Determine if an incomplete line should be shown.
|
|
10
|
+
* Shows incomplete lines if they are older than the delay threshold.
|
|
11
11
|
*/
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
function shouldShowIncompleteLine(lastChunk) {
|
|
13
|
+
return Date.now() - lastChunk.timestamp >= INCOMPLETE_LINE_DELAY;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Split a line into chunks of maxWidth characters.
|
|
17
|
+
*/
|
|
18
|
+
function splitIntoRows(line, maxWidth) {
|
|
19
|
+
if (line.length <= maxWidth)
|
|
20
|
+
return [line];
|
|
21
|
+
const rows = [];
|
|
22
|
+
for (let i = 0; i < line.length; i += maxWidth) {
|
|
23
|
+
rows.push(line.slice(i, i + maxWidth));
|
|
24
|
+
}
|
|
25
|
+
return rows;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Process text into terminal rows.
|
|
29
|
+
* Handles carriage returns and splits long lines into chunks.
|
|
30
|
+
*/
|
|
31
|
+
function textToRows(text, maxWidth) {
|
|
32
|
+
return text
|
|
14
33
|
.trim()
|
|
15
34
|
.split(/\r?\n/)
|
|
16
|
-
.
|
|
17
|
-
|
|
35
|
+
.map((line) => {
|
|
36
|
+
// Handle carriage returns: keep only content after the last \r
|
|
37
|
+
const lastCR = line.lastIndexOf('\r');
|
|
38
|
+
return lastCR >= 0 ? line.slice(lastCR + 1) : line;
|
|
39
|
+
})
|
|
40
|
+
.filter((line) => line.trim().length > 0)
|
|
41
|
+
.flatMap((line) => splitIntoRows(line, maxWidth));
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Get the last N terminal rows from text.
|
|
45
|
+
* Splits long lines into chunks of maxWidth characters, then takes the last N.
|
|
46
|
+
* Handles carriage returns used in progress output by keeping only the
|
|
47
|
+
* content after the last \r in each line.
|
|
48
|
+
*/
|
|
49
|
+
export function getLastLines(text, maxLines = MAX_LINES, maxWidth = MAX_WIDTH) {
|
|
50
|
+
const rows = textToRows(text, maxWidth);
|
|
51
|
+
return rows.length <= maxLines ? rows : rows.slice(-maxLines);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Convert output chunks to terminal rows.
|
|
55
|
+
* Sorts by timestamp, combines text, deduplicates adjacent lines.
|
|
56
|
+
* When not finished, only shows complete lines (ending with newline),
|
|
57
|
+
* unless the last chunk is older than INCOMPLETE_LINE_DELAY.
|
|
58
|
+
*/
|
|
59
|
+
export function chunksToRows(chunks, maxLines = MAX_LINES, maxWidth = MAX_WIDTH, isFinished = true) {
|
|
60
|
+
if (chunks.length === 0)
|
|
61
|
+
return [];
|
|
62
|
+
// Sort by timestamp and combine text (chunks already contain line endings)
|
|
63
|
+
const sorted = [...chunks].sort((a, b) => a.timestamp - b.timestamp);
|
|
64
|
+
let combined = sorted.map((c) => c.text).join('');
|
|
65
|
+
// When not finished, only show complete lines (strip trailing incomplete line)
|
|
66
|
+
// unless the last chunk is old enough to be shown
|
|
67
|
+
if (!isFinished && !combined.endsWith('\n')) {
|
|
68
|
+
const lastChunk = sorted[sorted.length - 1];
|
|
69
|
+
if (!shouldShowIncompleteLine(lastChunk)) {
|
|
70
|
+
const lastNewline = combined.lastIndexOf('\n');
|
|
71
|
+
if (lastNewline >= 0) {
|
|
72
|
+
combined = combined.slice(0, lastNewline + 1);
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
// No complete lines yet
|
|
76
|
+
return [];
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// Convert to rows
|
|
81
|
+
const rows = textToRows(combined, maxWidth);
|
|
82
|
+
// Deduplicate adjacent identical lines
|
|
83
|
+
const deduplicated = rows.filter((row, index) => index === 0 || row !== rows[index - 1]);
|
|
84
|
+
return deduplicated.length <= maxLines
|
|
85
|
+
? deduplicated
|
|
86
|
+
: deduplicated.slice(-maxLines);
|
|
18
87
|
}
|
|
19
88
|
/**
|
|
20
89
|
* Compute display configuration for output rendering.
|
|
21
|
-
* Encapsulates the logic for what to show and how to style it.
|
|
22
90
|
*/
|
|
23
|
-
export function computeDisplayConfig(
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
91
|
+
export function computeDisplayConfig(chunks, status, isFinished) {
|
|
92
|
+
if (chunks.length === 0)
|
|
93
|
+
return null;
|
|
94
|
+
const rows = chunksToRows(chunks, MAX_LINES, MAX_WIDTH, isFinished);
|
|
95
|
+
if (rows.length === 0)
|
|
27
96
|
return null;
|
|
28
|
-
|
|
29
|
-
const stderrLines = hasStderr ? getLastLines(stderr) : [];
|
|
30
|
-
// Show stdout if no stderr, or if stderr is minimal (provides context)
|
|
31
|
-
const showStdout = hasStdout && (!hasStderr || stderrLines.length <= MINIMAL_INFO_THRESHOLD);
|
|
32
|
-
// Use word wrapping for short outputs to show more detail
|
|
33
|
-
const totalLines = stdoutLines.length + stderrLines.length;
|
|
34
|
-
const wrapMode = totalLines <= SHORT_OUTPUT_THRESHOLD ? 'wrap' : 'truncate-end';
|
|
35
|
-
// Darker colors for finished tasks
|
|
97
|
+
// Use yellow for failed status, otherwise gray (darker if finished)
|
|
36
98
|
const baseColor = isFinished ? Palette.DarkGray : Palette.Gray;
|
|
37
|
-
const
|
|
38
|
-
return {
|
|
39
|
-
stdoutLines,
|
|
40
|
-
stderrLines,
|
|
41
|
-
showStdout,
|
|
42
|
-
wrapMode,
|
|
43
|
-
stdoutColor: baseColor,
|
|
44
|
-
stderrColor,
|
|
45
|
-
};
|
|
99
|
+
const color = status === ExecutionStatus.Failed ? Palette.Yellow : baseColor;
|
|
100
|
+
return { rows, color };
|
|
46
101
|
}
|
|
47
|
-
export function Output({
|
|
48
|
-
const config = computeDisplayConfig(
|
|
102
|
+
export function Output({ chunks, status, isFinished }) {
|
|
103
|
+
const config = computeDisplayConfig(chunks, status, isFinished ?? false);
|
|
49
104
|
if (!config)
|
|
50
105
|
return null;
|
|
51
|
-
const {
|
|
52
|
-
return (
|
|
53
|
-
stdoutLines.map((line, index) => (_jsx(Text, { color: stdoutColor, wrap: wrapMode, children: line }, `out-${index}`))), stderrLines.map((line, index) => (_jsx(Text, { color: stderrColor, wrap: wrapMode, children: line }, `err-${index}`)))] }));
|
|
106
|
+
const { rows, color } = config;
|
|
107
|
+
return (_jsx(Box, { marginTop: 1, marginLeft: 5, flexDirection: "column", width: MAX_WIDTH, children: rows.map((row, index) => (_jsx(Text, { color: color, wrap: "truncate", children: row }, index))) }));
|
|
54
108
|
}
|
|
@@ -1,25 +1,30 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
|
+
import { loadDebugSetting } from '../../configuration/io.js';
|
|
4
|
+
import { DebugLevel } from '../../configuration/types.js';
|
|
3
5
|
import { getStatusColors, Palette, STATUS_ICONS, } from '../../services/colors.js';
|
|
4
6
|
import { ExecutionStatus } from '../../services/shell.js';
|
|
5
|
-
import { formatDuration } from '../../services/utils.js';
|
|
7
|
+
import { formatDuration, formatMemory } from '../../services/utils.js';
|
|
6
8
|
import { Spinner } from './Spinner.js';
|
|
7
9
|
/**
|
|
8
10
|
* Pure display component for a single subtask.
|
|
9
11
|
* Shows label, command, status icon, and elapsed time.
|
|
10
12
|
*/
|
|
11
|
-
export function SubtaskView({ label, command, status, elapsed, }) {
|
|
13
|
+
export function SubtaskView({ label, command, status, elapsed, currentMemory, }) {
|
|
12
14
|
const colors = getStatusColors(status);
|
|
15
|
+
const debugLevel = loadDebugSetting();
|
|
16
|
+
const isVerbose = debugLevel === DebugLevel.Verbose;
|
|
13
17
|
const isCancelled = status === ExecutionStatus.Cancelled;
|
|
14
18
|
const isAborted = status === ExecutionStatus.Aborted;
|
|
19
|
+
const isRunning = status === ExecutionStatus.Running;
|
|
15
20
|
const isFinished = status === ExecutionStatus.Success ||
|
|
16
21
|
status === ExecutionStatus.Failed ||
|
|
17
22
|
status === ExecutionStatus.Aborted;
|
|
18
23
|
// Apply strikethrough for cancelled and aborted tasks
|
|
19
24
|
const shouldStrikethrough = isCancelled || isAborted;
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
+
// Show memory in verbose mode while running
|
|
26
|
+
const showMemory = isVerbose && isRunning && currentMemory !== undefined;
|
|
27
|
+
// Build time/memory display
|
|
28
|
+
const showTimeInfo = (isFinished || isRunning) && elapsed !== undefined;
|
|
29
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { paddingLeft: 2, gap: 1, children: [_jsx(Text, { color: colors.icon, children: STATUS_ICONS[status] }), _jsx(Text, { color: colors.description, strikethrough: shouldStrikethrough, children: label || command.description }), showTimeInfo && (_jsxs(Text, { color: Palette.DarkGray, children: ["(", formatDuration(elapsed), ")"] })), showMemory && (_jsx(Text, { color: Palette.Yellow, children: formatMemory(currentMemory) }))] }), _jsxs(Box, { paddingLeft: 5, flexDirection: "row", children: [_jsx(Box, { children: _jsx(Text, { color: colors.symbol, children: "\u221F " }) }), _jsxs(Box, { gap: 1, children: [_jsx(Text, { color: colors.command, children: command.command }), isRunning && _jsx(Spinner, {})] })] })] }));
|
|
25
30
|
}
|
|
@@ -6,13 +6,12 @@ import { Output } from './Output.js';
|
|
|
6
6
|
import { SubtaskView } from './Subtask.js';
|
|
7
7
|
/**
|
|
8
8
|
* Pure display component for a task.
|
|
9
|
-
* Combines SubtaskView (label/command/status) with Output (
|
|
9
|
+
* Combines SubtaskView (label/command/status) with Output (chunks).
|
|
10
10
|
* Output is shown during active execution, or in timeline only with debug mode.
|
|
11
11
|
*/
|
|
12
|
-
export function TaskView({ label, command, status, elapsed, output, isFinished, isActive = false, }) {
|
|
13
|
-
const
|
|
14
|
-
const stderr = output?.stderr ?? '';
|
|
12
|
+
export function TaskView({ label, command, status, elapsed, currentMemory, output, isFinished, isActive = false, }) {
|
|
13
|
+
const chunks = output?.chunks ?? [];
|
|
15
14
|
// Show output during active execution, or in timeline only with debug enabled
|
|
16
15
|
const showOutput = isActive || loadDebugSetting() !== DebugLevel.None;
|
|
17
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(SubtaskView, { label: label, command: command, status: status, elapsed: elapsed }), showOutput && (_jsx(Output, {
|
|
16
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(SubtaskView, { label: label, command: command, status: status, elapsed: elapsed, currentMemory: currentMemory }), showOutput && (_jsx(Output, { chunks: chunks, isFinished: isFinished, status: status }))] }));
|
|
18
17
|
}
|
|
@@ -25,6 +25,6 @@ export const Upcoming = ({ items, status = ExecutionStatus.Pending, }) => {
|
|
|
25
25
|
return (_jsxs(Box, { flexDirection: "column", marginLeft: 1, children: [_jsx(Text, { color: Palette.Gray, children: STATUS_LABELS[status] }), items.map((name, index) => {
|
|
26
26
|
const isLast = index === items.length - 1;
|
|
27
27
|
const symbol = isLast ? BRANCH_LAST : BRANCH_MIDDLE;
|
|
28
|
-
return (
|
|
28
|
+
return (_jsxs(Box, { marginLeft: 1, gap: 1, children: [_jsx(Text, { color: Palette.DarkGray, children: symbol }), _jsx(Text, { color: Palette.DarkGray, strikethrough: strikethrough, children: name })] }, index));
|
|
29
29
|
})] }));
|
|
30
30
|
};
|
package/dist/configuration/io.js
CHANGED
|
@@ -104,3 +104,13 @@ export function loadDebugSetting(fs = defaultFileSystem) {
|
|
|
104
104
|
return DebugLevel.None;
|
|
105
105
|
}
|
|
106
106
|
}
|
|
107
|
+
const DEFAULT_MEMORY_LIMIT = 1024;
|
|
108
|
+
export function loadMemorySetting(fs = defaultFileSystem) {
|
|
109
|
+
try {
|
|
110
|
+
const config = loadConfig(fs);
|
|
111
|
+
return config.settings?.memory ?? DEFAULT_MEMORY_LIMIT;
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return DEFAULT_MEMORY_LIMIT;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -38,6 +38,12 @@ const coreConfigSchema = {
|
|
|
38
38
|
default: DebugLevel.None,
|
|
39
39
|
description: 'Debug mode',
|
|
40
40
|
},
|
|
41
|
+
'settings.memory': {
|
|
42
|
+
type: ConfigDefinitionType.Number,
|
|
43
|
+
required: false,
|
|
44
|
+
default: 1024,
|
|
45
|
+
description: 'Child process memory limit (MB)',
|
|
46
|
+
},
|
|
41
47
|
};
|
|
42
48
|
/**
|
|
43
49
|
* Get complete configuration schema
|
|
@@ -37,6 +37,11 @@ export function validateConfig(parsed) {
|
|
|
37
37
|
validatedConfig.settings.debug = settings.debug;
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
|
+
if ('memory' in settings) {
|
|
41
|
+
if (typeof settings.memory === 'number' && settings.memory > 0) {
|
|
42
|
+
validatedConfig.settings.memory = settings.memory;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
40
45
|
}
|
|
41
46
|
return validatedConfig;
|
|
42
47
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { stringify } from 'yaml';
|
|
2
|
+
import { loadMemorySetting } from '../configuration/io.js';
|
|
1
3
|
import { loadUserConfig } from '../services/loader.js';
|
|
2
4
|
import { replacePlaceholders } from '../services/resolver.js';
|
|
3
5
|
import { validatePlaceholderResolution } from './validation.js';
|
|
@@ -10,6 +12,36 @@ export function fixEscapedQuotes(command) {
|
|
|
10
12
|
// Replace ="value" with =\"value\"
|
|
11
13
|
return command.replace(/="([^"]*)"/g, '=\\"$1\\"');
|
|
12
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* Format a task as YAML with action line and metadata block
|
|
17
|
+
*/
|
|
18
|
+
export function formatTaskAsYaml(action, metadata, indent = '') {
|
|
19
|
+
const normalizedAction = action.charAt(0).toLowerCase() + action.slice(1);
|
|
20
|
+
if (!metadata || Object.keys(metadata).length === 0) {
|
|
21
|
+
return normalizedAction;
|
|
22
|
+
}
|
|
23
|
+
const metadataYaml = stringify({ metadata })
|
|
24
|
+
.trim()
|
|
25
|
+
.split('\n')
|
|
26
|
+
.map((line) => `${indent}${line}`)
|
|
27
|
+
.join('\n');
|
|
28
|
+
return `${normalizedAction}\n\n${metadataYaml}`;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Build task descriptions for the LLM
|
|
32
|
+
* Single task: use as-is; multiple tasks: add header and bullet prefix
|
|
33
|
+
*/
|
|
34
|
+
function buildTaskDescriptions(resolvedTasks) {
|
|
35
|
+
if (resolvedTasks.length === 1) {
|
|
36
|
+
const { action, params } = resolvedTasks[0];
|
|
37
|
+
return formatTaskAsYaml(action, params);
|
|
38
|
+
}
|
|
39
|
+
const header = `complete these ${resolvedTasks.length} tasks:`;
|
|
40
|
+
const bulletedTasks = resolvedTasks
|
|
41
|
+
.map(({ action, params }) => `- ${formatTaskAsYaml(action, params, ' ')}`)
|
|
42
|
+
.join('\n\n');
|
|
43
|
+
return `${header}\n\n${bulletedTasks}`;
|
|
44
|
+
}
|
|
13
45
|
/**
|
|
14
46
|
* Processes tasks through the AI service to generate executable commands.
|
|
15
47
|
* Resolves placeholders in task descriptions and validates the results.
|
|
@@ -17,27 +49,26 @@ export function fixEscapedQuotes(command) {
|
|
|
17
49
|
export async function processTasks(tasks, service) {
|
|
18
50
|
// Load user config for placeholder resolution
|
|
19
51
|
const userConfig = loadUserConfig();
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
return `- ${resolvedAction}${params}`;
|
|
28
|
-
})
|
|
29
|
-
.join('\n');
|
|
30
|
-
// Build message with confirmed schedule header
|
|
31
|
-
const taskDescriptions = `Confirmed schedule (${tasks.length} tasks):\n${taskList}`;
|
|
52
|
+
const memoryLimitMB = loadMemorySetting();
|
|
53
|
+
// Resolve placeholders in task actions
|
|
54
|
+
const resolvedTasks = tasks.map((task) => ({
|
|
55
|
+
action: replacePlaceholders(task.action, userConfig),
|
|
56
|
+
params: task.params,
|
|
57
|
+
}));
|
|
58
|
+
const taskDescriptions = buildTaskDescriptions(resolvedTasks);
|
|
32
59
|
// Call execute tool to get commands
|
|
33
60
|
const result = await service.processWithTool(taskDescriptions, 'execute');
|
|
34
|
-
// Resolve placeholders in command strings
|
|
61
|
+
// Resolve placeholders in command strings and inject memory limit
|
|
35
62
|
const resolvedCommands = (result.commands || []).map((cmd) => {
|
|
36
63
|
// Fix escaped quotes lost in JSON parsing
|
|
37
64
|
const fixed = fixEscapedQuotes(cmd.command);
|
|
38
65
|
const resolved = replacePlaceholders(fixed, userConfig);
|
|
39
66
|
validatePlaceholderResolution(resolved);
|
|
40
|
-
return {
|
|
67
|
+
return {
|
|
68
|
+
...cmd,
|
|
69
|
+
command: resolved,
|
|
70
|
+
memoryLimit: memoryLimitMB,
|
|
71
|
+
};
|
|
41
72
|
});
|
|
42
73
|
return {
|
|
43
74
|
message: result.message,
|
package/dist/execution/runner.js
CHANGED
|
@@ -1,37 +1,31 @@
|
|
|
1
|
-
import { ExecutionResult, ExecutionStatus, executeCommand, setOutputCallback, } from '../services/shell.js';
|
|
1
|
+
import { ExecutionResult, ExecutionStatus, executeCommand, setMemoryCallback, setOutputCallback, } from '../services/shell.js';
|
|
2
2
|
import { calculateElapsed } from '../services/utils.js';
|
|
3
|
-
// Maximum number of output
|
|
4
|
-
const
|
|
5
|
-
/**
|
|
6
|
-
* Limit output to last MAX_OUTPUT_LINES lines to prevent memory exhaustion
|
|
7
|
-
*/
|
|
8
|
-
function limitLines(output) {
|
|
9
|
-
const lines = output.split('\n');
|
|
10
|
-
return lines.slice(-MAX_OUTPUT_LINES).join('\n');
|
|
11
|
-
}
|
|
3
|
+
// Maximum number of output chunks to keep in memory
|
|
4
|
+
const MAX_OUTPUT_CHUNKS = 256;
|
|
12
5
|
/**
|
|
13
6
|
* Execute a single task and track its progress.
|
|
14
7
|
* All execution logic is contained here, outside of React components.
|
|
15
8
|
*/
|
|
16
9
|
export async function executeTask(command, index, callbacks) {
|
|
17
10
|
const startTime = Date.now();
|
|
18
|
-
let
|
|
19
|
-
let stderr = '';
|
|
11
|
+
let chunks = [];
|
|
20
12
|
let error = '';
|
|
21
13
|
let workdir;
|
|
14
|
+
let currentMemory;
|
|
22
15
|
// Helper to create current output snapshot
|
|
23
16
|
const createOutput = () => ({
|
|
24
|
-
|
|
25
|
-
stderr,
|
|
17
|
+
chunks,
|
|
26
18
|
error,
|
|
27
19
|
workdir,
|
|
20
|
+
currentMemory,
|
|
28
21
|
});
|
|
29
|
-
// Throttle updates to avoid excessive re-renders (
|
|
22
|
+
// Throttle updates to avoid excessive re-renders (80ms minimum interval)
|
|
30
23
|
let lastUpdateTime = 0;
|
|
31
24
|
let pendingTimeout;
|
|
25
|
+
const THROTTLE_INTERVAL = 80;
|
|
32
26
|
const throttledUpdate = () => {
|
|
33
27
|
const now = Date.now();
|
|
34
|
-
if (now - lastUpdateTime >=
|
|
28
|
+
if (now - lastUpdateTime >= THROTTLE_INTERVAL) {
|
|
35
29
|
lastUpdateTime = now;
|
|
36
30
|
callbacks.onUpdate(createOutput());
|
|
37
31
|
}
|
|
@@ -40,29 +34,50 @@ export async function executeTask(command, index, callbacks) {
|
|
|
40
34
|
pendingTimeout = undefined;
|
|
41
35
|
lastUpdateTime = Date.now();
|
|
42
36
|
callbacks.onUpdate(createOutput());
|
|
43
|
-
},
|
|
37
|
+
}, THROTTLE_INTERVAL - (now - lastUpdateTime));
|
|
44
38
|
}
|
|
45
39
|
};
|
|
46
|
-
// Set up output streaming callback
|
|
40
|
+
// Set up output streaming callback - store chunks with timestamps
|
|
47
41
|
setOutputCallback((data, stream) => {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
42
|
+
chunks.push({
|
|
43
|
+
text: data,
|
|
44
|
+
timestamp: Date.now(),
|
|
45
|
+
source: stream,
|
|
46
|
+
});
|
|
47
|
+
// Limit chunks to prevent memory exhaustion
|
|
48
|
+
if (chunks.length > MAX_OUTPUT_CHUNKS) {
|
|
49
|
+
chunks = chunks.slice(-MAX_OUTPUT_CHUNKS);
|
|
53
50
|
}
|
|
54
51
|
throttledUpdate();
|
|
55
52
|
});
|
|
53
|
+
// Set up memory callback to track current memory
|
|
54
|
+
setMemoryCallback((memoryMB) => {
|
|
55
|
+
currentMemory = memoryMB;
|
|
56
|
+
throttledUpdate();
|
|
57
|
+
});
|
|
56
58
|
try {
|
|
57
59
|
const result = await executeCommand(command, undefined, index);
|
|
58
|
-
// Clear
|
|
60
|
+
// Clear callbacks and pending timeout
|
|
59
61
|
setOutputCallback(undefined);
|
|
62
|
+
setMemoryCallback(undefined);
|
|
60
63
|
clearTimeout(pendingTimeout);
|
|
61
64
|
const elapsed = calculateElapsed(startTime);
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
stderr = result.errors;
|
|
65
|
+
const now = Date.now();
|
|
66
|
+
// Update workdir from result
|
|
65
67
|
workdir = result.workdir;
|
|
68
|
+
// Add final output/errors as chunks only if not already captured during streaming
|
|
69
|
+
const hasStreamedStdout = chunks.some((c) => c.source === 'stdout');
|
|
70
|
+
const hasStreamedStderr = chunks.some((c) => c.source === 'stderr');
|
|
71
|
+
if (result.output && result.output.trim() && !hasStreamedStdout) {
|
|
72
|
+
chunks.push({ text: result.output, timestamp: now, source: 'stdout' });
|
|
73
|
+
}
|
|
74
|
+
if (result.errors && result.errors.trim() && !hasStreamedStderr) {
|
|
75
|
+
chunks.push({
|
|
76
|
+
text: result.errors,
|
|
77
|
+
timestamp: now + 1,
|
|
78
|
+
source: 'stderr',
|
|
79
|
+
});
|
|
80
|
+
}
|
|
66
81
|
if (result.result === ExecutionResult.Success) {
|
|
67
82
|
const output = createOutput();
|
|
68
83
|
callbacks.onUpdate(output);
|
|
@@ -70,7 +85,7 @@ export async function executeTask(command, index, callbacks) {
|
|
|
70
85
|
return { status: ExecutionStatus.Success, elapsed, output };
|
|
71
86
|
}
|
|
72
87
|
else {
|
|
73
|
-
const errorMsg = result.
|
|
88
|
+
const errorMsg = result.error || result.errors || 'Command failed';
|
|
74
89
|
error = errorMsg;
|
|
75
90
|
const output = createOutput();
|
|
76
91
|
callbacks.onUpdate(output);
|
|
@@ -79,8 +94,9 @@ export async function executeTask(command, index, callbacks) {
|
|
|
79
94
|
}
|
|
80
95
|
}
|
|
81
96
|
catch (err) {
|
|
82
|
-
// Clear
|
|
97
|
+
// Clear callbacks and pending timeout
|
|
83
98
|
setOutputCallback(undefined);
|
|
99
|
+
setMemoryCallback(undefined);
|
|
84
100
|
clearTimeout(pendingTimeout);
|
|
85
101
|
const elapsed = calculateElapsed(startTime);
|
|
86
102
|
const errorMsg = err instanceof Error ? err.message : 'Unknown error';
|
|
@@ -95,5 +111,5 @@ export async function executeTask(command, index, callbacks) {
|
|
|
95
111
|
* Create an empty execution output
|
|
96
112
|
*/
|
|
97
113
|
export function createEmptyOutput() {
|
|
98
|
-
return {
|
|
114
|
+
return { chunks: [], error: '' };
|
|
99
115
|
}
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,9 @@ import { dirname, join } from 'path';
|
|
|
5
5
|
import { fileURLToPath } from 'url';
|
|
6
6
|
import { render } from 'ink';
|
|
7
7
|
import { DebugLevel } from './configuration/types.js';
|
|
8
|
+
import { preventPerformanceBufferOverflow } from './services/performance.js';
|
|
8
9
|
import { Main } from './Main.js';
|
|
10
|
+
preventPerformanceBufferOverflow();
|
|
9
11
|
const __filename = fileURLToPath(import.meta.url);
|
|
10
12
|
const __dirname = dirname(__filename);
|
|
11
13
|
// Get package info
|