prompt-language-shell 0.9.4 → 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/Workflow.js +12 -1
- package/dist/components/controllers/Execute.js +3 -3
- package/dist/components/controllers/Schedule.js +6 -11
- package/dist/components/views/Debug.js +6 -1
- package/dist/components/views/Feedback.js +2 -13
- package/dist/components/views/Output.js +10 -8
- package/dist/components/views/Schedule.js +4 -3
- package/dist/components/views/Table.js +15 -0
- 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/anthropic.js +27 -31
- package/dist/services/colors.js +2 -1
- package/dist/services/filesystem.js +13 -1
- package/dist/services/logger.js +254 -21
- package/dist/services/messages.js +7 -4
- package/dist/services/monitor.js +288 -0
- package/dist/services/parser.js +13 -5
- package/dist/services/performance.js +14 -0
- package/dist/services/refinement.js +14 -11
- package/dist/services/router.js +159 -122
- package/dist/services/shell.js +32 -14
- package/dist/services/skills.js +35 -7
- package/dist/skills/execute.md +82 -3
- package/dist/skills/schedule.md +155 -0
- package/dist/tools/schedule.tool.js +1 -1
- package/package.json +5 -4
package/dist/services/logger.js
CHANGED
|
@@ -1,7 +1,22 @@
|
|
|
1
|
+
import { homedir, platform } from 'os';
|
|
2
|
+
import { dirname, join } from 'path';
|
|
1
3
|
import { DebugLevel } from '../configuration/types.js';
|
|
2
|
-
import { createDebug } from './components.js';
|
|
3
4
|
import { loadDebugSetting } from '../configuration/io.js';
|
|
4
5
|
import { Palette } from './colors.js';
|
|
6
|
+
import { createDebug } from './components.js';
|
|
7
|
+
import { defaultFileSystem } from './filesystem.js';
|
|
8
|
+
/**
|
|
9
|
+
* Enum controlling what content is shown in debug prompt output
|
|
10
|
+
* - LLM: Exact prompt as sent to LLM (no display formatting)
|
|
11
|
+
* - Skills: Same content with visual separators for readability
|
|
12
|
+
* - Summary: Condensed view (Name, Steps, Execution only)
|
|
13
|
+
*/
|
|
14
|
+
export var PromptDisplay;
|
|
15
|
+
(function (PromptDisplay) {
|
|
16
|
+
PromptDisplay["LLM"] = "llm";
|
|
17
|
+
PromptDisplay["Skills"] = "skills";
|
|
18
|
+
PromptDisplay["Summary"] = "summary";
|
|
19
|
+
})(PromptDisplay || (PromptDisplay = {}));
|
|
5
20
|
/**
|
|
6
21
|
* Debug logger for the application
|
|
7
22
|
* Logs information based on the current debug level setting
|
|
@@ -11,6 +26,112 @@ let currentDebugLevel = DebugLevel.None;
|
|
|
11
26
|
* Accumulated warnings to be displayed in the timeline
|
|
12
27
|
*/
|
|
13
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
|
+
}
|
|
14
135
|
/**
|
|
15
136
|
* Initialize the logger with the current debug level from config
|
|
16
137
|
*/
|
|
@@ -49,41 +170,153 @@ export function getWarnings() {
|
|
|
49
170
|
warnings.length = 0;
|
|
50
171
|
return result;
|
|
51
172
|
}
|
|
173
|
+
/**
|
|
174
|
+
* Join sections with separators matching display width
|
|
175
|
+
*/
|
|
176
|
+
function joinWithSeparators(sections) {
|
|
177
|
+
const separator = '-'.repeat(DISPLAY_CONTENT_WIDTH);
|
|
178
|
+
return sections.join('\n\n' + separator + '\n\n');
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Format a single skill definition as summary markdown
|
|
182
|
+
*/
|
|
183
|
+
function formatSkillSummary(skill) {
|
|
184
|
+
const lines = [];
|
|
185
|
+
lines.push(`### Name`);
|
|
186
|
+
lines.push(skill.name);
|
|
187
|
+
lines.push('');
|
|
188
|
+
if (skill.steps.length > 0) {
|
|
189
|
+
lines.push(`### Steps`);
|
|
190
|
+
for (const step of skill.steps) {
|
|
191
|
+
lines.push(`- ${step}`);
|
|
192
|
+
}
|
|
193
|
+
lines.push('');
|
|
194
|
+
}
|
|
195
|
+
if (skill.execution.length > 0) {
|
|
196
|
+
lines.push(`### Execution`);
|
|
197
|
+
for (const cmd of skill.execution) {
|
|
198
|
+
lines.push(`- ${cmd}`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return lines.join('\n').trim();
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Format skill definitions as summary for debug display
|
|
205
|
+
* Shows only Name, Steps, and Execution with visual separators
|
|
206
|
+
*/
|
|
207
|
+
export function formatSkillsSummary(definitions) {
|
|
208
|
+
if (definitions.length === 0) {
|
|
209
|
+
return '(no skills)';
|
|
210
|
+
}
|
|
211
|
+
const header = '## Available Skills';
|
|
212
|
+
const skillSummaries = definitions.map(formatSkillSummary);
|
|
213
|
+
return joinWithSeparators([header, ...skillSummaries]);
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Format skills section with visual separators for debug display
|
|
217
|
+
* Layout: Header description -> separator -> skills separated by lines
|
|
218
|
+
*/
|
|
219
|
+
function formatSkillsForDisplay(formattedSkills) {
|
|
220
|
+
if (!formattedSkills) {
|
|
221
|
+
return '(no skills)';
|
|
222
|
+
}
|
|
223
|
+
// Find the header (everything before first ### Name)
|
|
224
|
+
const firstNameIndex = formattedSkills.search(/^###\s+Name/m);
|
|
225
|
+
if (firstNameIndex === -1) {
|
|
226
|
+
return '(no skills)';
|
|
227
|
+
}
|
|
228
|
+
const header = formattedSkills.slice(0, firstNameIndex).trim();
|
|
229
|
+
const skillsContent = formattedSkills.slice(firstNameIndex);
|
|
230
|
+
// Split by ### Name to get individual skills
|
|
231
|
+
const skillParts = skillsContent
|
|
232
|
+
.split(/(?=^###\s+Name)/m)
|
|
233
|
+
.map((s) => s.trim())
|
|
234
|
+
.filter(Boolean);
|
|
235
|
+
if (skillParts.length === 0) {
|
|
236
|
+
return '(no skills)';
|
|
237
|
+
}
|
|
238
|
+
// Join header and skills with separators
|
|
239
|
+
return joinWithSeparators([header, ...skillParts]);
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Format prompt content based on the specified detail level
|
|
243
|
+
*
|
|
244
|
+
* - LLM: Returns header + base instructions + formatted skills (as sent to LLM)
|
|
245
|
+
* - Skills: Returns header + skills with visual separators (no base instructions)
|
|
246
|
+
* - Summary: Returns header + skill summaries (Name, Steps, Execution)
|
|
247
|
+
*/
|
|
248
|
+
export function formatPromptContent(toolName, command, baseInstructions, formattedSkills, mode, definitions) {
|
|
249
|
+
switch (mode) {
|
|
250
|
+
case PromptDisplay.LLM: {
|
|
251
|
+
const header = ['', `**Tool:** ${toolName}`];
|
|
252
|
+
return [...header, '', baseInstructions + formattedSkills].join('\n');
|
|
253
|
+
}
|
|
254
|
+
case PromptDisplay.Skills: {
|
|
255
|
+
const header = `\nTool: ${toolName}\nCommand: ${command}`;
|
|
256
|
+
const skillsDisplay = formatSkillsForDisplay(formattedSkills);
|
|
257
|
+
return joinWithSeparators([header, skillsDisplay]);
|
|
258
|
+
}
|
|
259
|
+
case PromptDisplay.Summary: {
|
|
260
|
+
const header = `\nTool: ${toolName}\nCommand: ${command}`;
|
|
261
|
+
const summary = definitions
|
|
262
|
+
? formatSkillsSummary(definitions)
|
|
263
|
+
: '(no skills)';
|
|
264
|
+
return joinWithSeparators([header, summary]);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
52
268
|
/**
|
|
53
269
|
* Create debug component for system prompts sent to the LLM
|
|
54
|
-
*
|
|
270
|
+
* Creates UI component at Verbose level, writes to file at Info or Verbose
|
|
271
|
+
*
|
|
272
|
+
* @param toolName - Name of the tool being invoked
|
|
273
|
+
* @param command - User command being processed
|
|
274
|
+
* @param baseInstructions - Base tool instructions (without skills)
|
|
275
|
+
* @param formattedSkills - Formatted skills section (as sent to LLM)
|
|
276
|
+
* @param definitions - Parsed skill definitions for summary display
|
|
55
277
|
*/
|
|
56
|
-
export function logPrompt(toolName, command,
|
|
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
|
|
57
286
|
if (currentDebugLevel !== DebugLevel.Verbose) {
|
|
58
287
|
return null;
|
|
59
288
|
}
|
|
60
|
-
const content =
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
instructions,
|
|
66
|
-
].join('\n');
|
|
67
|
-
// Calculate stats for the instructions
|
|
68
|
-
const lines = instructions.split('\n').length;
|
|
69
|
-
const bytes = Buffer.byteLength(instructions, 'utf-8');
|
|
289
|
+
const content = formatPromptContent(toolName, command, baseInstructions, formattedSkills, PromptDisplay.Summary, definitions);
|
|
290
|
+
// Calculate stats for the full prompt
|
|
291
|
+
const fullPrompt = baseInstructions + formattedSkills;
|
|
292
|
+
const lines = fullPrompt.split('\n').length;
|
|
293
|
+
const bytes = Buffer.byteLength(fullPrompt, 'utf-8');
|
|
70
294
|
const title = `SYSTEM PROMPT (${String(lines)} lines, ${String(bytes)} bytes)`;
|
|
71
295
|
return createDebug({ title, content, color: Palette.Gray });
|
|
72
296
|
}
|
|
73
297
|
/**
|
|
74
298
|
* Create debug component for LLM responses received
|
|
75
|
-
*
|
|
299
|
+
* Creates UI component at Verbose level, writes to file at Info or Verbose
|
|
76
300
|
*/
|
|
77
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
|
|
78
316
|
if (currentDebugLevel !== DebugLevel.Verbose) {
|
|
79
317
|
return null;
|
|
80
318
|
}
|
|
81
|
-
const content = [
|
|
82
|
-
'',
|
|
83
|
-
`Tool: ${toolName}`,
|
|
84
|
-
'',
|
|
85
|
-
JSON.stringify(response, null, 2),
|
|
86
|
-
].join('\n');
|
|
319
|
+
const content = ['', `Tool: ${toolName}`, '', jsonContent].join('\n');
|
|
87
320
|
const title = `LLM RESPONSE (${String(durationMs)} ms)`;
|
|
88
|
-
return createDebug({ title, content, color: Palette.
|
|
321
|
+
return createDebug({ title, content, color: Palette.LightGray });
|
|
89
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
|
+
}
|
package/dist/services/parser.js
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import YAML from 'yaml';
|
|
2
2
|
import { displayWarning } from './logger.js';
|
|
3
3
|
/**
|
|
4
|
-
* Validate
|
|
4
|
+
* Validate extracted sections from a skill
|
|
5
5
|
* Returns validation error if skill is invalid, null if valid
|
|
6
6
|
* Note: Name section is optional - key from filename is used as fallback
|
|
7
7
|
*/
|
|
8
|
-
|
|
9
|
-
const sections = extractSections(content);
|
|
8
|
+
function validateSections(sections, key) {
|
|
10
9
|
// Use key for error reporting if name not present
|
|
11
10
|
const skillName = sections.name || key;
|
|
12
11
|
// Check required sections (Name is now optional)
|
|
@@ -37,6 +36,15 @@ export function validateSkillStructure(content, key) {
|
|
|
37
36
|
}
|
|
38
37
|
return null;
|
|
39
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* Validate a skill without parsing it fully
|
|
41
|
+
* Returns validation error if skill is invalid, null if valid
|
|
42
|
+
* Note: Name section is optional - key from filename is used as fallback
|
|
43
|
+
*/
|
|
44
|
+
export function validateSkillStructure(content, key) {
|
|
45
|
+
const sections = extractSections(content);
|
|
46
|
+
return validateSections(sections, key);
|
|
47
|
+
}
|
|
40
48
|
/**
|
|
41
49
|
* Convert kebab-case key to Title Case display name
|
|
42
50
|
* Examples: "deploy-app" -> "Deploy App", "build-project-2" -> "Build Project 2"
|
|
@@ -64,8 +72,8 @@ export function parseSkillMarkdown(key, content) {
|
|
|
64
72
|
const sections = extractSections(content);
|
|
65
73
|
// Determine display name: prefer Name section, otherwise derive from key
|
|
66
74
|
const displayName = sections.name || keyToDisplayName(key);
|
|
67
|
-
// Validate
|
|
68
|
-
const validationError =
|
|
75
|
+
// Validate using already-extracted sections (avoids re-parsing)
|
|
76
|
+
const validationError = validateSections(sections, key);
|
|
69
77
|
// For invalid skills, return minimal definition with error
|
|
70
78
|
if (validationError) {
|
|
71
79
|
return {
|
|
@@ -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
|
+
}
|