prompt-language-shell 0.8.4 → 0.8.6

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.
@@ -0,0 +1,85 @@
1
+ import { homedir } from 'os';
2
+ import { join } from 'path';
3
+ import YAML from 'yaml';
4
+ import { ConfigError, DebugLevel } from './types.js';
5
+ import { defaultFileSystem } from '../services/filesystem.js';
6
+ import { isValidAnthropicApiKey, validateConfig } from './validation.js';
7
+ const RUNTIME_CONFIGURATION_FILE = '.plsrc';
8
+ function getConfigFile() {
9
+ return join(homedir(), RUNTIME_CONFIGURATION_FILE);
10
+ }
11
+ function parseYamlConfig(content) {
12
+ try {
13
+ return YAML.parse(content);
14
+ }
15
+ catch (error) {
16
+ throw new ConfigError('Failed to parse configuration file', error instanceof Error ? error : undefined);
17
+ }
18
+ }
19
+ export function getConfigPath() {
20
+ return getConfigFile();
21
+ }
22
+ export function configExists(fs = defaultFileSystem) {
23
+ return fs.exists(getConfigFile());
24
+ }
25
+ export function loadConfig(fs = defaultFileSystem) {
26
+ const configFile = getConfigFile();
27
+ if (!fs.exists(configFile)) {
28
+ throw new ConfigError('Configuration not found');
29
+ }
30
+ const content = fs.readFile(configFile, 'utf-8');
31
+ const parsed = parseYamlConfig(content);
32
+ return validateConfig(parsed);
33
+ }
34
+ export function hasValidAnthropicKey() {
35
+ try {
36
+ const config = loadConfig();
37
+ return (!!config.anthropic.key && isValidAnthropicApiKey(config.anthropic.key));
38
+ }
39
+ catch {
40
+ return false;
41
+ }
42
+ }
43
+ export function mergeConfig(existingContent, sectionName, newValues) {
44
+ const parsed = existingContent.trim()
45
+ ? YAML.parse(existingContent)
46
+ : {};
47
+ // Update or add section
48
+ const section = parsed[sectionName] ?? {};
49
+ for (const [key, value] of Object.entries(newValues)) {
50
+ section[key] = value;
51
+ }
52
+ parsed[sectionName] = section;
53
+ // Sort sections alphabetically
54
+ const sortedKeys = Object.keys(parsed).sort();
55
+ const sortedConfig = {};
56
+ for (const key of sortedKeys) {
57
+ sortedConfig[key] = parsed[key];
58
+ }
59
+ // Convert back to YAML
60
+ return YAML.stringify(sortedConfig);
61
+ }
62
+ export function saveConfig(section, config, fs = defaultFileSystem) {
63
+ const configFile = getConfigFile();
64
+ const existingContent = fs.exists(configFile)
65
+ ? fs.readFile(configFile, 'utf-8')
66
+ : '';
67
+ const newContent = mergeConfig(existingContent, section, config);
68
+ fs.writeFile(configFile, newContent);
69
+ }
70
+ export function saveAnthropicConfig(config, fs = defaultFileSystem) {
71
+ saveConfig('anthropic', config, fs);
72
+ return loadConfig(fs);
73
+ }
74
+ export function saveDebugSetting(debug, fs = defaultFileSystem) {
75
+ saveConfig('settings', { debug }, fs);
76
+ }
77
+ export function loadDebugSetting(fs = defaultFileSystem) {
78
+ try {
79
+ const config = loadConfig(fs);
80
+ return config.settings?.debug ?? DebugLevel.None;
81
+ }
82
+ catch {
83
+ return DebugLevel.None;
84
+ }
85
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Returns a message requesting initial setup.
3
+ * Provides natural language variations that sound like a professional concierge
4
+ * preparing to serve, avoiding technical jargon.
5
+ *
6
+ * @param forFutureUse - If true, indicates setup is for future requests rather than
7
+ * an immediate task
8
+ */
9
+ export function getConfigurationRequiredMessage(forFutureUse = false) {
10
+ if (forFutureUse) {
11
+ const messages = [
12
+ "Before I can assist with your requests, let's get a few things ready.",
13
+ 'Let me set up a few things so I can help you in the future.',
14
+ "I'll need to prepare a few things before I can assist you.",
15
+ "Let's get everything ready so I can help with your tasks.",
16
+ "I need to set up a few things first, then I'll be ready to assist.",
17
+ 'Let me prepare everything so I can help you going forward.',
18
+ ];
19
+ return messages[Math.floor(Math.random() * messages.length)];
20
+ }
21
+ const messages = [
22
+ 'Before I can help, let me get a few things ready.',
23
+ 'I need to set up a few things first.',
24
+ 'Let me prepare everything before we begin.',
25
+ 'Just a moment while I get ready to assist you.',
26
+ "I'll need to get set up before I can help with that.",
27
+ 'Let me get everything ready for you.',
28
+ ];
29
+ return messages[Math.floor(Math.random() * messages.length)];
30
+ }
@@ -0,0 +1,167 @@
1
+ import YAML from 'yaml';
2
+ import { AnthropicModel, ConfigDefinitionType, DebugLevel, SUPPORTED_DEBUG_LEVELS, SUPPORTED_MODELS, } from './types.js';
3
+ import { flattenConfig } from '../services/config-utils.js';
4
+ import { getConfigLabel } from '../services/config-labels.js';
5
+ import { defaultFileSystem } from '../services/filesystem.js';
6
+ import { getConfigPath, loadConfig } from './io.js';
7
+ /**
8
+ * Convert a dotted config key to a readable label
9
+ * Example: "project.alpha.repo" -> "Project Alpha Repo"
10
+ */
11
+ export function keyToLabel(key) {
12
+ return key
13
+ .split('.')
14
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
15
+ .join(' ');
16
+ }
17
+ /**
18
+ * Core configuration schema - defines structure and types for system settings
19
+ */
20
+ const coreConfigSchema = {
21
+ 'anthropic.key': {
22
+ type: ConfigDefinitionType.RegExp,
23
+ required: true,
24
+ pattern: /^sk-ant-api03-[A-Za-z0-9_-]{95}$/,
25
+ description: 'Anthropic API key',
26
+ },
27
+ 'anthropic.model': {
28
+ type: ConfigDefinitionType.Enum,
29
+ required: true,
30
+ values: SUPPORTED_MODELS,
31
+ default: AnthropicModel.Haiku,
32
+ description: 'Anthropic model',
33
+ },
34
+ 'settings.debug': {
35
+ type: ConfigDefinitionType.Enum,
36
+ required: false,
37
+ values: SUPPORTED_DEBUG_LEVELS,
38
+ default: DebugLevel.None,
39
+ description: 'Debug mode',
40
+ },
41
+ };
42
+ /**
43
+ * Get complete configuration schema
44
+ * Currently returns core schema only
45
+ * Future: will merge with skill-declared schemas
46
+ */
47
+ export function getConfigSchema() {
48
+ return {
49
+ ...coreConfigSchema,
50
+ // Future: ...loadSkillSchemas()
51
+ };
52
+ }
53
+ /**
54
+ * Get missing required configuration keys
55
+ * Returns array of keys that are required but not present or invalid in config
56
+ */
57
+ export function getMissingConfigKeys() {
58
+ const schema = getConfigSchema();
59
+ const missing = [];
60
+ let currentConfig = null;
61
+ try {
62
+ currentConfig = loadConfig();
63
+ }
64
+ catch {
65
+ // Config doesn't exist
66
+ }
67
+ for (const [key, definition] of Object.entries(schema)) {
68
+ if (!definition.required) {
69
+ continue;
70
+ }
71
+ // Get current value for this key
72
+ const parts = key.split('.');
73
+ let value = currentConfig;
74
+ for (const part of parts) {
75
+ if (value && typeof value === 'object' && part in value) {
76
+ value = value[part];
77
+ }
78
+ else {
79
+ value = undefined;
80
+ break;
81
+ }
82
+ }
83
+ // Check if value is missing or invalid
84
+ if (value === undefined || value === null) {
85
+ missing.push(key);
86
+ continue;
87
+ }
88
+ // Validate based on type
89
+ let isValid = false;
90
+ switch (definition.type) {
91
+ case ConfigDefinitionType.RegExp:
92
+ isValid = typeof value === 'string' && definition.pattern.test(value);
93
+ break;
94
+ case ConfigDefinitionType.String:
95
+ isValid = typeof value === 'string';
96
+ break;
97
+ case ConfigDefinitionType.Enum:
98
+ isValid =
99
+ typeof value === 'string' && definition.values.includes(value);
100
+ break;
101
+ case ConfigDefinitionType.Number:
102
+ isValid = typeof value === 'number';
103
+ break;
104
+ case ConfigDefinitionType.Boolean:
105
+ isValid = typeof value === 'boolean';
106
+ break;
107
+ }
108
+ if (!isValid) {
109
+ missing.push(key);
110
+ }
111
+ }
112
+ return missing;
113
+ }
114
+ /**
115
+ * Get list of configured keys from config file
116
+ * Returns array of dot-notation keys that exist in the config file
117
+ */
118
+ export function getConfiguredKeys(fs = defaultFileSystem) {
119
+ try {
120
+ const configFile = getConfigPath();
121
+ if (!fs.exists(configFile)) {
122
+ return [];
123
+ }
124
+ const content = fs.readFile(configFile, 'utf-8');
125
+ const parsed = YAML.parse(content);
126
+ // Flatten nested config to dot notation
127
+ const flatConfig = flattenConfig(parsed);
128
+ return Object.keys(flatConfig);
129
+ }
130
+ catch {
131
+ return [];
132
+ }
133
+ }
134
+ /**
135
+ * Get available config structure for CONFIG tool
136
+ * Returns keys with descriptions only (no values for privacy)
137
+ * Marks optional keys as "(optional)"
138
+ */
139
+ export function getAvailableConfigStructure(fs = defaultFileSystem) {
140
+ const schema = getConfigSchema();
141
+ const structure = {};
142
+ // Try to load existing config to see which keys are already set
143
+ let flatConfig = {};
144
+ try {
145
+ const configFile = getConfigPath();
146
+ if (fs.exists(configFile)) {
147
+ const content = fs.readFile(configFile, 'utf-8');
148
+ const parsed = YAML.parse(content);
149
+ // Flatten nested config to dot notation
150
+ flatConfig = flattenConfig(parsed);
151
+ }
152
+ }
153
+ catch {
154
+ // Config file doesn't exist or can't be read
155
+ }
156
+ // Add schema keys with descriptions
157
+ for (const [key, definition] of Object.entries(schema)) {
158
+ structure[key] = definition.description;
159
+ }
160
+ // Add discovered keys that aren't in schema
161
+ for (const key of Object.keys(flatConfig)) {
162
+ if (!(key in structure)) {
163
+ structure[key] = getConfigLabel(key) || keyToLabel(key);
164
+ }
165
+ }
166
+ return structure;
167
+ }
@@ -0,0 +1,55 @@
1
+ import { ConfigDefinitionType } from './types.js';
2
+ import { getConfigSchema } from './schema.js';
3
+ /**
4
+ * Convert string value to appropriate type based on schema definition
5
+ */
6
+ export function parseConfigValue(key, stringValue, schema) {
7
+ // If we have a schema definition, use its type
8
+ if (key in schema) {
9
+ const definition = schema[key];
10
+ switch (definition.type) {
11
+ case ConfigDefinitionType.Boolean:
12
+ return stringValue === 'true';
13
+ case ConfigDefinitionType.Number:
14
+ return Number(stringValue);
15
+ case ConfigDefinitionType.String:
16
+ case ConfigDefinitionType.RegExp:
17
+ case ConfigDefinitionType.Enum:
18
+ return stringValue;
19
+ }
20
+ }
21
+ // No schema definition - try to infer type from string value
22
+ // This handles skill-defined configs that may not be in schema yet
23
+ if (stringValue === 'true' || stringValue === 'false') {
24
+ return stringValue === 'true';
25
+ }
26
+ if (!isNaN(Number(stringValue)) && stringValue.trim() !== '') {
27
+ return Number(stringValue);
28
+ }
29
+ return stringValue;
30
+ }
31
+ /**
32
+ * Unflatten dotted keys into nested structure
33
+ * Example: { "product.alpha.path": "value" } -> { product: { alpha: { path: "value" } } }
34
+ * Converts string values to appropriate types based on config schema
35
+ */
36
+ export function unflattenConfig(dotted) {
37
+ const result = {};
38
+ const schema = getConfigSchema();
39
+ for (const [dottedKey, stringValue] of Object.entries(dotted)) {
40
+ const parts = dottedKey.split('.');
41
+ const section = parts[0];
42
+ // Initialize section if needed
43
+ result[section] = result[section] ?? {};
44
+ // Build nested structure for this section
45
+ let current = result[section];
46
+ for (let i = 1; i < parts.length - 1; i++) {
47
+ current[parts[i]] = current[parts[i]] ?? {};
48
+ current = current[parts[i]];
49
+ }
50
+ // Convert string value to appropriate type and set
51
+ const typedValue = parseConfigValue(dottedKey, stringValue, schema);
52
+ current[parts[parts.length - 1]] = typedValue;
53
+ }
54
+ return result;
55
+ }
@@ -0,0 +1,30 @@
1
+ export var AnthropicModel;
2
+ (function (AnthropicModel) {
3
+ AnthropicModel["Sonnet"] = "claude-sonnet-4-5";
4
+ AnthropicModel["Haiku"] = "claude-haiku-4-5";
5
+ AnthropicModel["Opus"] = "claude-opus-4-1";
6
+ })(AnthropicModel || (AnthropicModel = {}));
7
+ export const SUPPORTED_MODELS = Object.values(AnthropicModel);
8
+ export var DebugLevel;
9
+ (function (DebugLevel) {
10
+ DebugLevel["None"] = "none";
11
+ DebugLevel["Info"] = "info";
12
+ DebugLevel["Verbose"] = "verbose";
13
+ })(DebugLevel || (DebugLevel = {}));
14
+ export const SUPPORTED_DEBUG_LEVELS = Object.values(DebugLevel);
15
+ export var ConfigDefinitionType;
16
+ (function (ConfigDefinitionType) {
17
+ ConfigDefinitionType["RegExp"] = "regexp";
18
+ ConfigDefinitionType["String"] = "string";
19
+ ConfigDefinitionType["Enum"] = "enum";
20
+ ConfigDefinitionType["Number"] = "number";
21
+ ConfigDefinitionType["Boolean"] = "boolean";
22
+ })(ConfigDefinitionType || (ConfigDefinitionType = {}));
23
+ export class ConfigError extends Error {
24
+ origin;
25
+ constructor(message, origin) {
26
+ super(message);
27
+ this.name = 'ConfigError';
28
+ this.origin = origin;
29
+ }
30
+ }
@@ -0,0 +1,52 @@
1
+ import { ConfigError, DebugLevel, SUPPORTED_DEBUG_LEVELS, SUPPORTED_MODELS, } from './types.js';
2
+ export function validateConfig(parsed) {
3
+ if (!parsed || typeof parsed !== 'object') {
4
+ throw new ConfigError('Invalid configuration format');
5
+ }
6
+ const config = parsed;
7
+ // Validate anthropic section
8
+ if (!config.anthropic || typeof config.anthropic !== 'object') {
9
+ throw new ConfigError('Missing or invalid anthropic configuration');
10
+ }
11
+ const { key, model } = config.anthropic;
12
+ if (!key || typeof key !== 'string') {
13
+ throw new ConfigError('Missing or invalid API key');
14
+ }
15
+ const validatedConfig = {
16
+ anthropic: {
17
+ key,
18
+ },
19
+ };
20
+ // Optional model - only set if valid
21
+ if (model && typeof model === 'string' && isValidAnthropicModel(model)) {
22
+ validatedConfig.anthropic.model = model;
23
+ }
24
+ // Optional settings section
25
+ if (config.settings && typeof config.settings === 'object') {
26
+ const settings = config.settings;
27
+ validatedConfig.settings = {};
28
+ if ('debug' in settings) {
29
+ // Handle migration from boolean to enum
30
+ if (typeof settings.debug === 'boolean') {
31
+ validatedConfig.settings.debug = settings.debug
32
+ ? DebugLevel.Info
33
+ : DebugLevel.None;
34
+ }
35
+ else if (typeof settings.debug === 'string' &&
36
+ SUPPORTED_DEBUG_LEVELS.includes(settings.debug)) {
37
+ validatedConfig.settings.debug = settings.debug;
38
+ }
39
+ }
40
+ }
41
+ return validatedConfig;
42
+ }
43
+ export function isValidAnthropicApiKey(key) {
44
+ // Anthropic API keys format: sk-ant-api03-XXXXX (108 chars total)
45
+ // - Prefix: sk-ant-api03- (13 chars)
46
+ // - Key body: 95 characters (uppercase, lowercase, digits, hyphens, underscores)
47
+ const apiKeyPattern = /^sk-ant-api03-[A-Za-z0-9_-]{95}$/;
48
+ return apiKeyPattern.test(key);
49
+ }
50
+ export function isValidAnthropicModel(model) {
51
+ return SUPPORTED_MODELS.includes(model);
52
+ }
@@ -0,0 +1,135 @@
1
+ import { ExecutionStatus } from '../services/shell.js';
2
+ import { formatDuration } from '../services/utils.js';
3
+ import { ExecuteActionType, } from './types.js';
4
+ /**
5
+ * Handles task completion logic and returns the appropriate action and state.
6
+ */
7
+ export function handleTaskCompletion(index, elapsed, context) {
8
+ const { taskInfos, message, summary, taskExecutionTimes } = context;
9
+ const updatedTimes = [...taskExecutionTimes, elapsed];
10
+ const updatedTaskInfos = taskInfos.map((task, i) => i === index ? { ...task, status: ExecutionStatus.Success, elapsed } : task);
11
+ if (index < taskInfos.length - 1) {
12
+ // More tasks to execute
13
+ return {
14
+ action: {
15
+ type: ExecuteActionType.TaskComplete,
16
+ payload: { index, elapsed },
17
+ },
18
+ finalState: {
19
+ message,
20
+ summary,
21
+ taskInfos: updatedTaskInfos,
22
+ completed: index + 1,
23
+ taskExecutionTimes: updatedTimes,
24
+ completionMessage: null,
25
+ error: null,
26
+ },
27
+ shouldComplete: false,
28
+ };
29
+ }
30
+ // All tasks complete
31
+ const summaryText = summary.trim() || 'Execution completed';
32
+ const totalElapsed = updatedTimes.reduce((sum, time) => sum + time, 0);
33
+ const completion = `${summaryText} in ${formatDuration(totalElapsed)}.`;
34
+ return {
35
+ action: {
36
+ type: ExecuteActionType.AllTasksComplete,
37
+ payload: { index, elapsed, summaryText },
38
+ },
39
+ finalState: {
40
+ message,
41
+ summary,
42
+ taskInfos: updatedTaskInfos,
43
+ completed: index + 1,
44
+ taskExecutionTimes: updatedTimes,
45
+ completionMessage: completion,
46
+ error: null,
47
+ },
48
+ shouldComplete: true,
49
+ };
50
+ }
51
+ /**
52
+ * Handles task error logic and returns the appropriate action and state.
53
+ */
54
+ export function handleTaskFailure(index, error, elapsed, context) {
55
+ const { taskInfos, message, summary, taskExecutionTimes } = context;
56
+ const task = taskInfos[index];
57
+ const isCritical = task.command.critical !== false; // Default to true
58
+ const updatedTaskInfos = taskInfos.map((task, i) => i === index ? { ...task, status: ExecutionStatus.Failed, elapsed } : task);
59
+ if (isCritical) {
60
+ // Critical failure - stop execution
61
+ return {
62
+ action: {
63
+ type: ExecuteActionType.TaskErrorCritical,
64
+ payload: { index, error },
65
+ },
66
+ finalState: {
67
+ message,
68
+ summary,
69
+ taskInfos: updatedTaskInfos,
70
+ completed: index + 1,
71
+ taskExecutionTimes,
72
+ completionMessage: null,
73
+ error,
74
+ },
75
+ shouldComplete: true,
76
+ shouldReportError: true,
77
+ };
78
+ }
79
+ // Non-critical failure - continue to next task
80
+ const updatedTimes = [...taskExecutionTimes, elapsed];
81
+ if (index < taskInfos.length - 1) {
82
+ return {
83
+ action: {
84
+ type: ExecuteActionType.TaskErrorContinue,
85
+ payload: { index, elapsed },
86
+ },
87
+ finalState: {
88
+ message,
89
+ summary,
90
+ taskInfos: updatedTaskInfos,
91
+ completed: index + 1,
92
+ taskExecutionTimes: updatedTimes,
93
+ completionMessage: null,
94
+ error: null,
95
+ },
96
+ shouldComplete: false,
97
+ shouldReportError: false,
98
+ };
99
+ }
100
+ // Last task, complete execution
101
+ const summaryText = summary.trim() || 'Execution completed';
102
+ const totalElapsed = updatedTimes.reduce((sum, time) => sum + time, 0);
103
+ const completion = `${summaryText} in ${formatDuration(totalElapsed)}.`;
104
+ return {
105
+ action: {
106
+ type: ExecuteActionType.LastTaskError,
107
+ payload: { index, elapsed, summaryText },
108
+ },
109
+ finalState: {
110
+ message,
111
+ summary,
112
+ taskInfos: updatedTaskInfos,
113
+ completed: index + 1,
114
+ taskExecutionTimes: updatedTimes,
115
+ completionMessage: completion,
116
+ error: null,
117
+ },
118
+ shouldComplete: true,
119
+ shouldReportError: false,
120
+ };
121
+ }
122
+ /**
123
+ * Builds final state for task abortion.
124
+ */
125
+ export function buildAbortedState(taskInfos, message, summary, completed, taskExecutionTimes) {
126
+ return {
127
+ message,
128
+ summary,
129
+ taskInfos,
130
+ completed,
131
+ taskExecutionTimes,
132
+ completionMessage: null,
133
+ error: null,
134
+ };
135
+ }
@@ -0,0 +1,35 @@
1
+ import { loadUserConfig } from '../services/loader.js';
2
+ import { replacePlaceholders } from '../services/resolver.js';
3
+ import { validatePlaceholderResolution } from './validation.js';
4
+ /**
5
+ * Processes tasks through the AI service to generate executable commands.
6
+ * Resolves placeholders in task descriptions and validates the results.
7
+ */
8
+ export async function processTasks(tasks, service) {
9
+ // Load user config for placeholder resolution
10
+ const userConfig = loadUserConfig();
11
+ // Format tasks for the execute tool and resolve placeholders
12
+ const taskDescriptions = tasks
13
+ .map((task) => {
14
+ const resolvedAction = replacePlaceholders(task.action, userConfig);
15
+ const params = task.params
16
+ ? ` (params: ${JSON.stringify(task.params)})`
17
+ : '';
18
+ return `- ${resolvedAction}${params}`;
19
+ })
20
+ .join('\n');
21
+ // Call execute tool to get commands
22
+ const result = await service.processWithTool(taskDescriptions, 'execute');
23
+ // Resolve placeholders in command strings
24
+ const resolvedCommands = (result.commands || []).map((cmd) => {
25
+ const resolved = replacePlaceholders(cmd.command, userConfig);
26
+ validatePlaceholderResolution(resolved);
27
+ return { ...cmd, command: resolved };
28
+ });
29
+ return {
30
+ message: result.message,
31
+ summary: result.summary || '',
32
+ commands: resolvedCommands,
33
+ debug: result.debug,
34
+ };
35
+ }