prompt-language-shell 0.9.6 → 0.9.8
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 +37 -6
- package/dist/components/controllers/Execute.js +3 -3
- package/dist/components/views/Output.js +10 -8
- 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 +1 -1
- package/dist/index.js +2 -0
- package/dist/services/filesystem.js +13 -1
- package/dist/services/logger.js +140 -20
- package/dist/services/messages.js +7 -4
- package/dist/services/monitor.js +288 -0
- package/dist/services/performance.js +14 -0
- package/dist/services/refinement.js +6 -9
- package/dist/services/shell.js +32 -14
- package/dist/skills/execute.md +82 -3
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -31,7 +31,7 @@ Here's what I can help with:
|
|
|
31
31
|
- Configure - manage and configure system settings
|
|
32
32
|
- Answer - respond to questions and provide information
|
|
33
33
|
- Execute - run shell commands and process operations
|
|
34
|
-
|
|
34
|
+
```
|
|
35
35
|
|
|
36
36
|
Skills are custom workflows you can define to teach `pls` about your specific
|
|
37
37
|
projects and commands. Once defined, you can use them naturally:
|
|
@@ -87,16 +87,47 @@ commands your environment requires.
|
|
|
87
87
|
|
|
88
88
|
## Configuration
|
|
89
89
|
|
|
90
|
-
Your configuration is stored in `~/.plsrc` as a YAML file
|
|
90
|
+
Your configuration is stored in `~/.plsrc` as a YAML file:
|
|
91
|
+
|
|
92
|
+
```yaml
|
|
93
|
+
# Mandatory
|
|
94
|
+
anthropic:
|
|
95
|
+
key: sk-ant-...
|
|
96
|
+
model: claude-...
|
|
97
|
+
|
|
98
|
+
# Optional
|
|
99
|
+
settings:
|
|
100
|
+
memory: 1024 # Child process memory limit (MB)
|
|
101
|
+
debug: none # none | info | verbose
|
|
102
|
+
|
|
103
|
+
# Custom
|
|
104
|
+
project:
|
|
105
|
+
path: ~/projects/app
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Skills can define their own configuration properties via a `Config` section. When
|
|
109
|
+
a skill requires config values that don't exist, `pls` prompts you to provide
|
|
110
|
+
them before execution. See [Skills](#skills) for details.
|
|
111
|
+
|
|
112
|
+
## Reference
|
|
113
|
+
|
|
114
|
+
### Debug Mode
|
|
115
|
+
Press `Shift+Tab` during execution to cycle through debug levels
|
|
116
|
+
(none → info → verbose).
|
|
117
|
+
Logs are saved to `~/.pls/logs/` when debug is `info` or `verbose`.
|
|
91
118
|
|
|
92
|
-
|
|
93
|
-
|
|
119
|
+
### Data Locations
|
|
120
|
+
```
|
|
121
|
+
~/.plsrc # Configuration
|
|
122
|
+
~/.pls/skills/ # Custom skills
|
|
123
|
+
~/.pls/logs/ # Debug logs
|
|
124
|
+
```
|
|
94
125
|
|
|
95
126
|
## Skills
|
|
96
127
|
|
|
97
128
|
Skills let you teach `pls` about your project-specific workflows. Create
|
|
98
|
-
markdown files in `~/.pls/skills/` to define custom operations that
|
|
99
|
-
understand and execute.
|
|
129
|
+
markdown files in `~/.pls/skills/` to define custom operations that
|
|
130
|
+
`pls` can understand and execute.
|
|
100
131
|
|
|
101
132
|
For complete documentation, see [docs/SKILLS.md](./docs/SKILLS.md).
|
|
102
133
|
|
|
@@ -139,9 +139,9 @@ export function Execute({ tasks: inputTasks, status, service, upcoming, label, r
|
|
|
139
139
|
lifecycleHandlers.completeActive();
|
|
140
140
|
return;
|
|
141
141
|
}
|
|
142
|
-
// Create task data from commands
|
|
143
|
-
const tasks = result.commands.map((cmd
|
|
144
|
-
label:
|
|
142
|
+
// Create task data from commands - use descriptions from execute response
|
|
143
|
+
const tasks = result.commands.map((cmd) => ({
|
|
144
|
+
label: cmd.description,
|
|
145
145
|
command: cmd,
|
|
146
146
|
status: ExecutionStatus.Pending,
|
|
147
147
|
elapsed: 0,
|
|
@@ -4,15 +4,21 @@ 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 SHORT_OUTPUT_THRESHOLD = 4;
|
|
8
7
|
const MINIMAL_INFO_THRESHOLD = 2;
|
|
9
8
|
/**
|
|
10
|
-
* Get the last N lines from text, filtering out empty/whitespace-only lines
|
|
9
|
+
* Get the last N lines from text, filtering out empty/whitespace-only lines.
|
|
10
|
+
* Handles carriage returns used in progress output by keeping only the
|
|
11
|
+
* content after the last \r in each line.
|
|
11
12
|
*/
|
|
12
13
|
export function getLastLines(text, maxLines = MAX_LINES) {
|
|
13
14
|
const lines = text
|
|
14
15
|
.trim()
|
|
15
16
|
.split(/\r?\n/)
|
|
17
|
+
.map((line) => {
|
|
18
|
+
// Handle carriage returns: keep only content after the last \r
|
|
19
|
+
const lastCR = line.lastIndexOf('\r');
|
|
20
|
+
return lastCR >= 0 ? line.slice(lastCR + 1) : line;
|
|
21
|
+
})
|
|
16
22
|
.filter((line) => line.trim().length > 0);
|
|
17
23
|
return lines.length <= maxLines ? lines : lines.slice(-maxLines);
|
|
18
24
|
}
|
|
@@ -29,9 +35,6 @@ export function computeDisplayConfig(stdout, stderr, status, isFinished) {
|
|
|
29
35
|
const stderrLines = hasStderr ? getLastLines(stderr) : [];
|
|
30
36
|
// Show stdout if no stderr, or if stderr is minimal (provides context)
|
|
31
37
|
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
38
|
// Darker colors for finished tasks
|
|
36
39
|
const baseColor = isFinished ? Palette.DarkGray : Palette.Gray;
|
|
37
40
|
const stderrColor = status === ExecutionStatus.Failed ? Palette.Yellow : baseColor;
|
|
@@ -39,7 +42,6 @@ export function computeDisplayConfig(stdout, stderr, status, isFinished) {
|
|
|
39
42
|
stdoutLines,
|
|
40
43
|
stderrLines,
|
|
41
44
|
showStdout,
|
|
42
|
-
wrapMode,
|
|
43
45
|
stdoutColor: baseColor,
|
|
44
46
|
stderrColor,
|
|
45
47
|
};
|
|
@@ -48,7 +50,7 @@ export function Output({ stdout, stderr, status, isFinished }) {
|
|
|
48
50
|
const config = computeDisplayConfig(stdout, stderr, status, isFinished ?? false);
|
|
49
51
|
if (!config)
|
|
50
52
|
return null;
|
|
51
|
-
const { stdoutLines, stderrLines, showStdout,
|
|
53
|
+
const { stdoutLines, stderrLines, showStdout, stdoutColor, stderrColor } = config;
|
|
52
54
|
return (_jsxs(Box, { marginTop: 1, marginLeft: 5, flexDirection: "column", width: MAX_WIDTH, children: [showStdout &&
|
|
53
|
-
stdoutLines.map((line, index) => (_jsx(Text, { color: stdoutColor, wrap:
|
|
55
|
+
stdoutLines.map((line, index) => (_jsx(Text, { color: stdoutColor, wrap: "wrap", children: line }, `out-${index}`))), stderrLines.map((line, index) => (_jsx(Text, { color: stderrColor, wrap: "wrap", children: line }, `err-${index}`)))] }));
|
|
54
56
|
}
|
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
|
@@ -70,7 +70,7 @@ export async function executeTask(command, index, callbacks) {
|
|
|
70
70
|
return { status: ExecutionStatus.Success, elapsed, output };
|
|
71
71
|
}
|
|
72
72
|
else {
|
|
73
|
-
const errorMsg = result.
|
|
73
|
+
const errorMsg = result.error || result.errors || 'Command failed';
|
|
74
74
|
error = errorMsg;
|
|
75
75
|
const output = createOutput();
|
|
76
76
|
callbacks.onUpdate(output);
|
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
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, unlinkSync, writeFileSync, } from 'fs';
|
|
1
|
+
import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, unlinkSync, writeFileSync, } from 'fs';
|
|
2
2
|
import { dirname } from 'path';
|
|
3
3
|
/**
|
|
4
4
|
* Real filesystem implementation using Node's fs module
|
|
@@ -13,6 +13,9 @@ export class RealFileSystem {
|
|
|
13
13
|
writeFile(path, data) {
|
|
14
14
|
writeFileSync(path, data, 'utf-8');
|
|
15
15
|
}
|
|
16
|
+
appendFile(path, data) {
|
|
17
|
+
appendFileSync(path, data, 'utf-8');
|
|
18
|
+
}
|
|
16
19
|
readDirectory(path) {
|
|
17
20
|
return readdirSync(path);
|
|
18
21
|
}
|
|
@@ -51,6 +54,15 @@ export class MemoryFileSystem {
|
|
|
51
54
|
}
|
|
52
55
|
this.files.set(path, data);
|
|
53
56
|
}
|
|
57
|
+
appendFile(path, data) {
|
|
58
|
+
// Auto-create parent directories (consistent with writeFile)
|
|
59
|
+
const dir = dirname(path);
|
|
60
|
+
if (dir !== '.' && dir !== path) {
|
|
61
|
+
this.createDirectory(dir, { recursive: true });
|
|
62
|
+
}
|
|
63
|
+
const existing = this.files.get(path) ?? '';
|
|
64
|
+
this.files.set(path, existing + data);
|
|
65
|
+
}
|
|
54
66
|
readDirectory(path) {
|
|
55
67
|
if (!this.directories.has(path)) {
|
|
56
68
|
throw new Error(`ENOENT: no such file or directory, scandir '${path}'`);
|
package/dist/services/logger.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import { homedir, platform } from 'os';
|
|
2
|
+
import { dirname, join } from 'path';
|
|
1
3
|
import { DebugLevel } from '../configuration/types.js';
|
|
2
4
|
import { loadDebugSetting } from '../configuration/io.js';
|
|
3
5
|
import { Palette } from './colors.js';
|
|
4
6
|
import { createDebug } from './components.js';
|
|
7
|
+
import { defaultFileSystem } from './filesystem.js';
|
|
5
8
|
/**
|
|
6
9
|
* Enum controlling what content is shown in debug prompt output
|
|
7
10
|
* - LLM: Exact prompt as sent to LLM (no display formatting)
|
|
@@ -23,6 +26,112 @@ let currentDebugLevel = DebugLevel.None;
|
|
|
23
26
|
* Accumulated warnings to be displayed in the timeline
|
|
24
27
|
*/
|
|
25
28
|
const warnings = [];
|
|
29
|
+
/**
|
|
30
|
+
* Content width for debug display (matches Debug component)
|
|
31
|
+
* Box width 80 - 2 borders - 4 padding = 74 chars
|
|
32
|
+
*/
|
|
33
|
+
const DISPLAY_CONTENT_WIDTH = 74;
|
|
34
|
+
/**
|
|
35
|
+
* File logging configuration
|
|
36
|
+
*/
|
|
37
|
+
const LOGS_DIR = join(homedir(), '.pls', 'logs');
|
|
38
|
+
/**
|
|
39
|
+
* Whether running on Windows (affects filename separators)
|
|
40
|
+
*/
|
|
41
|
+
const IS_WINDOWS = platform() === 'win32';
|
|
42
|
+
/**
|
|
43
|
+
* Maximum number of letter suffixes (a-z) for unique filenames
|
|
44
|
+
*/
|
|
45
|
+
const MAX_LETTER_SUFFIXES = 26;
|
|
46
|
+
/**
|
|
47
|
+
* Pad a number with leading zeros to the specified width
|
|
48
|
+
*/
|
|
49
|
+
const pad = (n, width = 2) => String(n).padStart(width, '0');
|
|
50
|
+
/**
|
|
51
|
+
* Current session's log file path (null until first log entry)
|
|
52
|
+
*/
|
|
53
|
+
let currentLogFile = null;
|
|
54
|
+
/**
|
|
55
|
+
* Filesystem instance for file operations (injectable for testing)
|
|
56
|
+
*/
|
|
57
|
+
let fileSystem = defaultFileSystem;
|
|
58
|
+
/**
|
|
59
|
+
* Set the filesystem instance (used for testing)
|
|
60
|
+
*/
|
|
61
|
+
export function setFileSystem(fs) {
|
|
62
|
+
fileSystem = fs;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Reset the session log file (used for testing)
|
|
66
|
+
*/
|
|
67
|
+
export function resetSessionLog() {
|
|
68
|
+
currentLogFile = null;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Generate a timestamped log file path using local time
|
|
72
|
+
* Format: ~/.pls/logs/YYYY-MM-DD/HH:MM:SS.log.md (HH-MM-SS on Windows)
|
|
73
|
+
*/
|
|
74
|
+
function getLogFilePath() {
|
|
75
|
+
const now = new Date();
|
|
76
|
+
const date = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`;
|
|
77
|
+
const separator = IS_WINDOWS ? '-' : ':';
|
|
78
|
+
const time = `${pad(now.getHours())}${separator}${pad(now.getMinutes())}${separator}${pad(now.getSeconds())}`;
|
|
79
|
+
return join(LOGS_DIR, date, `${time}.log.md`);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Generate a unique log file path by adding suffix if file exists
|
|
83
|
+
*/
|
|
84
|
+
function getUniqueLogFilePath(basePath) {
|
|
85
|
+
if (!fileSystem.exists(basePath)) {
|
|
86
|
+
return basePath;
|
|
87
|
+
}
|
|
88
|
+
const dir = dirname(basePath);
|
|
89
|
+
const ext = '.log.md';
|
|
90
|
+
const name = basePath.slice(dir.length + 1, -ext.length);
|
|
91
|
+
for (let i = 0; i < MAX_LETTER_SUFFIXES; i++) {
|
|
92
|
+
const suffix = String.fromCharCode(97 + i); // a-z
|
|
93
|
+
const candidate = join(dir, `${name}-${suffix}${ext}`);
|
|
94
|
+
if (!fileSystem.exists(candidate)) {
|
|
95
|
+
return candidate;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// Fallback: use milliseconds for uniqueness (avoids overwriting)
|
|
99
|
+
return join(dir, `${name}-${pad(new Date().getMilliseconds(), 3)}${ext}`);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Initialize the session log file if not already created
|
|
103
|
+
*/
|
|
104
|
+
function initializeSessionLog() {
|
|
105
|
+
if (currentLogFile)
|
|
106
|
+
return true;
|
|
107
|
+
try {
|
|
108
|
+
const basePath = getLogFilePath();
|
|
109
|
+
const logDir = dirname(basePath);
|
|
110
|
+
if (!fileSystem.exists(logDir)) {
|
|
111
|
+
fileSystem.createDirectory(logDir, { recursive: true });
|
|
112
|
+
}
|
|
113
|
+
const logPath = getUniqueLogFilePath(basePath);
|
|
114
|
+
fileSystem.writeFile(logPath, '');
|
|
115
|
+
currentLogFile = logPath;
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Append content to the current session's log file
|
|
124
|
+
*/
|
|
125
|
+
function appendToLog(content) {
|
|
126
|
+
if (!initializeSessionLog() || !currentLogFile)
|
|
127
|
+
return;
|
|
128
|
+
try {
|
|
129
|
+
fileSystem.appendFile(currentLogFile, content);
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
// Silently fail - logging should not crash the app
|
|
133
|
+
}
|
|
134
|
+
}
|
|
26
135
|
/**
|
|
27
136
|
* Initialize the logger with the current debug level from config
|
|
28
137
|
*/
|
|
@@ -61,11 +170,6 @@ export function getWarnings() {
|
|
|
61
170
|
warnings.length = 0;
|
|
62
171
|
return result;
|
|
63
172
|
}
|
|
64
|
-
/**
|
|
65
|
-
* Content width for debug display (matches Debug component)
|
|
66
|
-
* Box width 80 - 2 borders - 4 padding = 74 chars
|
|
67
|
-
*/
|
|
68
|
-
const DISPLAY_CONTENT_WIDTH = 74;
|
|
69
173
|
/**
|
|
70
174
|
* Join sections with separators matching display width
|
|
71
175
|
*/
|
|
@@ -142,28 +246,28 @@ function formatSkillsForDisplay(formattedSkills) {
|
|
|
142
246
|
* - Summary: Returns header + skill summaries (Name, Steps, Execution)
|
|
143
247
|
*/
|
|
144
248
|
export function formatPromptContent(toolName, command, baseInstructions, formattedSkills, mode, definitions) {
|
|
145
|
-
const header = ['', `Tool: ${toolName}`, `Command: ${command}`];
|
|
146
249
|
switch (mode) {
|
|
147
|
-
case PromptDisplay.LLM:
|
|
250
|
+
case PromptDisplay.LLM: {
|
|
251
|
+
const header = ['', `**Tool:** ${toolName}`];
|
|
148
252
|
return [...header, '', baseInstructions + formattedSkills].join('\n');
|
|
253
|
+
}
|
|
149
254
|
case PromptDisplay.Skills: {
|
|
150
|
-
|
|
151
|
-
const headerString = header.join('\n');
|
|
255
|
+
const header = `\nTool: ${toolName}\nCommand: ${command}`;
|
|
152
256
|
const skillsDisplay = formatSkillsForDisplay(formattedSkills);
|
|
153
|
-
return joinWithSeparators([
|
|
257
|
+
return joinWithSeparators([header, skillsDisplay]);
|
|
154
258
|
}
|
|
155
259
|
case PromptDisplay.Summary: {
|
|
156
|
-
const
|
|
260
|
+
const header = `\nTool: ${toolName}\nCommand: ${command}`;
|
|
157
261
|
const summary = definitions
|
|
158
262
|
? formatSkillsSummary(definitions)
|
|
159
263
|
: '(no skills)';
|
|
160
|
-
return joinWithSeparators([
|
|
264
|
+
return joinWithSeparators([header, summary]);
|
|
161
265
|
}
|
|
162
266
|
}
|
|
163
267
|
}
|
|
164
268
|
/**
|
|
165
269
|
* Create debug component for system prompts sent to the LLM
|
|
166
|
-
*
|
|
270
|
+
* Creates UI component at Verbose level, writes to file at Info or Verbose
|
|
167
271
|
*
|
|
168
272
|
* @param toolName - Name of the tool being invoked
|
|
169
273
|
* @param command - User command being processed
|
|
@@ -172,6 +276,13 @@ export function formatPromptContent(toolName, command, baseInstructions, formatt
|
|
|
172
276
|
* @param definitions - Parsed skill definitions for summary display
|
|
173
277
|
*/
|
|
174
278
|
export function logPrompt(toolName, command, baseInstructions, formattedSkills, definitions = []) {
|
|
279
|
+
// Write to file at Info or Verbose level (full LLM format)
|
|
280
|
+
if (currentDebugLevel !== DebugLevel.None) {
|
|
281
|
+
const userPrompt = `# User Command\n\n\`\`\`\n${command}\n\`\`\`\n\n`;
|
|
282
|
+
const fileContent = formatPromptContent(toolName, command, baseInstructions, formattedSkills, PromptDisplay.LLM);
|
|
283
|
+
appendToLog(userPrompt + '# System Prompt\n' + fileContent + '\n\n');
|
|
284
|
+
}
|
|
285
|
+
// Create UI component only at Verbose level
|
|
175
286
|
if (currentDebugLevel !== DebugLevel.Verbose) {
|
|
176
287
|
return null;
|
|
177
288
|
}
|
|
@@ -185,18 +296,27 @@ export function logPrompt(toolName, command, baseInstructions, formattedSkills,
|
|
|
185
296
|
}
|
|
186
297
|
/**
|
|
187
298
|
* Create debug component for LLM responses received
|
|
188
|
-
*
|
|
299
|
+
* Creates UI component at Verbose level, writes to file at Info or Verbose
|
|
189
300
|
*/
|
|
190
301
|
export function logResponse(toolName, response, durationMs) {
|
|
302
|
+
const jsonContent = JSON.stringify(response, null, 2);
|
|
303
|
+
// Write to file at Info or Verbose level (markdown format)
|
|
304
|
+
if (currentDebugLevel !== DebugLevel.None) {
|
|
305
|
+
const fileContent = [
|
|
306
|
+
'',
|
|
307
|
+
`**Tool:** ${toolName}`,
|
|
308
|
+
'',
|
|
309
|
+
'```json',
|
|
310
|
+
jsonContent,
|
|
311
|
+
'```',
|
|
312
|
+
].join('\n');
|
|
313
|
+
appendToLog('# LLM Response\n' + fileContent + '\n\n');
|
|
314
|
+
}
|
|
315
|
+
// Create UI component only at Verbose level
|
|
191
316
|
if (currentDebugLevel !== DebugLevel.Verbose) {
|
|
192
317
|
return null;
|
|
193
318
|
}
|
|
194
|
-
const content = [
|
|
195
|
-
'',
|
|
196
|
-
`Tool: ${toolName}`,
|
|
197
|
-
'',
|
|
198
|
-
JSON.stringify(response, null, 2),
|
|
199
|
-
].join('\n');
|
|
319
|
+
const content = ['', `Tool: ${toolName}`, '', jsonContent].join('\n');
|
|
200
320
|
const title = `LLM RESPONSE (${String(durationMs)} ms)`;
|
|
201
321
|
return createDebug({ title, content, color: Palette.LightGray });
|
|
202
322
|
}
|
|
@@ -148,11 +148,14 @@ export function formatErrorMessage(error) {
|
|
|
148
148
|
return rawMessage;
|
|
149
149
|
}
|
|
150
150
|
/**
|
|
151
|
-
* Returns an execution error message
|
|
152
|
-
*
|
|
153
|
-
*
|
|
151
|
+
* Returns an execution error message.
|
|
152
|
+
* If a specific error is provided, returns it directly.
|
|
153
|
+
* Otherwise, returns a generic failure message with varied phrasing.
|
|
154
154
|
*/
|
|
155
|
-
export function getExecutionErrorMessage(
|
|
155
|
+
export function getExecutionErrorMessage(error) {
|
|
156
|
+
if (error) {
|
|
157
|
+
return error;
|
|
158
|
+
}
|
|
156
159
|
const messages = [
|
|
157
160
|
'The execution failed.',
|
|
158
161
|
'Execution has failed.',
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import { execFile, execSync } from 'child_process';
|
|
2
|
+
import { existsSync, readFileSync } from 'fs';
|
|
3
|
+
import { platform } from 'os';
|
|
4
|
+
import { promisify } from 'util';
|
|
5
|
+
// Memory monitoring constants
|
|
6
|
+
const MEMORY_CHECK_INTERVAL = 1000;
|
|
7
|
+
const DEFAULT_PAGE_SIZE = 4096;
|
|
8
|
+
export const SIGKILL_GRACE_PERIOD = 3000;
|
|
9
|
+
/**
|
|
10
|
+
* Get system page size in bytes.
|
|
11
|
+
* Computed lazily on first call, then cached at module level.
|
|
12
|
+
*/
|
|
13
|
+
let cachedPageSize;
|
|
14
|
+
function getPageSize() {
|
|
15
|
+
if (cachedPageSize !== undefined) {
|
|
16
|
+
return cachedPageSize;
|
|
17
|
+
}
|
|
18
|
+
if (platform() === 'linux') {
|
|
19
|
+
try {
|
|
20
|
+
const output = execSync('getconf PAGESIZE', {
|
|
21
|
+
encoding: 'utf-8',
|
|
22
|
+
timeout: 1000,
|
|
23
|
+
}).trim();
|
|
24
|
+
const size = parseInt(output, 10);
|
|
25
|
+
if (!isNaN(size) && size > 0) {
|
|
26
|
+
cachedPageSize = size;
|
|
27
|
+
return cachedPageSize;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
// Fall through to default
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
cachedPageSize = DEFAULT_PAGE_SIZE;
|
|
35
|
+
return cachedPageSize;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Gracefully terminate a child process with SIGTERM, escalating to SIGKILL
|
|
39
|
+
* after a grace period if the process doesn't terminate.
|
|
40
|
+
* Returns the kill timeout ID for cleanup.
|
|
41
|
+
*/
|
|
42
|
+
export function killGracefully(child, gracePeriod = SIGKILL_GRACE_PERIOD) {
|
|
43
|
+
child.kill('SIGTERM');
|
|
44
|
+
return setTimeout(() => {
|
|
45
|
+
try {
|
|
46
|
+
child.kill('SIGKILL');
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// Process already terminated
|
|
50
|
+
}
|
|
51
|
+
}, gracePeriod);
|
|
52
|
+
}
|
|
53
|
+
const execFileAsync = promisify(execFile);
|
|
54
|
+
/**
|
|
55
|
+
* Run a command asynchronously and return stdout.
|
|
56
|
+
* Returns undefined on error or timeout.
|
|
57
|
+
*/
|
|
58
|
+
async function runCommandAsync(command, args, timeout = 1000) {
|
|
59
|
+
try {
|
|
60
|
+
const { stdout } = await execFileAsync(command, args, { timeout });
|
|
61
|
+
return stdout.trim();
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Get memory usage of a process in bytes.
|
|
69
|
+
* Returns undefined if the process doesn't exist or memory can't be read.
|
|
70
|
+
* On macOS, uses async subprocess; on Linux, reads from /proc (fast).
|
|
71
|
+
*/
|
|
72
|
+
async function getProcessMemoryBytes(pid) {
|
|
73
|
+
try {
|
|
74
|
+
if (platform() === 'linux') {
|
|
75
|
+
// Linux: Read from /proc/[pid]/statm (memory in pages)
|
|
76
|
+
// This is fast and effectively non-blocking for procfs
|
|
77
|
+
const statmPath = `/proc/${pid}/statm`;
|
|
78
|
+
if (!existsSync(statmPath))
|
|
79
|
+
return undefined;
|
|
80
|
+
const statm = readFileSync(statmPath, 'utf-8');
|
|
81
|
+
const rssPages = parseInt(statm.split(' ')[1], 10);
|
|
82
|
+
return rssPages * getPageSize();
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
// macOS/BSD: Use ps command asynchronously
|
|
86
|
+
const output = await runCommandAsync('ps', [
|
|
87
|
+
'-o',
|
|
88
|
+
'rss=',
|
|
89
|
+
'-p',
|
|
90
|
+
`${pid}`,
|
|
91
|
+
]);
|
|
92
|
+
if (!output)
|
|
93
|
+
return undefined;
|
|
94
|
+
const rssKB = parseInt(output, 10);
|
|
95
|
+
if (isNaN(rssKB))
|
|
96
|
+
return undefined;
|
|
97
|
+
return rssKB * 1024;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Get child PIDs of a single process.
|
|
106
|
+
*/
|
|
107
|
+
async function getChildPids(pid) {
|
|
108
|
+
try {
|
|
109
|
+
let output;
|
|
110
|
+
if (platform() === 'linux') {
|
|
111
|
+
output = await runCommandAsync('ps', ['-o', 'pid=', '--ppid', `${pid}`]);
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
output = await runCommandAsync('pgrep', ['-P', `${pid}`]);
|
|
115
|
+
}
|
|
116
|
+
if (!output)
|
|
117
|
+
return [];
|
|
118
|
+
return output
|
|
119
|
+
.split('\n')
|
|
120
|
+
.map((p) => parseInt(p.trim(), 10))
|
|
121
|
+
.filter((p) => !isNaN(p) && p > 0);
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
return [];
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Get all descendant PIDs of a process.
|
|
129
|
+
* Uses iterative BFS with parallel child lookups at each level.
|
|
130
|
+
*/
|
|
131
|
+
async function getAllDescendantPids(pid) {
|
|
132
|
+
const visited = new Set();
|
|
133
|
+
const pids = [];
|
|
134
|
+
let currentLevel = [pid];
|
|
135
|
+
while (currentLevel.length > 0) {
|
|
136
|
+
// Add unvisited PIDs from current level
|
|
137
|
+
const unvisited = currentLevel.filter((p) => !visited.has(p));
|
|
138
|
+
for (const p of unvisited)
|
|
139
|
+
visited.add(p);
|
|
140
|
+
pids.push(...unvisited);
|
|
141
|
+
// Fetch children of all current level PIDs in parallel
|
|
142
|
+
const childArrays = await Promise.all(unvisited.map(getChildPids));
|
|
143
|
+
currentLevel = childArrays.flat();
|
|
144
|
+
}
|
|
145
|
+
return pids;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Get total memory usage of a process tree.
|
|
149
|
+
* Sums memory of the process and all its descendants.
|
|
150
|
+
*/
|
|
151
|
+
async function getProcessTreeMemoryBytes(pid) {
|
|
152
|
+
try {
|
|
153
|
+
const allPids = await getAllDescendantPids(pid);
|
|
154
|
+
let totalBytes = 0;
|
|
155
|
+
for (const p of allPids) {
|
|
156
|
+
const mem = await getProcessMemoryBytes(p);
|
|
157
|
+
if (mem)
|
|
158
|
+
totalBytes += mem;
|
|
159
|
+
}
|
|
160
|
+
return totalBytes > 0 ? totalBytes : undefined;
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
return getProcessMemoryBytes(pid);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/** Monitor lifecycle state */
|
|
167
|
+
var MonitorState;
|
|
168
|
+
(function (MonitorState) {
|
|
169
|
+
MonitorState["Idle"] = "idle";
|
|
170
|
+
MonitorState["Running"] = "running";
|
|
171
|
+
MonitorState["Stopped"] = "stopped";
|
|
172
|
+
MonitorState["Killed"] = "killed";
|
|
173
|
+
})(MonitorState || (MonitorState = {}));
|
|
174
|
+
/**
|
|
175
|
+
* Monitors a child process memory and kills it when the limit is exceeded.
|
|
176
|
+
* Uses async self-scheduling to avoid blocking the event loop.
|
|
177
|
+
* By default monitors only the root process; set includeDescendants for tree.
|
|
178
|
+
*/
|
|
179
|
+
export class MemoryMonitor {
|
|
180
|
+
nextCheckId;
|
|
181
|
+
killTimeoutId;
|
|
182
|
+
child;
|
|
183
|
+
memoryLimit;
|
|
184
|
+
limitBytes;
|
|
185
|
+
onExceeded;
|
|
186
|
+
state = MonitorState.Idle;
|
|
187
|
+
getMemoryFn;
|
|
188
|
+
constructor(child, memoryLimitMB, onExceeded, getMemoryFn) {
|
|
189
|
+
this.child = child;
|
|
190
|
+
this.memoryLimit = memoryLimitMB;
|
|
191
|
+
this.limitBytes = memoryLimitMB * 1024 * 1024;
|
|
192
|
+
this.onExceeded = onExceeded;
|
|
193
|
+
// Always monitor full process tree by default
|
|
194
|
+
this.getMemoryFn = getMemoryFn ?? getProcessTreeMemoryBytes;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Start monitoring the child process memory.
|
|
198
|
+
* Uses async self-scheduling loop instead of setInterval for non-blocking.
|
|
199
|
+
*/
|
|
200
|
+
start() {
|
|
201
|
+
if (!this.child.pid)
|
|
202
|
+
return;
|
|
203
|
+
this.state = MonitorState.Running;
|
|
204
|
+
this.scheduleNextCheck();
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Schedule the next memory check after the configured interval.
|
|
208
|
+
*/
|
|
209
|
+
scheduleNextCheck() {
|
|
210
|
+
if (this.state !== MonitorState.Running)
|
|
211
|
+
return;
|
|
212
|
+
this.nextCheckId = setTimeout(() => {
|
|
213
|
+
void this.checkMemory();
|
|
214
|
+
}, MEMORY_CHECK_INTERVAL);
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Perform async memory check and schedule next one.
|
|
218
|
+
*/
|
|
219
|
+
async checkMemory() {
|
|
220
|
+
if (this.state !== MonitorState.Running || !this.child.pid)
|
|
221
|
+
return;
|
|
222
|
+
let memoryBytes;
|
|
223
|
+
try {
|
|
224
|
+
memoryBytes = await this.getMemoryFn(this.child.pid);
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
// Memory reading failed, schedule next check and continue
|
|
228
|
+
this.scheduleNextCheck();
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
// Re-check after async operation - state may have changed
|
|
232
|
+
if (this.state !== MonitorState.Running)
|
|
233
|
+
return; // eslint-disable-line @typescript-eslint/no-unnecessary-condition
|
|
234
|
+
if (memoryBytes !== undefined && memoryBytes >= this.limitBytes) {
|
|
235
|
+
this.terminateProcess(memoryBytes);
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
this.scheduleNextCheck();
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Stop monitoring and cancel any pending timeouts.
|
|
243
|
+
*/
|
|
244
|
+
stop() {
|
|
245
|
+
if (this.state !== MonitorState.Killed) {
|
|
246
|
+
this.state = MonitorState.Stopped;
|
|
247
|
+
}
|
|
248
|
+
if (this.nextCheckId) {
|
|
249
|
+
clearTimeout(this.nextCheckId);
|
|
250
|
+
this.nextCheckId = undefined;
|
|
251
|
+
}
|
|
252
|
+
if (this.killTimeoutId) {
|
|
253
|
+
clearTimeout(this.killTimeoutId);
|
|
254
|
+
this.killTimeoutId = undefined;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Terminate the child process due to memory limit exceeded.
|
|
259
|
+
*/
|
|
260
|
+
terminateProcess(currentMemoryBytes) {
|
|
261
|
+
if (this.state === MonitorState.Killed)
|
|
262
|
+
return;
|
|
263
|
+
this.state = MonitorState.Killed;
|
|
264
|
+
// Clear only the next check timeout, keep killTimeoutId for cleanup
|
|
265
|
+
if (this.nextCheckId) {
|
|
266
|
+
clearTimeout(this.nextCheckId);
|
|
267
|
+
this.nextCheckId = undefined;
|
|
268
|
+
}
|
|
269
|
+
// Kill first, then notify - ensures termination even if callback throws
|
|
270
|
+
this.killTimeoutId = killGracefully(this.child);
|
|
271
|
+
const info = {
|
|
272
|
+
used: Math.ceil(currentMemoryBytes / 1024 / 1024),
|
|
273
|
+
limit: this.memoryLimit,
|
|
274
|
+
};
|
|
275
|
+
try {
|
|
276
|
+
this.onExceeded?.(info);
|
|
277
|
+
}
|
|
278
|
+
catch {
|
|
279
|
+
// Ignore callback errors - kill already initiated
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Check if the process was killed due to memory limit.
|
|
284
|
+
*/
|
|
285
|
+
wasKilledByMemoryLimit() {
|
|
286
|
+
return this.state === MonitorState.Killed;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { performance } from 'perf_hooks';
|
|
2
|
+
/**
|
|
3
|
+
* Prevent perf_hooks memory leak warning during long-running operations.
|
|
4
|
+
* React and Ink create performance measurements internally that accumulate
|
|
5
|
+
* in the global buffer. This clears them immediately and periodically.
|
|
6
|
+
*/
|
|
7
|
+
export function preventPerformanceBufferOverflow(intervalMs = 60000) {
|
|
8
|
+
performance.clearMarks();
|
|
9
|
+
performance.clearMeasures();
|
|
10
|
+
setInterval(() => {
|
|
11
|
+
performance.clearMarks();
|
|
12
|
+
performance.clearMeasures();
|
|
13
|
+
}, intervalMs).unref();
|
|
14
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ComponentStatus, } from '../types/components.js';
|
|
2
|
-
import {
|
|
2
|
+
import { formatTaskAsYaml } from '../execution/processing.js';
|
|
3
3
|
import { createCommand, createRefinement } from './components.js';
|
|
4
4
|
import { formatErrorMessage, getRefiningMessage } from './messages.js';
|
|
5
5
|
import { routeTasksWithConfirm } from './router.js';
|
|
@@ -22,18 +22,15 @@ export async function handleRefinement(selectedTasks, service, originalCommand,
|
|
|
22
22
|
});
|
|
23
23
|
workflowHandlers.addToQueue(refinementDef);
|
|
24
24
|
try {
|
|
25
|
-
// Build refined command
|
|
25
|
+
// Build refined command with action line followed by YAML metadata
|
|
26
26
|
const refinedCommand = selectedTasks
|
|
27
27
|
.map((task) => {
|
|
28
|
+
// Replace commas with dashes for cleaner LLM prompt formatting
|
|
28
29
|
const action = task.action.replace(/,/g, ' -');
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
if (type === TaskType.Execute || type === TaskType.Group) {
|
|
32
|
-
return `${action} (shell execution)`;
|
|
33
|
-
}
|
|
34
|
-
return `${action} (type: ${type})`;
|
|
30
|
+
const metadata = { ...task.params, type: task.type };
|
|
31
|
+
return formatTaskAsYaml(action, metadata);
|
|
35
32
|
})
|
|
36
|
-
.join('
|
|
33
|
+
.join('\n\n');
|
|
37
34
|
// Call LLM to refine plan with selected tasks
|
|
38
35
|
const refinedResult = await service.processWithTool(refinedCommand, 'schedule');
|
|
39
36
|
// Complete the Refinement component with success state
|
package/dist/services/shell.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { spawn } from 'child_process';
|
|
2
|
+
import { killGracefully, MemoryMonitor, } from './monitor.js';
|
|
2
3
|
export var ExecutionStatus;
|
|
3
4
|
(function (ExecutionStatus) {
|
|
4
5
|
ExecutionStatus["Pending"] = "pending";
|
|
@@ -60,12 +61,12 @@ export class DummyExecutor {
|
|
|
60
61
|
}
|
|
61
62
|
}
|
|
62
63
|
// Marker for extracting pwd from command output
|
|
63
|
-
const PWD_MARKER = '__PWD_MARKER_7x9k2m__';
|
|
64
|
-
const MAX_OUTPUT_LINES = 128;
|
|
64
|
+
export const PWD_MARKER = '__PWD_MARKER_7x9k2m__';
|
|
65
|
+
export const MAX_OUTPUT_LINES = 128;
|
|
65
66
|
/**
|
|
66
67
|
* Limit output to last MAX_OUTPUT_LINES lines.
|
|
67
68
|
*/
|
|
68
|
-
function limitLines(output) {
|
|
69
|
+
export function limitLines(output) {
|
|
69
70
|
const lines = output.split('\n');
|
|
70
71
|
return lines.slice(-MAX_OUTPUT_LINES).join('\n');
|
|
71
72
|
}
|
|
@@ -73,7 +74,7 @@ function limitLines(output) {
|
|
|
73
74
|
* Parse stdout to extract workdir and clean output.
|
|
74
75
|
* Returns the cleaned output and the extracted workdir.
|
|
75
76
|
*/
|
|
76
|
-
function parseWorkdir(rawOutput) {
|
|
77
|
+
export function parseWorkdir(rawOutput) {
|
|
77
78
|
const markerIndex = rawOutput.lastIndexOf(PWD_MARKER);
|
|
78
79
|
if (markerIndex === -1) {
|
|
79
80
|
return { output: rawOutput };
|
|
@@ -88,7 +89,7 @@ function parseWorkdir(rawOutput) {
|
|
|
88
89
|
* Manages streaming output while filtering out the PWD marker.
|
|
89
90
|
* Buffers output to avoid emitting partial markers to the callback.
|
|
90
91
|
*/
|
|
91
|
-
class OutputStreamer {
|
|
92
|
+
export class OutputStreamer {
|
|
92
93
|
chunks = [];
|
|
93
94
|
emittedLength = 0;
|
|
94
95
|
callback;
|
|
@@ -183,18 +184,22 @@ export class RealExecutor {
|
|
|
183
184
|
return;
|
|
184
185
|
}
|
|
185
186
|
// Handle timeout if specified
|
|
186
|
-
const SIGKILL_GRACE_PERIOD = 3000;
|
|
187
187
|
let timeoutId;
|
|
188
188
|
let killTimeoutId;
|
|
189
189
|
if (cmd.timeout && cmd.timeout > 0) {
|
|
190
190
|
timeoutId = setTimeout(() => {
|
|
191
|
-
child
|
|
192
|
-
// Escalate to SIGKILL if process doesn't terminate
|
|
193
|
-
killTimeoutId = setTimeout(() => {
|
|
194
|
-
child.kill('SIGKILL');
|
|
195
|
-
}, SIGKILL_GRACE_PERIOD);
|
|
191
|
+
killTimeoutId = killGracefully(child);
|
|
196
192
|
}, cmd.timeout);
|
|
197
193
|
}
|
|
194
|
+
// Handle memory limit monitoring
|
|
195
|
+
let memoryMonitor;
|
|
196
|
+
let memoryInfo;
|
|
197
|
+
if (cmd.memoryLimit) {
|
|
198
|
+
memoryMonitor = new MemoryMonitor(child, cmd.memoryLimit, (info) => {
|
|
199
|
+
memoryInfo = info;
|
|
200
|
+
});
|
|
201
|
+
memoryMonitor.start();
|
|
202
|
+
}
|
|
198
203
|
// Use OutputStreamer for buffered stdout streaming
|
|
199
204
|
const stdoutStreamer = new OutputStreamer(this.outputCallback);
|
|
200
205
|
child.stdout.on('data', (data) => {
|
|
@@ -217,6 +222,7 @@ export class RealExecutor {
|
|
|
217
222
|
clearTimeout(timeoutId);
|
|
218
223
|
if (killTimeoutId)
|
|
219
224
|
clearTimeout(killTimeoutId);
|
|
225
|
+
memoryMonitor?.stop();
|
|
220
226
|
const commandResult = {
|
|
221
227
|
description: cmd.description,
|
|
222
228
|
command: cmd.command,
|
|
@@ -228,20 +234,32 @@ export class RealExecutor {
|
|
|
228
234
|
onProgress?.(ExecutionStatus.Failed);
|
|
229
235
|
resolve(commandResult);
|
|
230
236
|
});
|
|
231
|
-
child.on('
|
|
237
|
+
child.on('exit', (code) => {
|
|
232
238
|
if (timeoutId)
|
|
233
239
|
clearTimeout(timeoutId);
|
|
234
240
|
if (killTimeoutId)
|
|
235
241
|
clearTimeout(killTimeoutId);
|
|
236
|
-
|
|
242
|
+
memoryMonitor?.stop();
|
|
237
243
|
const { output, workdir } = parseWorkdir(stdoutStreamer.getAccumulated());
|
|
244
|
+
// Check if terminated due to memory limit
|
|
245
|
+
const killedByMemoryLimit = memoryMonitor?.wasKilledByMemoryLimit();
|
|
246
|
+
const success = code === 0 && !killedByMemoryLimit;
|
|
247
|
+
let errorMessage;
|
|
248
|
+
if (killedByMemoryLimit && memoryInfo) {
|
|
249
|
+
errorMessage =
|
|
250
|
+
`Process exceeded ${memoryInfo.limit} MB memory limit, ` +
|
|
251
|
+
`${memoryInfo.used} MB was used.`;
|
|
252
|
+
}
|
|
253
|
+
else if (!success) {
|
|
254
|
+
errorMessage = `Exit code: ${code}`;
|
|
255
|
+
}
|
|
238
256
|
const commandResult = {
|
|
239
257
|
description: cmd.description,
|
|
240
258
|
command: cmd.command,
|
|
241
259
|
output,
|
|
242
260
|
errors: limitLines(stderr.join('')),
|
|
243
261
|
result: success ? ExecutionResult.Success : ExecutionResult.Error,
|
|
244
|
-
error:
|
|
262
|
+
error: errorMessage,
|
|
245
263
|
workdir,
|
|
246
264
|
};
|
|
247
265
|
onProgress?.(success ? ExecutionStatus.Success : ExecutionStatus.Failed);
|
package/dist/skills/execute.md
CHANGED
|
@@ -100,14 +100,28 @@ position.
|
|
|
100
100
|
|
|
101
101
|
### How to Generate Commands from Skills
|
|
102
102
|
|
|
103
|
+
**CRITICAL - ONE TASK = ONE COMMAND**: Each input task maps to exactly
|
|
104
|
+
ONE command in your response. The task's action tells you WHICH specific
|
|
105
|
+
step from the skill to use. Do NOT expand an entire skill workflow for
|
|
106
|
+
a single task - only generate the command for that specific step.
|
|
107
|
+
|
|
103
108
|
1. **Identify skill tasks**: Check if tasks have params.skill
|
|
104
109
|
2. **Find the skill**: Look up the skill in "Available Skills" section
|
|
105
110
|
below (REQUIRED - must exist)
|
|
106
|
-
3. **Match
|
|
107
|
-
|
|
108
|
-
|
|
111
|
+
3. **Match task action to skill step**: The task action describes which
|
|
112
|
+
step from the skill's Steps section this task represents. Find the
|
|
113
|
+
matching step by semantic meaning (e.g., "Export results" matches
|
|
114
|
+
"Export the results to {FORMAT}", NOT all three steps of the skill)
|
|
115
|
+
4. **Use corresponding Execution line**: Once you identify which step
|
|
116
|
+
the task represents, use ONLY that step's corresponding Execution line
|
|
117
|
+
5. **Substitute parameters**: Replace {PARAM} placeholders with actual
|
|
109
118
|
values from task params
|
|
110
119
|
|
|
120
|
+
**IMPORTANT**: If the schedule contains separate tasks for different
|
|
121
|
+
steps of the same skill (e.g., one task for fetching data, another for
|
|
122
|
+
exporting), each task produces its own single command. Do NOT combine
|
|
123
|
+
them or add steps that weren't scheduled.
|
|
124
|
+
|
|
111
125
|
### Example Skill
|
|
112
126
|
|
|
113
127
|
```markdown
|
|
@@ -151,6 +165,28 @@ steps 1 and 3 (with step 2 skipped), use Execution lines 1 and 3
|
|
|
151
165
|
which Execution line to use - always match by original position, never
|
|
152
166
|
by sequential task index.
|
|
153
167
|
|
|
168
|
+
### Expanding Skill References in Execution Lines
|
|
169
|
+
|
|
170
|
+
Execution lines may contain **skill references** in the format
|
|
171
|
+
`[ Skill Name ]`. These are references to other skills that must be
|
|
172
|
+
expanded to actual commands before execution.
|
|
173
|
+
|
|
174
|
+
**Format**: `[ Skill Name ]` with spaces inside the brackets
|
|
175
|
+
|
|
176
|
+
**How to expand**:
|
|
177
|
+
1. When an Execution line contains `[ Skill Name ]`, look up that skill
|
|
178
|
+
in the "Available Skills" section
|
|
179
|
+
2. Get the referenced skill's Execution command
|
|
180
|
+
3. Replace the `[ Skill Name ]` reference with the actual command
|
|
181
|
+
|
|
182
|
+
**IMPORTANT**: Skill references are the ONLY exception to the verbatim
|
|
183
|
+
execution rule below. You MUST expand them - never output `[ ... ]`
|
|
184
|
+
syntax in the final command.
|
|
185
|
+
|
|
186
|
+
**Note**: Use the `skill:` field from task metadata to find the skill
|
|
187
|
+
definition. If that skill's Execution line contains `[ Other Skill ]`,
|
|
188
|
+
look up "Other Skill" and replace the reference with its command.
|
|
189
|
+
|
|
154
190
|
**CRITICAL - VERBATIM EXECUTION**: Run shell commands EXACTLY as written in
|
|
155
191
|
the ### Execution section. Do NOT:
|
|
156
192
|
- Modify the command string in any way
|
|
@@ -371,6 +407,41 @@ commands:
|
|
|
371
407
|
command: "df -h"
|
|
372
408
|
```
|
|
373
409
|
|
|
410
|
+
### Example 8: Partial skill execution (specific steps only)
|
|
411
|
+
|
|
412
|
+
When the schedule breaks a multi-step skill into separate tasks, each
|
|
413
|
+
task produces exactly ONE command for its specific step:
|
|
414
|
+
|
|
415
|
+
Skill "Prepare Report" has 3 steps:
|
|
416
|
+
- Steps: Fetch source data | Transform data | Export results
|
|
417
|
+
- Execution: curl {url} | python3 process.py | cat output.csv
|
|
418
|
+
|
|
419
|
+
Tasks (schedule requested only steps 1 and 3, skipping transform):
|
|
420
|
+
- { action: "Fetch source data", params: { skill: "Prepare Report" } }
|
|
421
|
+
- { action: "Export results", params: { skill: "Prepare Report" } }
|
|
422
|
+
|
|
423
|
+
Response (2 tasks = 2 commands, NOT 3):
|
|
424
|
+
```
|
|
425
|
+
message: "Prepare report:"
|
|
426
|
+
summary: "Report prepared"
|
|
427
|
+
commands:
|
|
428
|
+
- description: "Fetch source data"
|
|
429
|
+
command: "curl {url}"
|
|
430
|
+
- description: "Export results"
|
|
431
|
+
command: "cat output.csv"
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
**WRONG** response (adding unscheduled transform step):
|
|
435
|
+
```
|
|
436
|
+
commands:
|
|
437
|
+
- description: "Fetch source data"
|
|
438
|
+
command: "curl {url}"
|
|
439
|
+
- description: "Transform data" ← NOT IN SCHEDULE - DO NOT ADD
|
|
440
|
+
command: "python3 process.py"
|
|
441
|
+
- description: "Export results"
|
|
442
|
+
command: "cat output.csv"
|
|
443
|
+
```
|
|
444
|
+
|
|
374
445
|
## Handling Complex Operations
|
|
375
446
|
|
|
376
447
|
For complex multi-step operations:
|
|
@@ -420,6 +491,10 @@ Example:
|
|
|
420
491
|
- **CRITICAL: Assume what commands to run when skill is missing**
|
|
421
492
|
- **CRITICAL: Replace unknown placeholders with `<UNKNOWN>` - this breaks
|
|
422
493
|
shell syntax**
|
|
494
|
+
- **CRITICAL: Add steps that weren't in the scheduled tasks** - if the
|
|
495
|
+
schedule has 2 tasks, you MUST return exactly 2 commands
|
|
496
|
+
- **CRITICAL: Expand entire skill workflows** when only specific steps
|
|
497
|
+
were scheduled - match task actions to individual skill steps
|
|
423
498
|
|
|
424
499
|
**DO:**
|
|
425
500
|
- Match commands precisely to task descriptions
|
|
@@ -434,6 +509,10 @@ Example:
|
|
|
434
509
|
commands**
|
|
435
510
|
- Always use skill's Execution section when params.skill is present
|
|
436
511
|
- Replace all {PARAM} placeholders with values from task params
|
|
512
|
+
- **CRITICAL: Count input tasks and ensure output has same count** -
|
|
513
|
+
N tasks in = N commands out, no exceptions
|
|
514
|
+
- **CRITICAL: Match each task action to its specific skill step** -
|
|
515
|
+
use only that step's Execution line for the command
|
|
437
516
|
|
|
438
517
|
## Final Validation
|
|
439
518
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "prompt-language-shell",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.8",
|
|
4
4
|
"description": "Your personal command-line concierge. Ask politely, and it gets things done.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -17,9 +17,10 @@
|
|
|
17
17
|
"dev": "npm run build && tsc --watch",
|
|
18
18
|
"prepare": "husky",
|
|
19
19
|
"prepublishOnly": "npm run check",
|
|
20
|
-
"test": "vitest run --exclude 'tests/tools/schedule/*.test.tsx'",
|
|
21
|
-
"test:watch": "vitest --exclude 'tests/tools/schedule/*.test.tsx'",
|
|
20
|
+
"test": "vitest run --exclude 'tests/tools/schedule/*.test.tsx' --exclude 'tests/shell/*.test.ts'",
|
|
21
|
+
"test:watch": "vitest --exclude 'tests/tools/schedule/*.test.tsx' --exclude 'tests/shell/*.test.ts'",
|
|
22
22
|
"test:llm": "vitest run tests/tools/schedule/*.test.tsx",
|
|
23
|
+
"test:shell": "vitest run tests/shell/*.test.ts",
|
|
23
24
|
"format": "prettier --write '**/*.{ts,tsx}'",
|
|
24
25
|
"format:check": "prettier --check '**/*.{ts,tsx}'",
|
|
25
26
|
"lint": "eslint .",
|