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.
- package/dist/configuration/io.js +85 -0
- package/dist/configuration/messages.js +30 -0
- package/dist/configuration/schema.js +167 -0
- package/dist/configuration/transformation.js +55 -0
- package/dist/configuration/types.js +30 -0
- package/dist/configuration/validation.js +52 -0
- package/dist/execution/handlers.js +135 -0
- package/dist/execution/processing.js +35 -0
- package/dist/execution/reducer.js +148 -0
- package/dist/execution/types.js +12 -0
- package/dist/execution/validation.js +12 -0
- package/dist/index.js +1 -1
- package/dist/services/anthropic.js +1 -1
- package/dist/services/colors.js +1 -1
- package/dist/services/components.js +35 -11
- package/dist/services/config-labels.js +15 -15
- package/dist/services/logger.js +2 -1
- package/dist/services/messages.js +32 -1
- package/dist/services/refinement.js +6 -6
- package/dist/services/router.js +43 -27
- package/dist/ui/Answer.js +36 -15
- package/dist/ui/Command.js +43 -23
- package/dist/ui/Component.js +147 -33
- package/dist/ui/Config.js +68 -78
- package/dist/ui/Confirm.js +34 -21
- package/dist/ui/Execute.js +107 -328
- package/dist/ui/Introspect.js +51 -24
- package/dist/ui/Label.js +1 -1
- package/dist/ui/Main.js +5 -1
- package/dist/ui/Refinement.js +8 -1
- package/dist/ui/Schedule.js +75 -52
- package/dist/ui/Validate.js +75 -77
- package/dist/ui/Workflow.js +50 -123
- package/package.json +1 -1
- package/dist/services/configuration.js +0 -409
package/dist/ui/Workflow.js
CHANGED
|
@@ -3,12 +3,11 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
|
3
3
|
import { Box, Static } from 'ink';
|
|
4
4
|
import { ComponentStatus, } from '../types/components.js';
|
|
5
5
|
import { ComponentName, FeedbackType } from '../types/types.js';
|
|
6
|
-
import { createFeedback,
|
|
7
|
-
import { DebugLevel } from '../services/configuration.js';
|
|
6
|
+
import { createFeedback, isSimple, markAsDone, } from '../services/components.js';
|
|
8
7
|
import { getWarnings } from '../services/logger.js';
|
|
9
8
|
import { getCancellationMessage } from '../services/messages.js';
|
|
10
9
|
import { exitApp } from '../services/process.js';
|
|
11
|
-
import {
|
|
10
|
+
import { SimpleComponent, ControllerComponent, TimelineComponent, } from './Component.js';
|
|
12
11
|
export const Workflow = ({ initialQueue, debug }) => {
|
|
13
12
|
const [timeline, setTimeline] = useState([]);
|
|
14
13
|
const [current, setCurrent] = useState({ active: null, pending: null });
|
|
@@ -36,25 +35,42 @@ export const Workflow = ({ initialQueue, debug }) => {
|
|
|
36
35
|
return { active: null, pending };
|
|
37
36
|
});
|
|
38
37
|
}, []);
|
|
39
|
-
//
|
|
40
|
-
const
|
|
41
|
-
|
|
38
|
+
// Request handlers - manages errors, aborts, and completions
|
|
39
|
+
const requestHandlers = useMemo(() => ({
|
|
40
|
+
onError: (error) => {
|
|
41
|
+
moveActiveToTimeline();
|
|
42
|
+
// Add feedback to queue
|
|
43
|
+
setQueue((queue) => [
|
|
44
|
+
...queue,
|
|
45
|
+
createFeedback(FeedbackType.Failed, error),
|
|
46
|
+
]);
|
|
47
|
+
},
|
|
48
|
+
onAborted: (operation) => {
|
|
49
|
+
moveActiveToTimeline();
|
|
50
|
+
// Add feedback to queue
|
|
51
|
+
const message = getCancellationMessage(operation);
|
|
52
|
+
setQueue((queue) => [
|
|
53
|
+
...queue,
|
|
54
|
+
createFeedback(FeedbackType.Aborted, message),
|
|
55
|
+
]);
|
|
56
|
+
},
|
|
57
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
|
|
58
|
+
onCompleted: (finalState) => {
|
|
42
59
|
setCurrent((curr) => {
|
|
43
60
|
const { active, pending } = curr;
|
|
44
61
|
if (!active || !('state' in active))
|
|
45
62
|
return curr;
|
|
46
|
-
|
|
63
|
+
// Save final state to definition
|
|
64
|
+
const managed = active;
|
|
47
65
|
const updated = {
|
|
48
|
-
...
|
|
49
|
-
state:
|
|
50
|
-
...stateful.state,
|
|
51
|
-
...newState,
|
|
52
|
-
},
|
|
66
|
+
...managed,
|
|
67
|
+
state: finalState,
|
|
53
68
|
};
|
|
54
69
|
return { active: updated, pending };
|
|
55
70
|
});
|
|
56
71
|
},
|
|
57
|
-
}), []);
|
|
72
|
+
}), [moveActiveToTimeline]);
|
|
73
|
+
// Lifecycle handlers - for components with active/pending states
|
|
58
74
|
const lifecycleHandlers = useMemo(() => ({
|
|
59
75
|
completeActive: (...items) => {
|
|
60
76
|
moveActiveToPending();
|
|
@@ -62,33 +78,6 @@ export const Workflow = ({ initialQueue, debug }) => {
|
|
|
62
78
|
setQueue((queue) => [...items, ...queue]);
|
|
63
79
|
}
|
|
64
80
|
},
|
|
65
|
-
}), [moveActiveToPending]);
|
|
66
|
-
const queueHandlers = useMemo(() => ({
|
|
67
|
-
addToQueue: (...items) => {
|
|
68
|
-
setQueue((queue) => [...queue, ...items]);
|
|
69
|
-
},
|
|
70
|
-
}), []);
|
|
71
|
-
const errorHandlers = useMemo(() => ({
|
|
72
|
-
onAborted: (operation) => {
|
|
73
|
-
moveActiveToTimeline();
|
|
74
|
-
// Add feedback to queue
|
|
75
|
-
const message = getCancellationMessage(operation);
|
|
76
|
-
setQueue((queue) => [
|
|
77
|
-
...queue,
|
|
78
|
-
createFeedback(FeedbackType.Aborted, message),
|
|
79
|
-
]);
|
|
80
|
-
},
|
|
81
|
-
onError: (error) => {
|
|
82
|
-
moveActiveToTimeline();
|
|
83
|
-
// Add feedback to queue
|
|
84
|
-
setQueue((queue) => [
|
|
85
|
-
...queue,
|
|
86
|
-
createFeedback(FeedbackType.Failed, error),
|
|
87
|
-
]);
|
|
88
|
-
},
|
|
89
|
-
}), [moveActiveToTimeline]);
|
|
90
|
-
// Workflow handlers - used for timeline/queue management
|
|
91
|
-
const workflowHandlers = useMemo(() => ({
|
|
92
81
|
completeActiveAndPending: (...items) => {
|
|
93
82
|
setCurrent((curr) => {
|
|
94
83
|
const { active, pending } = curr;
|
|
@@ -107,6 +96,12 @@ export const Workflow = ({ initialQueue, debug }) => {
|
|
|
107
96
|
setQueue((queue) => [...items, ...queue]);
|
|
108
97
|
}
|
|
109
98
|
},
|
|
99
|
+
}), [moveActiveToPending]);
|
|
100
|
+
// Workflow handlers - manages queue and timeline
|
|
101
|
+
const workflowHandlers = useMemo(() => ({
|
|
102
|
+
addToQueue: (...items) => {
|
|
103
|
+
setQueue((queue) => [...queue, ...items]);
|
|
104
|
+
},
|
|
110
105
|
addToTimeline: (...items) => {
|
|
111
106
|
setTimeline((prev) => [...prev, ...items]);
|
|
112
107
|
},
|
|
@@ -140,13 +135,13 @@ export const Workflow = ({ initialQueue, debug }) => {
|
|
|
140
135
|
const { active, pending } = current;
|
|
141
136
|
if (!active)
|
|
142
137
|
return;
|
|
143
|
-
if (
|
|
144
|
-
//
|
|
138
|
+
if (isSimple(active)) {
|
|
139
|
+
// Simple components move directly to timeline
|
|
145
140
|
const doneComponent = markAsDone(active);
|
|
146
141
|
setTimeline((prev) => [...prev, doneComponent]);
|
|
147
142
|
setCurrent({ active: null, pending });
|
|
148
143
|
}
|
|
149
|
-
//
|
|
144
|
+
// Managed components stay in active until handlers move them to pending
|
|
150
145
|
}, [current]);
|
|
151
146
|
// Check for accumulated warnings and add them to timeline
|
|
152
147
|
useEffect(() => {
|
|
@@ -180,86 +175,18 @@ export const Workflow = ({ initialQueue, debug }) => {
|
|
|
180
175
|
lastItem.props.type === FeedbackType.Failed;
|
|
181
176
|
exitApp(isFailed ? 1 : 0);
|
|
182
177
|
}, [current, queue, timeline]);
|
|
183
|
-
// Render active and pending
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
if (!active)
|
|
187
|
-
return null;
|
|
188
|
-
// For stateless components, render as-is
|
|
189
|
-
if (isStateless(active)) {
|
|
190
|
-
return _jsx(Component, { def: active, debug: debug }, active.id);
|
|
191
|
-
}
|
|
192
|
-
// For stateful components, inject focused handlers
|
|
193
|
-
const statefulActive = active;
|
|
194
|
-
const wrappedDef = {
|
|
195
|
-
...statefulActive,
|
|
196
|
-
props: {
|
|
197
|
-
...statefulActive.props,
|
|
198
|
-
stateHandlers,
|
|
199
|
-
lifecycleHandlers,
|
|
200
|
-
queueHandlers,
|
|
201
|
-
errorHandlers,
|
|
202
|
-
workflowHandlers,
|
|
203
|
-
},
|
|
204
|
-
};
|
|
205
|
-
return _jsx(Component, { def: wrappedDef, debug: debug }, active.id);
|
|
206
|
-
}, [
|
|
207
|
-
current,
|
|
208
|
-
debug,
|
|
209
|
-
stateHandlers,
|
|
210
|
-
lifecycleHandlers,
|
|
211
|
-
queueHandlers,
|
|
212
|
-
errorHandlers,
|
|
213
|
-
workflowHandlers,
|
|
214
|
-
]);
|
|
215
|
-
const pendingComponent = useMemo(() => {
|
|
216
|
-
const { pending } = current;
|
|
217
|
-
if (!pending)
|
|
178
|
+
// Render component with handlers (used for both active and pending)
|
|
179
|
+
const renderComponent = useCallback((def, status) => {
|
|
180
|
+
if (!def)
|
|
218
181
|
return null;
|
|
219
|
-
// For
|
|
220
|
-
if (
|
|
221
|
-
return _jsx(
|
|
182
|
+
// For simple components, render as-is
|
|
183
|
+
if (isSimple(def)) {
|
|
184
|
+
return _jsx(SimpleComponent, { def: def }, def.id);
|
|
222
185
|
}
|
|
223
|
-
// For
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
stateHandlers,
|
|
230
|
-
lifecycleHandlers,
|
|
231
|
-
queueHandlers,
|
|
232
|
-
errorHandlers,
|
|
233
|
-
workflowHandlers,
|
|
234
|
-
},
|
|
235
|
-
};
|
|
236
|
-
return _jsx(Component, { def: wrappedDef, debug: debug }, pending.id);
|
|
237
|
-
}, [
|
|
238
|
-
current,
|
|
239
|
-
debug,
|
|
240
|
-
stateHandlers,
|
|
241
|
-
lifecycleHandlers,
|
|
242
|
-
queueHandlers,
|
|
243
|
-
errorHandlers,
|
|
244
|
-
workflowHandlers,
|
|
245
|
-
]);
|
|
246
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Static, { items: timeline, children: (item) => {
|
|
247
|
-
// For stateful timeline components, inject handlers (useEffect hooks may still run)
|
|
248
|
-
let def = item;
|
|
249
|
-
if (!isStateless(item)) {
|
|
250
|
-
const statefulItem = item;
|
|
251
|
-
def = {
|
|
252
|
-
...statefulItem,
|
|
253
|
-
props: {
|
|
254
|
-
...statefulItem.props,
|
|
255
|
-
stateHandlers,
|
|
256
|
-
lifecycleHandlers,
|
|
257
|
-
queueHandlers,
|
|
258
|
-
errorHandlers,
|
|
259
|
-
workflowHandlers,
|
|
260
|
-
},
|
|
261
|
-
};
|
|
262
|
-
}
|
|
263
|
-
return (_jsx(Box, { marginTop: 1, children: _jsx(Component, { def: def, debug: DebugLevel.None }) }, item.id));
|
|
264
|
-
} }, "timeline"), pendingComponent && _jsx(Box, { marginTop: 1, children: pendingComponent }), activeComponent && _jsx(Box, { marginTop: 1, children: activeComponent })] }));
|
|
186
|
+
// For managed components, inject handlers via ControllerComponent
|
|
187
|
+
return (_jsx(ControllerComponent, { def: { ...def, status }, debug: debug, requestHandlers: requestHandlers, lifecycleHandlers: lifecycleHandlers, workflowHandlers: workflowHandlers }, def.id));
|
|
188
|
+
}, [debug, requestHandlers, lifecycleHandlers, workflowHandlers]);
|
|
189
|
+
const activeComponent = useMemo(() => renderComponent(current.active, ComponentStatus.Active), [current.active, renderComponent]);
|
|
190
|
+
const pendingComponent = useMemo(() => renderComponent(current.pending, ComponentStatus.Pending), [current.pending, renderComponent]);
|
|
191
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Static, { items: timeline, children: (item) => (_jsx(Box, { marginTop: 1, children: _jsx(TimelineComponent, { def: item }) }, item.id)) }, "timeline"), pendingComponent && _jsx(Box, { marginTop: 1, children: pendingComponent }), activeComponent && _jsx(Box, { marginTop: 1, children: activeComponent })] }));
|
|
265
192
|
};
|
package/package.json
CHANGED
|
@@ -1,409 +0,0 @@
|
|
|
1
|
-
import { homedir } from 'os';
|
|
2
|
-
import { join } from 'path';
|
|
3
|
-
import YAML from 'yaml';
|
|
4
|
-
import { getConfigLabel } from './config-labels.js';
|
|
5
|
-
import { flattenConfig } from './config-utils.js';
|
|
6
|
-
import { defaultFileSystem } from './filesystem.js';
|
|
7
|
-
/**
|
|
8
|
-
* Convert a dotted config key to a readable label
|
|
9
|
-
* Example: "project.alpha.repo" -> "Project Alpha Repo"
|
|
10
|
-
*/
|
|
11
|
-
function keyToLabel(key) {
|
|
12
|
-
return key
|
|
13
|
-
.split('.')
|
|
14
|
-
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
15
|
-
.join(' ');
|
|
16
|
-
}
|
|
17
|
-
export var AnthropicModel;
|
|
18
|
-
(function (AnthropicModel) {
|
|
19
|
-
AnthropicModel["Sonnet"] = "claude-sonnet-4-5";
|
|
20
|
-
AnthropicModel["Haiku"] = "claude-haiku-4-5";
|
|
21
|
-
AnthropicModel["Opus"] = "claude-opus-4-1";
|
|
22
|
-
})(AnthropicModel || (AnthropicModel = {}));
|
|
23
|
-
export const SUPPORTED_MODELS = Object.values(AnthropicModel);
|
|
24
|
-
export var DebugLevel;
|
|
25
|
-
(function (DebugLevel) {
|
|
26
|
-
DebugLevel["None"] = "none";
|
|
27
|
-
DebugLevel["Info"] = "info";
|
|
28
|
-
DebugLevel["Verbose"] = "verbose";
|
|
29
|
-
})(DebugLevel || (DebugLevel = {}));
|
|
30
|
-
export const SUPPORTED_DEBUG_LEVELS = Object.values(DebugLevel);
|
|
31
|
-
export var ConfigDefinitionType;
|
|
32
|
-
(function (ConfigDefinitionType) {
|
|
33
|
-
ConfigDefinitionType["RegExp"] = "regexp";
|
|
34
|
-
ConfigDefinitionType["String"] = "string";
|
|
35
|
-
ConfigDefinitionType["Enum"] = "enum";
|
|
36
|
-
ConfigDefinitionType["Number"] = "number";
|
|
37
|
-
ConfigDefinitionType["Boolean"] = "boolean";
|
|
38
|
-
})(ConfigDefinitionType || (ConfigDefinitionType = {}));
|
|
39
|
-
export class ConfigError extends Error {
|
|
40
|
-
origin;
|
|
41
|
-
constructor(message, origin) {
|
|
42
|
-
super(message);
|
|
43
|
-
this.name = 'ConfigError';
|
|
44
|
-
this.origin = origin;
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
function getConfigFile() {
|
|
48
|
-
return join(homedir(), '.plsrc');
|
|
49
|
-
}
|
|
50
|
-
function parseYamlConfig(content) {
|
|
51
|
-
try {
|
|
52
|
-
return YAML.parse(content);
|
|
53
|
-
}
|
|
54
|
-
catch (error) {
|
|
55
|
-
throw new ConfigError('Failed to parse configuration file', error instanceof Error ? error : undefined);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
function validateConfig(parsed) {
|
|
59
|
-
if (!parsed || typeof parsed !== 'object') {
|
|
60
|
-
throw new ConfigError('Invalid configuration format');
|
|
61
|
-
}
|
|
62
|
-
const config = parsed;
|
|
63
|
-
// Validate anthropic section
|
|
64
|
-
if (!config.anthropic || typeof config.anthropic !== 'object') {
|
|
65
|
-
throw new ConfigError('Missing or invalid anthropic configuration');
|
|
66
|
-
}
|
|
67
|
-
const { key, model } = config.anthropic;
|
|
68
|
-
if (!key || typeof key !== 'string') {
|
|
69
|
-
throw new ConfigError('Missing or invalid API key');
|
|
70
|
-
}
|
|
71
|
-
const validatedConfig = {
|
|
72
|
-
anthropic: {
|
|
73
|
-
key,
|
|
74
|
-
},
|
|
75
|
-
};
|
|
76
|
-
// Optional model - only set if valid
|
|
77
|
-
if (model && typeof model === 'string' && isValidAnthropicModel(model)) {
|
|
78
|
-
validatedConfig.anthropic.model = model;
|
|
79
|
-
}
|
|
80
|
-
// Optional settings section
|
|
81
|
-
if (config.settings && typeof config.settings === 'object') {
|
|
82
|
-
const settings = config.settings;
|
|
83
|
-
validatedConfig.settings = {};
|
|
84
|
-
if ('debug' in settings) {
|
|
85
|
-
// Handle migration from boolean to enum
|
|
86
|
-
if (typeof settings.debug === 'boolean') {
|
|
87
|
-
validatedConfig.settings.debug = settings.debug
|
|
88
|
-
? DebugLevel.Info
|
|
89
|
-
: DebugLevel.None;
|
|
90
|
-
}
|
|
91
|
-
else if (typeof settings.debug === 'string' &&
|
|
92
|
-
SUPPORTED_DEBUG_LEVELS.includes(settings.debug)) {
|
|
93
|
-
validatedConfig.settings.debug = settings.debug;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
return validatedConfig;
|
|
98
|
-
}
|
|
99
|
-
export function loadConfig(fs = defaultFileSystem) {
|
|
100
|
-
const configFile = getConfigFile();
|
|
101
|
-
if (!fs.exists(configFile)) {
|
|
102
|
-
throw new ConfigError('Configuration not found');
|
|
103
|
-
}
|
|
104
|
-
const content = fs.readFile(configFile, 'utf-8');
|
|
105
|
-
const parsed = parseYamlConfig(content);
|
|
106
|
-
return validateConfig(parsed);
|
|
107
|
-
}
|
|
108
|
-
export function getConfigPath() {
|
|
109
|
-
return getConfigFile();
|
|
110
|
-
}
|
|
111
|
-
export function configExists(fs = defaultFileSystem) {
|
|
112
|
-
return fs.exists(getConfigFile());
|
|
113
|
-
}
|
|
114
|
-
export function isValidAnthropicApiKey(key) {
|
|
115
|
-
// Anthropic API keys format: sk-ant-api03-XXXXX (108 chars total)
|
|
116
|
-
// - Prefix: sk-ant-api03- (13 chars)
|
|
117
|
-
// - Key body: 95 characters (uppercase, lowercase, digits, hyphens, underscores)
|
|
118
|
-
const apiKeyPattern = /^sk-ant-api03-[A-Za-z0-9_-]{95}$/;
|
|
119
|
-
return apiKeyPattern.test(key);
|
|
120
|
-
}
|
|
121
|
-
export function isValidAnthropicModel(model) {
|
|
122
|
-
return SUPPORTED_MODELS.includes(model);
|
|
123
|
-
}
|
|
124
|
-
export function hasValidAnthropicKey() {
|
|
125
|
-
try {
|
|
126
|
-
const config = loadConfig();
|
|
127
|
-
return (!!config.anthropic.key && isValidAnthropicApiKey(config.anthropic.key));
|
|
128
|
-
}
|
|
129
|
-
catch {
|
|
130
|
-
return false;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
export function mergeConfig(existingContent, sectionName, newValues) {
|
|
134
|
-
const parsed = existingContent.trim()
|
|
135
|
-
? YAML.parse(existingContent)
|
|
136
|
-
: {};
|
|
137
|
-
// Update or add section
|
|
138
|
-
const section = parsed[sectionName] ?? {};
|
|
139
|
-
for (const [key, value] of Object.entries(newValues)) {
|
|
140
|
-
section[key] = value;
|
|
141
|
-
}
|
|
142
|
-
parsed[sectionName] = section;
|
|
143
|
-
// Sort sections alphabetically
|
|
144
|
-
const sortedKeys = Object.keys(parsed).sort();
|
|
145
|
-
const sortedConfig = {};
|
|
146
|
-
for (const key of sortedKeys) {
|
|
147
|
-
sortedConfig[key] = parsed[key];
|
|
148
|
-
}
|
|
149
|
-
// Convert back to YAML
|
|
150
|
-
return YAML.stringify(sortedConfig);
|
|
151
|
-
}
|
|
152
|
-
export function saveConfig(section, config, fs = defaultFileSystem) {
|
|
153
|
-
const configFile = getConfigFile();
|
|
154
|
-
const existingContent = fs.exists(configFile)
|
|
155
|
-
? fs.readFile(configFile, 'utf-8')
|
|
156
|
-
: '';
|
|
157
|
-
const newContent = mergeConfig(existingContent, section, config);
|
|
158
|
-
fs.writeFile(configFile, newContent);
|
|
159
|
-
}
|
|
160
|
-
export function saveAnthropicConfig(config, fs = defaultFileSystem) {
|
|
161
|
-
saveConfig('anthropic', config, fs);
|
|
162
|
-
return loadConfig(fs);
|
|
163
|
-
}
|
|
164
|
-
export function saveDebugSetting(debug, fs = defaultFileSystem) {
|
|
165
|
-
saveConfig('settings', { debug }, fs);
|
|
166
|
-
}
|
|
167
|
-
export function loadDebugSetting(fs = defaultFileSystem) {
|
|
168
|
-
try {
|
|
169
|
-
const config = loadConfig(fs);
|
|
170
|
-
return config.settings?.debug ?? DebugLevel.None;
|
|
171
|
-
}
|
|
172
|
-
catch {
|
|
173
|
-
return DebugLevel.None;
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
/**
|
|
177
|
-
* Returns a message requesting initial setup.
|
|
178
|
-
* Provides natural language variations that sound like a professional concierge
|
|
179
|
-
* preparing to serve, avoiding technical jargon.
|
|
180
|
-
*
|
|
181
|
-
* @param forFutureUse - If true, indicates setup is for future requests rather than
|
|
182
|
-
* an immediate task
|
|
183
|
-
*/
|
|
184
|
-
export function getConfigurationRequiredMessage(forFutureUse = false) {
|
|
185
|
-
if (forFutureUse) {
|
|
186
|
-
const messages = [
|
|
187
|
-
"Before I can assist with your requests, let's get a few things ready.",
|
|
188
|
-
'Let me set up a few things so I can help you in the future.',
|
|
189
|
-
"I'll need to prepare a few things before I can assist you.",
|
|
190
|
-
"Let's get everything ready so I can help with your tasks.",
|
|
191
|
-
"I need to set up a few things first, then I'll be ready to assist.",
|
|
192
|
-
'Let me prepare everything so I can help you going forward.',
|
|
193
|
-
];
|
|
194
|
-
return messages[Math.floor(Math.random() * messages.length)];
|
|
195
|
-
}
|
|
196
|
-
const messages = [
|
|
197
|
-
'Before I can help, let me get a few things ready.',
|
|
198
|
-
'I need to set up a few things first.',
|
|
199
|
-
'Let me prepare everything before we begin.',
|
|
200
|
-
'Just a moment while I get ready to assist you.',
|
|
201
|
-
"I'll need to get set up before I can help with that.",
|
|
202
|
-
'Let me get everything ready for you.',
|
|
203
|
-
];
|
|
204
|
-
return messages[Math.floor(Math.random() * messages.length)];
|
|
205
|
-
}
|
|
206
|
-
/**
|
|
207
|
-
* Core configuration schema - defines structure and types for system settings
|
|
208
|
-
*/
|
|
209
|
-
const coreConfigSchema = {
|
|
210
|
-
'anthropic.key': {
|
|
211
|
-
type: ConfigDefinitionType.RegExp,
|
|
212
|
-
required: true,
|
|
213
|
-
pattern: /^sk-ant-api03-[A-Za-z0-9_-]{95}$/,
|
|
214
|
-
description: 'Anthropic API key',
|
|
215
|
-
},
|
|
216
|
-
'anthropic.model': {
|
|
217
|
-
type: ConfigDefinitionType.Enum,
|
|
218
|
-
required: true,
|
|
219
|
-
values: SUPPORTED_MODELS,
|
|
220
|
-
default: AnthropicModel.Haiku,
|
|
221
|
-
description: 'Anthropic model',
|
|
222
|
-
},
|
|
223
|
-
'settings.debug': {
|
|
224
|
-
type: ConfigDefinitionType.Enum,
|
|
225
|
-
required: false,
|
|
226
|
-
values: SUPPORTED_DEBUG_LEVELS,
|
|
227
|
-
default: DebugLevel.None,
|
|
228
|
-
description: 'Debug mode',
|
|
229
|
-
},
|
|
230
|
-
};
|
|
231
|
-
/**
|
|
232
|
-
* Get complete configuration schema
|
|
233
|
-
* Currently returns core schema only
|
|
234
|
-
* Future: will merge with skill-declared schemas
|
|
235
|
-
*/
|
|
236
|
-
export function getConfigSchema() {
|
|
237
|
-
return {
|
|
238
|
-
...coreConfigSchema,
|
|
239
|
-
// Future: ...loadSkillSchemas()
|
|
240
|
-
};
|
|
241
|
-
}
|
|
242
|
-
/**
|
|
243
|
-
* Get missing required configuration keys
|
|
244
|
-
* Returns array of keys that are required but not present or invalid in config
|
|
245
|
-
*/
|
|
246
|
-
export function getMissingConfigKeys() {
|
|
247
|
-
const schema = getConfigSchema();
|
|
248
|
-
const missing = [];
|
|
249
|
-
let currentConfig = null;
|
|
250
|
-
try {
|
|
251
|
-
currentConfig = loadConfig();
|
|
252
|
-
}
|
|
253
|
-
catch {
|
|
254
|
-
// Config doesn't exist
|
|
255
|
-
}
|
|
256
|
-
for (const [key, definition] of Object.entries(schema)) {
|
|
257
|
-
if (!definition.required) {
|
|
258
|
-
continue;
|
|
259
|
-
}
|
|
260
|
-
// Get current value for this key
|
|
261
|
-
const parts = key.split('.');
|
|
262
|
-
let value = currentConfig;
|
|
263
|
-
for (const part of parts) {
|
|
264
|
-
if (value && typeof value === 'object' && part in value) {
|
|
265
|
-
value = value[part];
|
|
266
|
-
}
|
|
267
|
-
else {
|
|
268
|
-
value = undefined;
|
|
269
|
-
break;
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
// Check if value is missing or invalid
|
|
273
|
-
if (value === undefined || value === null) {
|
|
274
|
-
missing.push(key);
|
|
275
|
-
continue;
|
|
276
|
-
}
|
|
277
|
-
// Validate based on type
|
|
278
|
-
let isValid = false;
|
|
279
|
-
switch (definition.type) {
|
|
280
|
-
case ConfigDefinitionType.RegExp:
|
|
281
|
-
isValid = typeof value === 'string' && definition.pattern.test(value);
|
|
282
|
-
break;
|
|
283
|
-
case ConfigDefinitionType.String:
|
|
284
|
-
isValid = typeof value === 'string';
|
|
285
|
-
break;
|
|
286
|
-
case ConfigDefinitionType.Enum:
|
|
287
|
-
isValid =
|
|
288
|
-
typeof value === 'string' && definition.values.includes(value);
|
|
289
|
-
break;
|
|
290
|
-
case ConfigDefinitionType.Number:
|
|
291
|
-
isValid = typeof value === 'number';
|
|
292
|
-
break;
|
|
293
|
-
case ConfigDefinitionType.Boolean:
|
|
294
|
-
isValid = typeof value === 'boolean';
|
|
295
|
-
break;
|
|
296
|
-
}
|
|
297
|
-
if (!isValid) {
|
|
298
|
-
missing.push(key);
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
return missing;
|
|
302
|
-
}
|
|
303
|
-
/**
|
|
304
|
-
* Get list of configured keys from config file
|
|
305
|
-
* Returns array of dot-notation keys that exist in the config file
|
|
306
|
-
*/
|
|
307
|
-
export function getConfiguredKeys(fs = defaultFileSystem) {
|
|
308
|
-
try {
|
|
309
|
-
const configFile = getConfigFile();
|
|
310
|
-
if (!fs.exists(configFile)) {
|
|
311
|
-
return [];
|
|
312
|
-
}
|
|
313
|
-
const content = fs.readFile(configFile, 'utf-8');
|
|
314
|
-
const parsed = YAML.parse(content);
|
|
315
|
-
// Flatten nested config to dot notation
|
|
316
|
-
const flatConfig = flattenConfig(parsed);
|
|
317
|
-
return Object.keys(flatConfig);
|
|
318
|
-
}
|
|
319
|
-
catch {
|
|
320
|
-
return [];
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
/**
|
|
324
|
-
* Get available config structure for CONFIG tool
|
|
325
|
-
* Returns keys with descriptions only (no values for privacy)
|
|
326
|
-
* Marks optional keys as "(optional)"
|
|
327
|
-
*/
|
|
328
|
-
export function getAvailableConfigStructure(fs = defaultFileSystem) {
|
|
329
|
-
const schema = getConfigSchema();
|
|
330
|
-
const structure = {};
|
|
331
|
-
// Try to load existing config to see which keys are already set
|
|
332
|
-
let flatConfig = {};
|
|
333
|
-
try {
|
|
334
|
-
const configFile = getConfigFile();
|
|
335
|
-
if (fs.exists(configFile)) {
|
|
336
|
-
const content = fs.readFile(configFile, 'utf-8');
|
|
337
|
-
const parsed = YAML.parse(content);
|
|
338
|
-
// Flatten nested config to dot notation
|
|
339
|
-
flatConfig = flattenConfig(parsed);
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
catch {
|
|
343
|
-
// Config file doesn't exist or can't be read
|
|
344
|
-
}
|
|
345
|
-
// Add schema keys with descriptions
|
|
346
|
-
for (const [key, definition] of Object.entries(schema)) {
|
|
347
|
-
structure[key] = definition.description;
|
|
348
|
-
}
|
|
349
|
-
// Add discovered keys that aren't in schema
|
|
350
|
-
for (const key of Object.keys(flatConfig)) {
|
|
351
|
-
if (!(key in structure)) {
|
|
352
|
-
structure[key] = getConfigLabel(key) || keyToLabel(key);
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
return structure;
|
|
356
|
-
}
|
|
357
|
-
/**
|
|
358
|
-
* Convert string value to appropriate type based on schema definition
|
|
359
|
-
*/
|
|
360
|
-
function parseConfigValue(key, stringValue, schema) {
|
|
361
|
-
// If we have a schema definition, use its type
|
|
362
|
-
if (key in schema) {
|
|
363
|
-
const definition = schema[key];
|
|
364
|
-
switch (definition.type) {
|
|
365
|
-
case ConfigDefinitionType.Boolean:
|
|
366
|
-
return stringValue === 'true';
|
|
367
|
-
case ConfigDefinitionType.Number:
|
|
368
|
-
return Number(stringValue);
|
|
369
|
-
case ConfigDefinitionType.String:
|
|
370
|
-
case ConfigDefinitionType.RegExp:
|
|
371
|
-
case ConfigDefinitionType.Enum:
|
|
372
|
-
return stringValue;
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
// No schema definition - try to infer type from string value
|
|
376
|
-
// This handles skill-defined configs that may not be in schema yet
|
|
377
|
-
if (stringValue === 'true' || stringValue === 'false') {
|
|
378
|
-
return stringValue === 'true';
|
|
379
|
-
}
|
|
380
|
-
if (!isNaN(Number(stringValue)) && stringValue.trim() !== '') {
|
|
381
|
-
return Number(stringValue);
|
|
382
|
-
}
|
|
383
|
-
return stringValue;
|
|
384
|
-
}
|
|
385
|
-
/**
|
|
386
|
-
* Unflatten dotted keys into nested structure
|
|
387
|
-
* Example: { "product.alpha.path": "value" } -> { product: { alpha: { path: "value" } } }
|
|
388
|
-
* Converts string values to appropriate types based on config schema
|
|
389
|
-
*/
|
|
390
|
-
export function unflattenConfig(dotted) {
|
|
391
|
-
const result = {};
|
|
392
|
-
const schema = getConfigSchema();
|
|
393
|
-
for (const [dottedKey, stringValue] of Object.entries(dotted)) {
|
|
394
|
-
const parts = dottedKey.split('.');
|
|
395
|
-
const section = parts[0];
|
|
396
|
-
// Initialize section if needed
|
|
397
|
-
result[section] = result[section] ?? {};
|
|
398
|
-
// Build nested structure for this section
|
|
399
|
-
let current = result[section];
|
|
400
|
-
for (let i = 1; i < parts.length - 1; i++) {
|
|
401
|
-
current[parts[i]] = current[parts[i]] ?? {};
|
|
402
|
-
current = current[parts[i]];
|
|
403
|
-
}
|
|
404
|
-
// Convert string value to appropriate type and set
|
|
405
|
-
const typedValue = parseConfigValue(dottedKey, stringValue, schema);
|
|
406
|
-
current[parts[parts.length - 1]] = typedValue;
|
|
407
|
-
}
|
|
408
|
-
return result;
|
|
409
|
-
}
|