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 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. Supported settings:
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
- - `anthropic.key` - Your API key
93
- - `anthropic.model` - The model to use
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 `pls` can
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
 
@@ -107,7 +107,18 @@ export const Workflow = ({ initialQueue, debug }) => {
107
107
  setQueue((queue) => [...queue, ...items]);
108
108
  },
109
109
  addToTimeline: (...items) => {
110
- setTimeline((prev) => [...prev, ...items]);
110
+ // Flush pending to timeline first, then add new items
111
+ // Both are added in a SINGLE setTimeline call to guarantee order
112
+ setCurrent((curr) => {
113
+ const { active, pending } = curr;
114
+ if (pending) {
115
+ const donePending = markAsDone(pending);
116
+ setTimeline((prev) => [...prev, donePending, ...items]);
117
+ return { active, pending: null };
118
+ }
119
+ setTimeline((prev) => [...prev, ...items]);
120
+ return curr;
121
+ });
111
122
  },
112
123
  }), []);
113
124
  // Global Esc handler removed - components handle their own Esc individually
@@ -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, index) => ({
144
- label: inputTasks[index]?.action ?? cmd.description,
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,
@@ -37,8 +37,7 @@ export function Schedule({ message, tasks, status, debug = DebugLevel.None, requ
37
37
  completedSelections,
38
38
  };
39
39
  requestHandlers.onCompleted(finalState);
40
- // Complete the selection phase - it goes to timeline
41
- // Callback will create a new Plan showing refined tasks (pending) + Confirm (active)
40
+ // Move Schedule to pending - callback will flush to timeline
42
41
  lifecycleHandlers.completeActive();
43
42
  void onSelectionConfirmed(concreteTasks);
44
43
  }
@@ -102,9 +101,9 @@ export function Schedule({ message, tasks, status, debug = DebugLevel.None, requ
102
101
  const options = task.params.options;
103
102
  const selectedIndex = newCompletedSelections[defineGroupIndex];
104
103
  const selectedOption = options[selectedIndex];
105
- // Use Execute as default - LLM will properly classify during refinement
104
+ // Use the command from the selected option
106
105
  refinedTasks.push({
107
- action: selectedOption,
106
+ action: selectedOption.command,
108
107
  type: TaskType.Execute,
109
108
  config: [],
110
109
  });
@@ -122,16 +121,12 @@ export function Schedule({ message, tasks, status, debug = DebugLevel.None, requ
122
121
  completedSelections: newCompletedSelections,
123
122
  };
124
123
  requestHandlers.onCompleted(finalState);
124
+ // Move Schedule to pending - refinement will flush it to timeline
125
+ // before adding Command, ensuring correct order
126
+ lifecycleHandlers.completeActive();
125
127
  if (onSelectionConfirmed) {
126
- // Complete the selection phase - it goes to timeline
127
- // Callback will create a new Plan showing refined tasks (pending) + Confirm (active)
128
- lifecycleHandlers.completeActive();
129
128
  void onSelectionConfirmed(refinedTasks);
130
129
  }
131
- else {
132
- // No selection callback, just complete normally
133
- lifecycleHandlers.completeActive();
134
- }
135
130
  }
136
131
  }
137
132
  }, { isActive: isActive && defineTask !== null });
@@ -3,5 +3,10 @@ import { Box, Text } from 'ink';
3
3
  const CONTENT_WIDTH = 80;
4
4
  const HORIZONTAL_PADDING = 2;
5
5
  export const Debug = ({ title, content, color }) => {
6
- return (_jsxs(Box, { flexDirection: "column", paddingX: HORIZONTAL_PADDING, paddingY: 1, borderStyle: "single", borderColor: color, width: CONTENT_WIDTH, children: [_jsx(Text, { color: color, wrap: "wrap", children: title }), _jsx(Text, { color: color, wrap: "wrap", children: content })] }));
6
+ // Plain text content - single bordered box
7
+ if (typeof content === 'string') {
8
+ return (_jsxs(Box, { flexDirection: "column", paddingX: HORIZONTAL_PADDING, paddingY: 1, borderStyle: "single", borderColor: color, width: CONTENT_WIDTH, children: [_jsx(Text, { color: color, wrap: "wrap", children: title }), _jsx(Text, { color: color, wrap: "wrap", children: content })] }));
9
+ }
10
+ // Array content - table with one column, each item in bordered row
11
+ return (_jsxs(Box, { flexDirection: "column", width: CONTENT_WIDTH, children: [_jsx(Box, { paddingX: HORIZONTAL_PADDING, paddingY: 1, borderStyle: "single", borderColor: color, width: CONTENT_WIDTH, children: _jsx(Text, { color: color, wrap: "wrap", children: title }) }), content.map((section, index) => (_jsx(Box, { paddingX: HORIZONTAL_PADDING, paddingY: 1, borderStyle: "single", borderColor: color, width: CONTENT_WIDTH, marginTop: -1, children: _jsx(Text, { color: color, wrap: "wrap", children: section }) }, index)))] }));
7
12
  };
@@ -1,19 +1,8 @@
1
- import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { Box, Text } from 'ink';
3
3
  import { ComponentStatus } from '../../types/components.js';
4
- import { FeedbackType } from '../../types/types.js';
5
4
  import { getFeedbackColor } from '../../services/colors.js';
6
- function getSymbol(type) {
7
- return {
8
- [FeedbackType.Info]: 'ℹ',
9
- [FeedbackType.Warning]: '⚠',
10
- [FeedbackType.Succeeded]: '✓',
11
- [FeedbackType.Aborted]: '⊘',
12
- [FeedbackType.Failed]: '✗',
13
- }[type];
14
- }
15
5
  export function Feedback({ type, message }) {
16
6
  const color = getFeedbackColor(type, ComponentStatus.Done);
17
- const symbol = getSymbol(type);
18
- return (_jsx(Box, { marginLeft: 1, children: _jsxs(Text, { color: color, children: [symbol, " ", message] }) }));
7
+ return (_jsx(Box, { marginLeft: 1, children: _jsx(Text, { color: color, children: message }) }));
19
8
  }
@@ -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, wrapMode, stdoutColor, stderrColor, } = config;
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: wrapMode, children: line }, `out-${index}`))), stderrLines.map((line, index) => (_jsx(Text, { color: stderrColor, wrap: wrapMode, children: line }, `err-${index}`)))] }));
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
  }
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Box } from 'ink';
3
3
  import { ComponentStatus } from '../../types/components.js';
4
- import { TaskType } from '../../types/types.js';
4
+ import { TaskType, } from '../../types/types.js';
5
5
  import { DebugLevel } from '../../configuration/types.js';
6
6
  import { getTaskColors, getTaskTypeLabel, Palette, } from '../../services/colors.js';
7
7
  import { Label } from './Label.js';
@@ -28,7 +28,8 @@ export function taskToListItem(task, highlightedChildIndex = null, isDefineTaskW
28
28
  }
29
29
  // Add children for Define tasks with options
30
30
  if (task.type === TaskType.Define && Array.isArray(task.params?.options)) {
31
- item.children = task.params.options.map((option, index) => {
31
+ const options = task.params.options;
32
+ item.children = options.map((option, index) => {
32
33
  // Determine the type based on selection state
33
34
  let childType = TaskType.Select;
34
35
  if (highlightedChildIndex !== null) {
@@ -40,7 +41,7 @@ export function taskToListItem(task, highlightedChildIndex = null, isDefineTaskW
40
41
  const planColors = getTaskColors(TaskType.Schedule, status);
41
42
  return {
42
43
  description: {
43
- text: option,
44
+ text: option.name,
44
45
  color: colors.description,
45
46
  highlightedColor: planColors.description,
46
47
  },
@@ -0,0 +1,15 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React from 'react';
3
+ import { Box, Text } from 'ink';
4
+ const DEFAULT_WIDTH = 80;
5
+ const DEFAULT_PADDING = 1;
6
+ export const Cell = ({ children, padding = DEFAULT_PADDING }) => (_jsx(Box, { paddingX: padding, children: _jsx(Text, { wrap: "wrap", children: children }) }));
7
+ export const Column = ({ children, width }) => (_jsx(Box, { flexDirection: "column", width: width, children: children }));
8
+ const Row = ({ children, color, innerWidth }) => (_jsxs(Box, { children: [_jsx(Text, { color: color, children: '│' }), _jsx(Box, { width: innerWidth, children: children }), _jsx(Text, { color: color, children: '│' })] }));
9
+ export const Table = ({ data, width = DEFAULT_WIDTH, color }) => {
10
+ const innerWidth = width - 2;
11
+ const TOP = '┌' + '─'.repeat(innerWidth) + '┐';
12
+ const DIV = '├' + '─'.repeat(innerWidth) + '┤';
13
+ const BOT = '└' + '─'.repeat(innerWidth) + '┘';
14
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: color, children: TOP }), data.map((content, index) => (_jsxs(React.Fragment, { children: [index > 0 && _jsx(Text, { color: color, children: DIV }), _jsx(Row, { color: color, innerWidth: innerWidth, children: _jsx(Cell, { children: content }) })] }, index))), _jsx(Text, { color: color, children: BOT })] }));
15
+ };
@@ -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
- // Format tasks for the execute tool and resolve placeholders
21
- const taskList = tasks
22
- .map((task) => {
23
- const resolvedAction = replacePlaceholders(task.action, userConfig);
24
- const params = task.params
25
- ? ` (params: ${JSON.stringify(task.params)})`
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 { ...cmd, command: resolved };
67
+ return {
68
+ ...cmd,
69
+ command: resolved,
70
+ memoryLimit: memoryLimitMB,
71
+ };
41
72
  });
42
73
  return {
43
74
  message: result.message,
@@ -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.errors || result.error || 'Command failed';
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,7 +1,7 @@
1
1
  import Anthropic from '@anthropic-ai/sdk';
2
2
  import { getAvailableConfigStructure, getConfiguredKeys, } from '../configuration/schema.js';
3
3
  import { logPrompt, logResponse } from './logger.js';
4
- import { formatSkillsForPrompt, loadSkillsWithValidation } from './skills.js';
4
+ import { loadSkillsForPrompt } from './skills.js';
5
5
  import { toolRegistry } from './registry.js';
6
6
  import { CommandResultSchema, IntrospectResultSchema, } from '../types/schemas.js';
7
7
  /**
@@ -69,36 +69,32 @@ export class AnthropicService {
69
69
  async processWithTool(command, toolName, customInstructions) {
70
70
  // Load tool from registry
71
71
  const tool = toolRegistry.getSchema(toolName);
72
- // Use custom instructions if provided, otherwise load from registry
73
- let systemPrompt;
74
- if (customInstructions) {
75
- // Custom instructions provided (typically for testing)
76
- systemPrompt = customInstructions;
72
+ // Check if this tool uses skills
73
+ const usesSkills = toolName === 'schedule' ||
74
+ toolName === 'introspect' ||
75
+ toolName === 'execute' ||
76
+ toolName === 'validate';
77
+ // Load base instructions and skills
78
+ const baseInstructions = customInstructions || toolRegistry.getInstructions(toolName);
79
+ let formattedSkills = '';
80
+ let skillDefinitions = [];
81
+ let systemPrompt = baseInstructions;
82
+ if (!customInstructions && usesSkills) {
83
+ const skillsResult = loadSkillsForPrompt();
84
+ formattedSkills = skillsResult.formatted;
85
+ skillDefinitions = skillsResult.definitions;
86
+ systemPrompt += formattedSkills;
77
87
  }
78
- else {
79
- // Load and build system prompt automatically (production)
80
- const instructions = toolRegistry.getInstructions(toolName);
81
- systemPrompt = instructions;
82
- // Add skills section for applicable tools
83
- if (toolName === 'schedule' ||
84
- toolName === 'introspect' ||
85
- toolName === 'execute' ||
86
- toolName === 'validate') {
87
- const skills = loadSkillsWithValidation();
88
- const skillsSection = formatSkillsForPrompt(skills);
89
- systemPrompt += skillsSection;
90
- }
91
- // Add config structure for configure tool only
92
- if (toolName === 'configure') {
93
- const configStructure = getAvailableConfigStructure();
94
- const configuredKeys = getConfiguredKeys();
95
- const configSection = '\n## Available Configuration\n\n' +
96
- 'Config structure (key: description):\n' +
97
- JSON.stringify(configStructure, null, 2) +
98
- '\n\nConfigured keys (keys that exist in config file):\n' +
99
- JSON.stringify(configuredKeys, null, 2);
100
- systemPrompt += configSection;
101
- }
88
+ // Add config structure for configure tool only
89
+ if (!customInstructions && toolName === 'configure') {
90
+ const configStructure = getAvailableConfigStructure();
91
+ const configuredKeys = getConfiguredKeys();
92
+ const configSection = '\n## Available Configuration\n\n' +
93
+ 'Config structure (key: description):\n' +
94
+ JSON.stringify(configStructure, null, 2) +
95
+ '\n\nConfigured keys (keys that exist in config file):\n' +
96
+ JSON.stringify(configuredKeys, null, 2);
97
+ systemPrompt += configSection;
102
98
  }
103
99
  // Build tools array - add web search for answer tool
104
100
  const tools = [tool];
@@ -111,7 +107,7 @@ export class AnthropicService {
111
107
  // Collect debug components
112
108
  const debug = [];
113
109
  // Log prompt at Verbose level
114
- const promptDebug = logPrompt(toolName, command, systemPrompt);
110
+ const promptDebug = logPrompt(toolName, command, baseInstructions, formattedSkills, skillDefinitions);
115
111
  if (promptDebug) {
116
112
  debug.push(promptDebug);
117
113
  }
@@ -20,6 +20,7 @@ export const Palette = {
20
20
  Yellow: '#cccc5c',
21
21
  Orange: '#f48c80',
22
22
  MediumOrange: '#d07560',
23
+ WarmOrange: '#ce985e',
23
24
  DarkOrange: '#ab5e40',
24
25
  BurntOrange: '#cc7a5c',
25
26
  Red: '#cc5c5c',
@@ -132,7 +133,7 @@ const taskColors = {
132
133
  */
133
134
  const feedbackColors = {
134
135
  [FeedbackType.Info]: Colors.Status.Info,
135
- [FeedbackType.Warning]: Palette.Yellow,
136
+ [FeedbackType.Warning]: Palette.WarmOrange,
136
137
  [FeedbackType.Succeeded]: Colors.Status.Success,
137
138
  [FeedbackType.Aborted]: Palette.MediumOrange,
138
139
  [FeedbackType.Failed]: Colors.Status.Error,
@@ -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}'`);