prompt-language-shell 0.4.6 → 0.4.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/dist/config/CONFIG.md +66 -0
- package/dist/config/INTROSPECT.md +29 -14
- package/dist/config/PLAN.md +21 -0
- package/dist/handlers/config.js +42 -1
- package/dist/handlers/execution.js +22 -2
- package/dist/handlers/plan.js +2 -2
- package/dist/services/anthropic.js +41 -12
- package/dist/services/colors.js +5 -4
- package/dist/services/components.js +142 -22
- package/dist/services/configuration.js +80 -0
- package/dist/services/keyboard.js +86 -0
- package/dist/services/messages.js +30 -0
- package/dist/services/tool-registry.js +5 -0
- package/dist/tools/config.tool.js +43 -0
- package/dist/ui/Answer.js +7 -5
- package/dist/ui/Command.js +15 -3
- package/dist/ui/Component.js +1 -1
- package/dist/ui/Config.js +6 -3
- package/dist/ui/Confirm.js +2 -1
- package/dist/ui/Introspect.js +20 -7
- package/dist/ui/Main.js +8 -8
- package/dist/ui/Plan.js +2 -1
- package/dist/ui/Refinement.js +2 -1
- package/dist/ui/Report.js +7 -3
- package/package.json +1 -1
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
## Overview
|
|
2
|
+
|
|
3
|
+
You are the CONFIG tool for "pls" (please), a professional command-line concierge.
|
|
4
|
+
Your role is to determine which configuration settings the user wants to configure
|
|
5
|
+
based on their query.
|
|
6
|
+
|
|
7
|
+
## Input
|
|
8
|
+
|
|
9
|
+
You will receive:
|
|
10
|
+
- `configStructure`: Object mapping config keys to descriptions (e.g., {"anthropic.key": "Anthropic API key"})
|
|
11
|
+
- `query`: User's request (e.g., "app", "mode", "anthropic", or empty)
|
|
12
|
+
|
|
13
|
+
## Task
|
|
14
|
+
|
|
15
|
+
Determine which config keys the user wants to configure and return them as tasks.
|
|
16
|
+
|
|
17
|
+
## Mapping Rules
|
|
18
|
+
|
|
19
|
+
### Query: "app" or empty/unclear
|
|
20
|
+
- Return all **required** config keys (those needed for the app to work)
|
|
21
|
+
- Also include any **optional** config keys that are marked as "(discovered)" (they exist in user's config file)
|
|
22
|
+
- Required keys: `anthropic.key`, `anthropic.model`
|
|
23
|
+
|
|
24
|
+
### Query: "mode"
|
|
25
|
+
- Return only: `settings.debug`
|
|
26
|
+
|
|
27
|
+
### Query: "anthropic"
|
|
28
|
+
- Return all keys starting with `anthropic.` (usually `anthropic.key` and `anthropic.model`)
|
|
29
|
+
|
|
30
|
+
### Other queries
|
|
31
|
+
- Match the query against config key names and descriptions
|
|
32
|
+
- Return keys that seem relevant to the query
|
|
33
|
+
- If unclear, return only required keys
|
|
34
|
+
|
|
35
|
+
## Response Format
|
|
36
|
+
|
|
37
|
+
```json
|
|
38
|
+
{
|
|
39
|
+
"message": "Brief intro message ending with period.",
|
|
40
|
+
"tasks": [
|
|
41
|
+
{
|
|
42
|
+
"action": "Anthropic API key",
|
|
43
|
+
"type": "config",
|
|
44
|
+
"params": {
|
|
45
|
+
"key": "anthropic.key"
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"action": "Model",
|
|
50
|
+
"type": "config",
|
|
51
|
+
"params": {
|
|
52
|
+
"key": "anthropic.model"
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
]
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Important
|
|
60
|
+
|
|
61
|
+
- Use the exact config keys from `configStructure`
|
|
62
|
+
- Use the descriptions from `configStructure` as the action text
|
|
63
|
+
- Always use type "config"
|
|
64
|
+
- Always include the key in params
|
|
65
|
+
- Keep message concise (≤64 characters)
|
|
66
|
+
- Return at least one task (required keys if unsure)
|
|
@@ -53,21 +53,36 @@ Every response MUST include an introductory message before the capability list.
|
|
|
53
53
|
|
|
54
54
|
## Capabilities Structure
|
|
55
55
|
|
|
56
|
-
|
|
56
|
+
**⚠️ CRITICAL ORDERING REQUIREMENT ⚠️**
|
|
57
57
|
|
|
58
|
-
|
|
58
|
+
You MUST present capabilities in the EXACT order specified below. This is
|
|
59
|
+
NON-NEGOTIABLE and applies to EVERY response.
|
|
59
60
|
|
|
60
|
-
|
|
61
|
+
**DO NOT:**
|
|
62
|
+
- Reorder capabilities based on alphabetical sorting
|
|
63
|
+
- Put Plan or Report first (this is WRONG)
|
|
64
|
+
- Rearrange based on perceived importance
|
|
65
|
+
- Deviate from this order for any reason
|
|
61
66
|
|
|
62
|
-
|
|
63
|
-
- **Plan**: Plan and structure tasks from natural language requests, breaking
|
|
64
|
-
them down into clear, actionable steps
|
|
65
|
-
- **Introspect**: List and describe available capabilities and skills
|
|
66
|
-
- **Answer**: Answer questions, explain concepts, provide information
|
|
67
|
-
- **Execute**: Run shell commands, execute programs, process operations
|
|
68
|
-
- **Report**: Generate summaries, create reports, display results
|
|
67
|
+
**CORRECT ORDER - FOLLOW EXACTLY:**
|
|
69
68
|
|
|
70
|
-
###
|
|
69
|
+
### Position 1-4: Built-in Capabilities (Direct User Operations)
|
|
70
|
+
|
|
71
|
+
These MUST appear FIRST, in this EXACT sequence:
|
|
72
|
+
|
|
73
|
+
1. **Introspect** ← ALWAYS FIRST
|
|
74
|
+
2. **Config** ← ALWAYS SECOND
|
|
75
|
+
3. **Answer** ← ALWAYS THIRD
|
|
76
|
+
4. **Execute** ← ALWAYS FOURTH
|
|
77
|
+
|
|
78
|
+
### Position 5-6: Indirect Workflow Capabilities
|
|
79
|
+
|
|
80
|
+
These MUST appear AFTER Execute and BEFORE user skills:
|
|
81
|
+
|
|
82
|
+
5. **Plan** ← NEVER FIRST, ALWAYS position 5 (after Execute)
|
|
83
|
+
6. **Report** ← NEVER FIRST, ALWAYS position 6 (after Plan)
|
|
84
|
+
|
|
85
|
+
### 3. User-Defined Skills
|
|
71
86
|
|
|
72
87
|
If skills are provided in the "Available Skills" section below, include them
|
|
73
88
|
in the response. For each skill:
|
|
@@ -116,9 +131,9 @@ Examples:
|
|
|
116
131
|
### Example 1: List All Capabilities
|
|
117
132
|
|
|
118
133
|
When user asks "list your skills", create an introductory message like "here
|
|
119
|
-
are my capabilities:" followed by
|
|
120
|
-
|
|
121
|
-
"introspect" with an action describing the capability.
|
|
134
|
+
are my capabilities:" followed by tasks for built-in capabilities (Introspect,
|
|
135
|
+
Config, Answer, Execute), then indirect workflow capabilities (Plan, Report).
|
|
136
|
+
Each task uses type "introspect" with an action describing the capability.
|
|
122
137
|
|
|
123
138
|
### Example 2: Filtered Skills
|
|
124
139
|
|
package/dist/config/PLAN.md
CHANGED
|
@@ -422,6 +422,27 @@ When creating task definitions, focus on:
|
|
|
422
422
|
Prioritize clarity and precision over brevity. Each task should be unambiguous
|
|
423
423
|
and executable.
|
|
424
424
|
|
|
425
|
+
## Configuration Requests
|
|
426
|
+
|
|
427
|
+
When the user wants to configure or change settings (e.g., "pls config", "pls configure", "pls change settings", "pls run settings", "pls config anthropic", "pls config mode"), create a SINGLE task with type "config".
|
|
428
|
+
|
|
429
|
+
**Task format:**
|
|
430
|
+
- **action**: "Configure settings" (or similar natural description)
|
|
431
|
+
- **type**: "config"
|
|
432
|
+
- **params**: Include `{ "query": "filter" }` where filter specifies which settings to configure:
|
|
433
|
+
- If command contains specific keywords like "anthropic", "mode", "debug" → use that keyword
|
|
434
|
+
- If command is just "config" or "configure" or "settings" with no specific area → use "app"
|
|
435
|
+
- Extract the relevant context, not the full command
|
|
436
|
+
|
|
437
|
+
**Examples:**
|
|
438
|
+
- User: "pls config anthropic" → `{ "action": "Configure settings", "type": "config", "params": { "query": "anthropic" } }`
|
|
439
|
+
- User: "pls configure" → `{ "action": "Configure settings", "type": "config", "params": { "query": "app" } }`
|
|
440
|
+
- User: "pls run settings" → `{ "action": "Configure settings", "type": "config", "params": { "query": "app" } }`
|
|
441
|
+
- User: "pls config mode" → `{ "action": "Configure settings", "type": "config", "params": { "query": "mode" } }`
|
|
442
|
+
- User: "pls change debug settings" → `{ "action": "Configure settings", "type": "config", "params": { "query": "mode" } }`
|
|
443
|
+
|
|
444
|
+
The CONFIG tool will handle determining which specific config keys to show based on the query.
|
|
445
|
+
|
|
425
446
|
## Multiple Tasks
|
|
426
447
|
|
|
427
448
|
When the user provides multiple tasks separated by commas, semicolons, or the
|
package/dist/handlers/config.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ComponentName, FeedbackType } from '../types/types.js';
|
|
2
2
|
import { createAnthropicService, } from '../services/anthropic.js';
|
|
3
3
|
import { createCommandDefinition, createFeedback, markAsDone, } from '../services/components.js';
|
|
4
|
-
import { saveAnthropicConfig } from '../services/configuration.js';
|
|
4
|
+
import { saveAnthropicConfig, saveConfig } from '../services/configuration.js';
|
|
5
5
|
import { FeedbackMessages } from '../services/messages.js';
|
|
6
6
|
import { exitApp } from '../services/process.js';
|
|
7
7
|
import { withQueueHandler } from '../services/queue.js';
|
|
@@ -37,3 +37,44 @@ export function createConfigAbortedHandler(handleAborted) {
|
|
|
37
37
|
handleAborted('Configuration');
|
|
38
38
|
};
|
|
39
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* Creates config execution finished handler for CONFIG skill
|
|
42
|
+
* Saves arbitrary config keys and exits
|
|
43
|
+
*/
|
|
44
|
+
export function createConfigExecutionFinishedHandler(addToTimeline, keys) {
|
|
45
|
+
return (config) => {
|
|
46
|
+
// Map short keys back to full keys and save
|
|
47
|
+
// Group by section (e.g., "anthropic", "settings")
|
|
48
|
+
const sections = {};
|
|
49
|
+
for (const fullKey of keys) {
|
|
50
|
+
const parts = fullKey.split('.');
|
|
51
|
+
const shortKey = parts[parts.length - 1];
|
|
52
|
+
const section = parts.slice(0, -1).join('.');
|
|
53
|
+
sections[section] = sections[section] ?? {};
|
|
54
|
+
if (shortKey in config) {
|
|
55
|
+
sections[section][shortKey] = config[shortKey];
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// Save each section
|
|
59
|
+
for (const [section, sectionConfig] of Object.entries(sections)) {
|
|
60
|
+
saveConfig(section, sectionConfig);
|
|
61
|
+
}
|
|
62
|
+
return withQueueHandler(ComponentName.Config, (first, rest) => {
|
|
63
|
+
addToTimeline(markAsDone(first), createFeedback(FeedbackType.Succeeded, FeedbackMessages.ConfigurationComplete));
|
|
64
|
+
exitApp(0);
|
|
65
|
+
return rest;
|
|
66
|
+
}, false, 0);
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Creates config execution aborted handler for CONFIG skill
|
|
71
|
+
*/
|
|
72
|
+
export function createConfigExecutionAbortedHandler(addToTimeline) {
|
|
73
|
+
return () => {
|
|
74
|
+
return withQueueHandler(ComponentName.Config, (first, rest) => {
|
|
75
|
+
addToTimeline(markAsDone(first), createFeedback(FeedbackType.Aborted, 'Configuration cancelled.'));
|
|
76
|
+
exitApp(0);
|
|
77
|
+
return rest;
|
|
78
|
+
}, false, 0);
|
|
79
|
+
};
|
|
80
|
+
}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { ComponentName, FeedbackType, TaskType } from '../types/types.js';
|
|
2
|
-
import { createAnswerDefinition, createFeedback, createIntrospectDefinition, markAsDone, } from '../services/components.js';
|
|
2
|
+
import { createAnswerDefinition, createConfigDefinitionWithKeys, createFeedback, createIntrospectDefinition, markAsDone, } from '../services/components.js';
|
|
3
|
+
import { createConfigExecutionAbortedHandler, createConfigExecutionFinishedHandler, } from './config.js';
|
|
3
4
|
import { getCancellationMessage } from '../services/messages.js';
|
|
4
5
|
import { exitApp } from '../services/process.js';
|
|
5
6
|
import { withQueueHandler } from '../services/queue.js';
|
|
6
7
|
/**
|
|
7
8
|
* Creates execution confirmed handler
|
|
8
9
|
*/
|
|
9
|
-
export function createExecutionConfirmedHandler(timelineRef, addToTimeline, service, handleIntrospectError, handleIntrospectComplete, handleIntrospectAborted, handleAnswerError, handleAnswerComplete, handleAnswerAborted) {
|
|
10
|
+
export function createExecutionConfirmedHandler(timelineRef, addToTimeline, service, handleIntrospectError, handleIntrospectComplete, handleIntrospectAborted, handleAnswerError, handleAnswerComplete, handleAnswerAborted, setQueue) {
|
|
10
11
|
return () => withQueueHandler(ComponentName.Confirm, (first) => {
|
|
11
12
|
// Find the most recent Plan in timeline to get tasks
|
|
12
13
|
const currentTimeline = timelineRef.current;
|
|
@@ -22,6 +23,7 @@ export function createExecutionConfirmedHandler(timelineRef, addToTimeline, serv
|
|
|
22
23
|
: [];
|
|
23
24
|
const allIntrospect = tasks.every((task) => task.type === TaskType.Introspect);
|
|
24
25
|
const allAnswer = tasks.every((task) => task.type === TaskType.Answer);
|
|
26
|
+
const allConfig = tasks.every((task) => task.type === TaskType.Config);
|
|
25
27
|
if (allIntrospect && tasks.length > 0) {
|
|
26
28
|
// Execute introspection
|
|
27
29
|
addToTimeline(markAsDone(first));
|
|
@@ -37,6 +39,24 @@ export function createExecutionConfirmedHandler(timelineRef, addToTimeline, serv
|
|
|
37
39
|
createAnswerDefinition(question, service, handleAnswerError, handleAnswerComplete, handleAnswerAborted),
|
|
38
40
|
];
|
|
39
41
|
}
|
|
42
|
+
else if (allConfig && tasks.length > 0) {
|
|
43
|
+
// Execute config - extract keys from task params
|
|
44
|
+
const keys = tasks
|
|
45
|
+
.map((task) => task.params?.key)
|
|
46
|
+
.filter((key) => typeof key === 'string');
|
|
47
|
+
addToTimeline(markAsDone(first));
|
|
48
|
+
// Create handlers with keys for proper saving
|
|
49
|
+
// Wrap in setQueue to properly update queue when Config finishes
|
|
50
|
+
const handleConfigFinished = (config) => {
|
|
51
|
+
setQueue(createConfigExecutionFinishedHandler(addToTimeline, keys)(config));
|
|
52
|
+
};
|
|
53
|
+
const handleConfigAborted = () => {
|
|
54
|
+
setQueue(createConfigExecutionAbortedHandler(addToTimeline)());
|
|
55
|
+
};
|
|
56
|
+
return [
|
|
57
|
+
createConfigDefinitionWithKeys(keys, handleConfigFinished, handleConfigAborted),
|
|
58
|
+
];
|
|
59
|
+
}
|
|
40
60
|
else {
|
|
41
61
|
// Regular execution - just exit for now
|
|
42
62
|
addToTimeline(markAsDone(first));
|
package/dist/handlers/plan.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ComponentName, FeedbackType, TaskType } from '../types/types.js';
|
|
2
2
|
import { createConfirmDefinition, createFeedback, createPlanDefinition, markAsDone, createRefinement, } from '../services/components.js';
|
|
3
|
-
import { FeedbackMessages, getRefiningMessage } from '../services/messages.js';
|
|
3
|
+
import { FeedbackMessages, formatErrorMessage, getRefiningMessage, } from '../services/messages.js';
|
|
4
4
|
import { exitApp } from '../services/process.js';
|
|
5
5
|
/**
|
|
6
6
|
* Creates plan aborted handler
|
|
@@ -66,7 +66,7 @@ export function createPlanSelectionConfirmedHandler(addToTimeline, service, hand
|
|
|
66
66
|
setQueue([confirmDefinition]);
|
|
67
67
|
}
|
|
68
68
|
catch (error) {
|
|
69
|
-
const errorMessage = error
|
|
69
|
+
const errorMessage = formatErrorMessage(error);
|
|
70
70
|
// Mark refinement as done and move to timeline before showing error
|
|
71
71
|
setQueue((currentQueue) => {
|
|
72
72
|
if (currentQueue.length > 0 &&
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import Anthropic from '@anthropic-ai/sdk';
|
|
2
|
+
import { getAvailableConfigStructure, } from './configuration.js';
|
|
2
3
|
import { formatSkillsForPrompt, loadSkills } from './skills.js';
|
|
3
4
|
import { toolRegistry } from './tool-registry.js';
|
|
4
5
|
export class AnthropicService {
|
|
@@ -12,16 +13,36 @@ export class AnthropicService {
|
|
|
12
13
|
// Load tool from registry
|
|
13
14
|
const tool = toolRegistry.getSchema(toolName);
|
|
14
15
|
const instructions = toolRegistry.getInstructions(toolName);
|
|
15
|
-
//
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
// Build system prompt with additional context based on tool
|
|
17
|
+
let systemPrompt = instructions;
|
|
18
|
+
// Add skills section for applicable tools
|
|
19
|
+
if (toolName === 'plan' || toolName === 'introspect') {
|
|
20
|
+
const skills = loadSkills();
|
|
21
|
+
const skillsSection = formatSkillsForPrompt(skills);
|
|
22
|
+
systemPrompt += skillsSection;
|
|
23
|
+
}
|
|
24
|
+
// Add config structure for config tool only
|
|
25
|
+
if (toolName === 'config') {
|
|
26
|
+
const configStructure = getAvailableConfigStructure();
|
|
27
|
+
const configSection = '\n\n## Available Configuration\n\n' +
|
|
28
|
+
'Config structure (key: description):\n' +
|
|
29
|
+
JSON.stringify(configStructure, null, 2);
|
|
30
|
+
systemPrompt += configSection;
|
|
31
|
+
}
|
|
32
|
+
// Build tools array - add web search for answer tool
|
|
33
|
+
const tools = [tool];
|
|
34
|
+
if (toolName === 'answer') {
|
|
35
|
+
tools.push({
|
|
36
|
+
type: 'web_search_20250305',
|
|
37
|
+
name: 'web_search',
|
|
38
|
+
});
|
|
39
|
+
}
|
|
19
40
|
// Call API with tool
|
|
20
41
|
const response = await this.client.messages.create({
|
|
21
42
|
model: this.model,
|
|
22
43
|
max_tokens: 1024,
|
|
23
44
|
system: systemPrompt,
|
|
24
|
-
tools
|
|
45
|
+
tools,
|
|
25
46
|
tool_choice: { type: 'any' },
|
|
26
47
|
messages: [
|
|
27
48
|
{
|
|
@@ -34,15 +55,25 @@ export class AnthropicService {
|
|
|
34
55
|
if (response.stop_reason === 'max_tokens') {
|
|
35
56
|
throw new Error('Response was truncated due to length. Please simplify your request or break it into smaller parts.');
|
|
36
57
|
}
|
|
37
|
-
//
|
|
38
|
-
|
|
39
|
-
|
|
58
|
+
// Find tool_use block in response (may not be first with web search)
|
|
59
|
+
const toolUseContent = response.content.find((block) => block.type === 'tool_use');
|
|
60
|
+
// For answer tool with web search, model might return text directly
|
|
61
|
+
if (toolName === 'answer' && !toolUseContent) {
|
|
62
|
+
const textContent = response.content.find((block) => block.type === 'text');
|
|
63
|
+
if (textContent) {
|
|
64
|
+
return {
|
|
65
|
+
message: '',
|
|
66
|
+
tasks: [],
|
|
67
|
+
answer: textContent.text,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (!toolUseContent) {
|
|
40
72
|
throw new Error('Expected tool_use response from Claude API');
|
|
41
73
|
}
|
|
42
|
-
const content =
|
|
74
|
+
const content = toolUseContent;
|
|
43
75
|
// Extract and validate response based on tool type
|
|
44
76
|
const input = content.input;
|
|
45
|
-
const isDebug = process.env.DEBUG === 'true';
|
|
46
77
|
// Handle answer tool response
|
|
47
78
|
if (toolName === 'answer') {
|
|
48
79
|
if (!input.question || typeof input.question !== 'string') {
|
|
@@ -55,7 +86,6 @@ export class AnthropicService {
|
|
|
55
86
|
message: '',
|
|
56
87
|
tasks: [],
|
|
57
88
|
answer: input.answer,
|
|
58
|
-
systemPrompt: isDebug ? systemPrompt : undefined,
|
|
59
89
|
};
|
|
60
90
|
}
|
|
61
91
|
// Handle plan and introspect tool responses
|
|
@@ -74,7 +104,6 @@ export class AnthropicService {
|
|
|
74
104
|
return {
|
|
75
105
|
message: input.message,
|
|
76
106
|
tasks: input.tasks,
|
|
77
|
-
systemPrompt: isDebug ? systemPrompt : undefined,
|
|
78
107
|
};
|
|
79
108
|
}
|
|
80
109
|
}
|
package/dist/services/colors.js
CHANGED
|
@@ -69,12 +69,13 @@ export const Colors = {
|
|
|
69
69
|
Origin: {
|
|
70
70
|
BuiltIn: Palette.Cyan,
|
|
71
71
|
UserProvided: Palette.Green,
|
|
72
|
+
Indirect: Palette.Purple,
|
|
72
73
|
},
|
|
73
74
|
};
|
|
74
75
|
/**
|
|
75
76
|
* Task-specific color mappings (internal)
|
|
76
77
|
*/
|
|
77
|
-
const
|
|
78
|
+
const taskColors = {
|
|
78
79
|
[TaskType.Config]: {
|
|
79
80
|
description: Colors.Label.Default,
|
|
80
81
|
type: Colors.Type.Config,
|
|
@@ -119,7 +120,7 @@ const TaskColors = {
|
|
|
119
120
|
/**
|
|
120
121
|
* Feedback-specific color mappings (internal)
|
|
121
122
|
*/
|
|
122
|
-
const
|
|
123
|
+
const feedbackColors = {
|
|
123
124
|
[FeedbackType.Info]: Colors.Status.Info,
|
|
124
125
|
[FeedbackType.Succeeded]: Colors.Status.Success,
|
|
125
126
|
[FeedbackType.Aborted]: Colors.Status.Warning,
|
|
@@ -147,7 +148,7 @@ function processColor(color, isCurrent) {
|
|
|
147
148
|
* - Colors.Text.Active for current items
|
|
148
149
|
*/
|
|
149
150
|
export function getTaskColors(type, isCurrent) {
|
|
150
|
-
const colors =
|
|
151
|
+
const colors = taskColors[type];
|
|
151
152
|
return {
|
|
152
153
|
description: processColor(colors.description, isCurrent),
|
|
153
154
|
type: processColor(colors.type, isCurrent),
|
|
@@ -161,7 +162,7 @@ export function getTaskColors(type, isCurrent) {
|
|
|
161
162
|
* - Colors.Text.Active for current items
|
|
162
163
|
*/
|
|
163
164
|
export function getFeedbackColor(type, isCurrent) {
|
|
164
|
-
return processColor(
|
|
165
|
+
return processColor(feedbackColors[type], isCurrent);
|
|
165
166
|
}
|
|
166
167
|
/**
|
|
167
168
|
* Get text color based on current/historical state.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { ComponentName } from '../types/types.js';
|
|
3
|
-
import {
|
|
3
|
+
import { getConfigSchema, loadConfig, } from './configuration.js';
|
|
4
4
|
import { getConfirmationMessage } from './messages.js';
|
|
5
5
|
import { StepType } from '../ui/Config.js';
|
|
6
6
|
export function markAsDone(component) {
|
|
@@ -14,27 +14,132 @@ export function createWelcomeDefinition(app) {
|
|
|
14
14
|
};
|
|
15
15
|
}
|
|
16
16
|
export function createConfigSteps() {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
17
|
+
// Use schema-based config step generation for required Anthropic settings
|
|
18
|
+
return createConfigStepsFromSchema(['anthropic.key', 'anthropic.model']);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Get current config value for a dotted key path
|
|
22
|
+
*/
|
|
23
|
+
function getConfigValue(config, key) {
|
|
24
|
+
if (!config)
|
|
25
|
+
return undefined;
|
|
26
|
+
const parts = key.split('.');
|
|
27
|
+
let value = config;
|
|
28
|
+
for (const part of parts) {
|
|
29
|
+
if (value && typeof value === 'object' && part in value) {
|
|
30
|
+
value = value[part];
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Get validation function for a config definition
|
|
40
|
+
*/
|
|
41
|
+
function getValidator(definition) {
|
|
42
|
+
switch (definition.type) {
|
|
43
|
+
case 'regexp':
|
|
44
|
+
return (value) => definition.pattern.test(value);
|
|
45
|
+
case 'string':
|
|
46
|
+
return () => true; // Strings are always valid
|
|
47
|
+
case 'enum':
|
|
48
|
+
return (value) => definition.values.includes(value);
|
|
49
|
+
case 'number':
|
|
50
|
+
return (value) => !isNaN(Number(value));
|
|
51
|
+
case 'boolean':
|
|
52
|
+
return (value) => value === 'true' || value === 'false';
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Create config steps from schema for specified keys
|
|
57
|
+
*/
|
|
58
|
+
export function createConfigStepsFromSchema(keys) {
|
|
59
|
+
const schema = getConfigSchema();
|
|
60
|
+
let currentConfig = null;
|
|
61
|
+
try {
|
|
62
|
+
currentConfig = loadConfig();
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
// Config doesn't exist yet, use defaults
|
|
66
|
+
}
|
|
67
|
+
return keys.map((key) => {
|
|
68
|
+
if (!(key in schema)) {
|
|
69
|
+
throw new Error(`Unknown config key: ${key}`);
|
|
70
|
+
}
|
|
71
|
+
const definition = schema[key];
|
|
72
|
+
const currentValue = getConfigValue(currentConfig, key);
|
|
73
|
+
const keyParts = key.split('.');
|
|
74
|
+
const shortKey = keyParts[keyParts.length - 1];
|
|
75
|
+
// Map definition to ConfigStep based on type
|
|
76
|
+
switch (definition.type) {
|
|
77
|
+
case 'regexp':
|
|
78
|
+
case 'string': {
|
|
79
|
+
const value = currentValue !== undefined && typeof currentValue === 'string'
|
|
80
|
+
? currentValue
|
|
81
|
+
: definition.type === 'string'
|
|
82
|
+
? (definition.default ?? '')
|
|
83
|
+
: null;
|
|
84
|
+
return {
|
|
85
|
+
description: definition.description,
|
|
86
|
+
key: shortKey,
|
|
87
|
+
type: StepType.Text,
|
|
88
|
+
value,
|
|
89
|
+
validate: getValidator(definition),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
case 'number': {
|
|
93
|
+
const value = currentValue !== undefined && typeof currentValue === 'number'
|
|
94
|
+
? String(currentValue)
|
|
95
|
+
: definition.default !== undefined
|
|
96
|
+
? String(definition.default)
|
|
97
|
+
: '0';
|
|
98
|
+
return {
|
|
99
|
+
description: definition.description,
|
|
100
|
+
key: shortKey,
|
|
101
|
+
type: StepType.Text,
|
|
102
|
+
value,
|
|
103
|
+
validate: getValidator(definition),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
case 'enum': {
|
|
107
|
+
const currentStr = currentValue !== undefined && typeof currentValue === 'string'
|
|
108
|
+
? currentValue
|
|
109
|
+
: definition.default;
|
|
110
|
+
const defaultIndex = currentStr
|
|
111
|
+
? definition.values.indexOf(currentStr)
|
|
112
|
+
: 0;
|
|
113
|
+
return {
|
|
114
|
+
description: definition.description,
|
|
115
|
+
key: shortKey,
|
|
116
|
+
type: StepType.Selection,
|
|
117
|
+
options: definition.values.map((value) => ({
|
|
118
|
+
label: value,
|
|
119
|
+
value,
|
|
120
|
+
})),
|
|
121
|
+
defaultIndex: Math.max(0, defaultIndex),
|
|
122
|
+
validate: getValidator(definition),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
case 'boolean': {
|
|
126
|
+
const currentBool = currentValue !== undefined && typeof currentValue === 'boolean'
|
|
127
|
+
? currentValue
|
|
128
|
+
: undefined;
|
|
129
|
+
return {
|
|
130
|
+
description: definition.description,
|
|
131
|
+
key: shortKey,
|
|
132
|
+
type: StepType.Selection,
|
|
133
|
+
options: [
|
|
134
|
+
{ label: 'Yes', value: 'true' },
|
|
135
|
+
{ label: 'No', value: 'false' },
|
|
136
|
+
],
|
|
137
|
+
defaultIndex: currentBool !== undefined ? (currentBool ? 0 : 1) : 0,
|
|
138
|
+
validate: getValidator(definition),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
});
|
|
38
143
|
}
|
|
39
144
|
export function createConfigDefinition(onFinished, onAborted) {
|
|
40
145
|
return {
|
|
@@ -48,6 +153,21 @@ export function createConfigDefinition(onFinished, onAborted) {
|
|
|
48
153
|
},
|
|
49
154
|
};
|
|
50
155
|
}
|
|
156
|
+
/**
|
|
157
|
+
* Create config definition with specific keys
|
|
158
|
+
*/
|
|
159
|
+
export function createConfigDefinitionWithKeys(keys, onFinished, onAborted) {
|
|
160
|
+
return {
|
|
161
|
+
id: randomUUID(),
|
|
162
|
+
name: ComponentName.Config,
|
|
163
|
+
state: { done: false },
|
|
164
|
+
props: {
|
|
165
|
+
steps: createConfigStepsFromSchema(keys),
|
|
166
|
+
onFinished,
|
|
167
|
+
onAborted,
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
}
|
|
51
171
|
export function createCommandDefinition(command, service, onError, onComplete, onAborted) {
|
|
52
172
|
return {
|
|
53
173
|
id: randomUUID(),
|
|
@@ -166,3 +166,83 @@ export function getConfigurationRequiredMessage(forFutureUse = false) {
|
|
|
166
166
|
];
|
|
167
167
|
return messages[Math.floor(Math.random() * messages.length)];
|
|
168
168
|
}
|
|
169
|
+
/**
|
|
170
|
+
* Core configuration schema - defines structure and types for built-in settings
|
|
171
|
+
*/
|
|
172
|
+
const coreConfigSchema = {
|
|
173
|
+
'anthropic.key': {
|
|
174
|
+
type: 'regexp',
|
|
175
|
+
required: true,
|
|
176
|
+
pattern: /^sk-ant-api03-[A-Za-z0-9_-]{95}$/,
|
|
177
|
+
description: 'Anthropic API key',
|
|
178
|
+
},
|
|
179
|
+
'anthropic.model': {
|
|
180
|
+
type: 'enum',
|
|
181
|
+
required: true,
|
|
182
|
+
values: SUPPORTED_MODELS,
|
|
183
|
+
default: AnthropicModel.Haiku,
|
|
184
|
+
description: 'Anthropic model',
|
|
185
|
+
},
|
|
186
|
+
'settings.debug': {
|
|
187
|
+
type: 'boolean',
|
|
188
|
+
required: false,
|
|
189
|
+
description: 'Debug mode',
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
/**
|
|
193
|
+
* Get complete configuration schema
|
|
194
|
+
* Currently returns core schema only
|
|
195
|
+
* Future: will merge with skill-declared schemas
|
|
196
|
+
*/
|
|
197
|
+
export function getConfigSchema() {
|
|
198
|
+
return {
|
|
199
|
+
...coreConfigSchema,
|
|
200
|
+
// Future: ...loadSkillSchemas()
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Get available config structure for CONFIG tool
|
|
205
|
+
* Returns keys with descriptions only (no values for privacy)
|
|
206
|
+
*/
|
|
207
|
+
export function getAvailableConfigStructure() {
|
|
208
|
+
const schema = getConfigSchema();
|
|
209
|
+
const structure = {};
|
|
210
|
+
// Add core schema keys with descriptions
|
|
211
|
+
for (const [key, definition] of Object.entries(schema)) {
|
|
212
|
+
structure[key] = definition.description;
|
|
213
|
+
}
|
|
214
|
+
// Add discovered keys from config file (if it exists)
|
|
215
|
+
try {
|
|
216
|
+
const configFile = getConfigFile();
|
|
217
|
+
if (!existsSync(configFile)) {
|
|
218
|
+
return structure;
|
|
219
|
+
}
|
|
220
|
+
const content = readFileSync(configFile, 'utf-8');
|
|
221
|
+
const parsed = YAML.parse(content);
|
|
222
|
+
// Flatten nested config to dot notation
|
|
223
|
+
function flattenConfig(obj, prefix = '') {
|
|
224
|
+
const result = {};
|
|
225
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
226
|
+
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
227
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
228
|
+
Object.assign(result, flattenConfig(value, fullKey));
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
result[fullKey] = value;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return result;
|
|
235
|
+
}
|
|
236
|
+
const flatConfig = flattenConfig(parsed);
|
|
237
|
+
// Add discovered keys that aren't in schema
|
|
238
|
+
for (const key of Object.keys(flatConfig)) {
|
|
239
|
+
if (!structure[key]) {
|
|
240
|
+
structure[key] = `${key} (discovered)`;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
catch {
|
|
245
|
+
// Config file doesn't exist or can't be read, only use schema
|
|
246
|
+
}
|
|
247
|
+
return structure;
|
|
248
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { useInput as useInkInput } from 'ink';
|
|
2
|
+
const globalShortcuts = new Map();
|
|
3
|
+
/**
|
|
4
|
+
* Converts a Key object to a normalized pattern string
|
|
5
|
+
* Example: { shift: true, tab: true } → "shift+tab"
|
|
6
|
+
*/
|
|
7
|
+
function keyToPattern(key) {
|
|
8
|
+
const modifiers = [];
|
|
9
|
+
if (key.ctrl)
|
|
10
|
+
modifiers.push('ctrl');
|
|
11
|
+
if (key.meta)
|
|
12
|
+
modifiers.push('meta');
|
|
13
|
+
if (key.shift)
|
|
14
|
+
modifiers.push('shift');
|
|
15
|
+
// Get the key name
|
|
16
|
+
let keyName = '';
|
|
17
|
+
if (key.escape)
|
|
18
|
+
keyName = 'escape';
|
|
19
|
+
else if (key.tab)
|
|
20
|
+
keyName = 'tab';
|
|
21
|
+
else if (key.return)
|
|
22
|
+
keyName = 'return';
|
|
23
|
+
else if (key.upArrow)
|
|
24
|
+
keyName = 'up';
|
|
25
|
+
else if (key.downArrow)
|
|
26
|
+
keyName = 'down';
|
|
27
|
+
else if (key.leftArrow)
|
|
28
|
+
keyName = 'left';
|
|
29
|
+
else if (key.rightArrow)
|
|
30
|
+
keyName = 'right';
|
|
31
|
+
else if (key.backspace)
|
|
32
|
+
keyName = 'backspace';
|
|
33
|
+
else if (key.delete)
|
|
34
|
+
keyName = 'delete';
|
|
35
|
+
if (!keyName)
|
|
36
|
+
return null;
|
|
37
|
+
return [...modifiers, keyName].join('+');
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Register a global keyboard shortcut
|
|
41
|
+
* @param pattern Pattern string like "shift+tab" or "ctrl+c"
|
|
42
|
+
* @param handler Function to call when shortcut is triggered
|
|
43
|
+
*/
|
|
44
|
+
export function registerGlobalShortcut(pattern, handler) {
|
|
45
|
+
globalShortcuts.set(pattern.toLowerCase(), handler);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Check if a key event matches a global shortcut
|
|
49
|
+
* If matched, calls the handler and returns true
|
|
50
|
+
* If not matched, returns false
|
|
51
|
+
* @param key The key object from ink's useInput
|
|
52
|
+
* @returns true if global shortcut was handled, false otherwise
|
|
53
|
+
*/
|
|
54
|
+
export function isGlobalShortcut(key) {
|
|
55
|
+
const pattern = keyToPattern(key);
|
|
56
|
+
if (!pattern)
|
|
57
|
+
return false;
|
|
58
|
+
const handler = globalShortcuts.get(pattern.toLowerCase());
|
|
59
|
+
if (handler) {
|
|
60
|
+
handler();
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Clear all registered global shortcuts (useful for testing)
|
|
67
|
+
*/
|
|
68
|
+
export function clearGlobalShortcuts() {
|
|
69
|
+
globalShortcuts.clear();
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Custom useInput hook that automatically handles global shortcuts
|
|
73
|
+
* before passing events to the component handler
|
|
74
|
+
* @param handler Component's keyboard event handler
|
|
75
|
+
* @param options Options for useInput (isActive, etc.)
|
|
76
|
+
*/
|
|
77
|
+
export function useInput(handler, options) {
|
|
78
|
+
useInkInput((input, key) => {
|
|
79
|
+
// Check for global shortcuts first
|
|
80
|
+
if (isGlobalShortcut(key)) {
|
|
81
|
+
return; // Global shortcut handled, don't propagate to component
|
|
82
|
+
}
|
|
83
|
+
// No global shortcut matched, call component handler
|
|
84
|
+
handler(input, key);
|
|
85
|
+
}, options);
|
|
86
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { loadDebugSetting } from './configuration.js';
|
|
1
2
|
/**
|
|
2
3
|
* Returns a natural language confirmation message for plan execution.
|
|
3
4
|
* Randomly selects from variations to sound less robotic.
|
|
@@ -48,3 +49,32 @@ export const FeedbackMessages = {
|
|
|
48
49
|
ConfigurationComplete: 'Configuration complete.',
|
|
49
50
|
UnexpectedError: 'Unexpected error occurred:',
|
|
50
51
|
};
|
|
52
|
+
/**
|
|
53
|
+
* Extracts a user-friendly error message from API errors.
|
|
54
|
+
* In debug mode, returns the full error; otherwise, returns just the message.
|
|
55
|
+
*
|
|
56
|
+
* Handles Anthropic API error format:
|
|
57
|
+
* 400 {"type":"error","error":{"type":"...","message":"..."},"request_id":"..."}
|
|
58
|
+
*/
|
|
59
|
+
export function formatErrorMessage(error) {
|
|
60
|
+
const rawMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
61
|
+
if (loadDebugSetting()) {
|
|
62
|
+
return rawMessage;
|
|
63
|
+
}
|
|
64
|
+
// Try to extract message from Anthropic API error format
|
|
65
|
+
// Format: "400 {json...}" or just "{json...}"
|
|
66
|
+
const jsonMatch = rawMessage.match(/\{.*\}/s);
|
|
67
|
+
if (jsonMatch) {
|
|
68
|
+
try {
|
|
69
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
70
|
+
const message = parsed.error?.message ?? parsed.message;
|
|
71
|
+
if (message) {
|
|
72
|
+
return message;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
// JSON parsing failed, return original message
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return rawMessage;
|
|
80
|
+
}
|
|
@@ -34,6 +34,7 @@ class ToolRegistry {
|
|
|
34
34
|
export const toolRegistry = new ToolRegistry();
|
|
35
35
|
// Register built-in tools
|
|
36
36
|
import { answerTool } from '../tools/answer.tool.js';
|
|
37
|
+
import { configTool } from '../tools/config.tool.js';
|
|
37
38
|
import { introspectTool } from '../tools/introspect.tool.js';
|
|
38
39
|
import { planTool } from '../tools/plan.tool.js';
|
|
39
40
|
toolRegistry.register('plan', {
|
|
@@ -48,3 +49,7 @@ toolRegistry.register('answer', {
|
|
|
48
49
|
schema: answerTool,
|
|
49
50
|
instructionsPath: 'config/ANSWER.md',
|
|
50
51
|
});
|
|
52
|
+
toolRegistry.register('config', {
|
|
53
|
+
schema: configTool,
|
|
54
|
+
instructionsPath: 'config/CONFIG.md',
|
|
55
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export const configTool = {
|
|
2
|
+
name: 'config',
|
|
3
|
+
description: 'Determine which configuration settings to show based on user query. Receives available config keys with descriptions and returns which keys the user wants to configure.',
|
|
4
|
+
input_schema: {
|
|
5
|
+
type: 'object',
|
|
6
|
+
properties: {
|
|
7
|
+
message: {
|
|
8
|
+
type: 'string',
|
|
9
|
+
description: 'Brief message to display before config plan. Single sentence, maximum 64 characters. End with period.',
|
|
10
|
+
},
|
|
11
|
+
tasks: {
|
|
12
|
+
type: 'array',
|
|
13
|
+
description: 'Array of config settings to configure. Each task has type "config" and params with the config key.',
|
|
14
|
+
items: {
|
|
15
|
+
type: 'object',
|
|
16
|
+
properties: {
|
|
17
|
+
action: {
|
|
18
|
+
type: 'string',
|
|
19
|
+
description: 'Description of the config setting (from the provided descriptions). Maximum 64 characters.',
|
|
20
|
+
},
|
|
21
|
+
type: {
|
|
22
|
+
type: 'string',
|
|
23
|
+
description: 'Always "config" for configuration tasks.',
|
|
24
|
+
},
|
|
25
|
+
params: {
|
|
26
|
+
type: 'object',
|
|
27
|
+
description: 'Parameters for the config task.',
|
|
28
|
+
properties: {
|
|
29
|
+
key: {
|
|
30
|
+
type: 'string',
|
|
31
|
+
description: 'The config key to configure (e.g., "anthropic.key", "settings.debug").',
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
required: ['key'],
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
required: ['action', 'type', 'params'],
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
required: ['message', 'tasks'],
|
|
42
|
+
},
|
|
43
|
+
};
|
package/dist/ui/Answer.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useState } from 'react';
|
|
3
|
-
import { Box, Text
|
|
3
|
+
import { Box, Text } from 'ink';
|
|
4
4
|
import { Colors, getTextColor } from '../services/colors.js';
|
|
5
|
+
import { useInput } from '../services/keyboard.js';
|
|
6
|
+
import { formatErrorMessage } from '../services/messages.js';
|
|
5
7
|
import { Spinner } from './Spinner.js';
|
|
6
|
-
const
|
|
8
|
+
const MINIMUM_PROCESSING_TIME = 400;
|
|
7
9
|
export function Answer({ question, state, service, onError, onComplete, onAborted, }) {
|
|
8
10
|
const done = state?.done ?? false;
|
|
9
11
|
const isCurrent = done === false;
|
|
@@ -33,7 +35,7 @@ export function Answer({ question, state, service, onError, onComplete, onAborte
|
|
|
33
35
|
// Call answer tool
|
|
34
36
|
const result = await svc.processWithTool(question, 'answer');
|
|
35
37
|
const elapsed = Date.now() - startTime;
|
|
36
|
-
const remainingTime = Math.max(0,
|
|
38
|
+
const remainingTime = Math.max(0, MINIMUM_PROCESSING_TIME - elapsed);
|
|
37
39
|
await new Promise((resolve) => setTimeout(resolve, remainingTime));
|
|
38
40
|
if (mounted) {
|
|
39
41
|
// Extract answer from result
|
|
@@ -44,10 +46,10 @@ export function Answer({ question, state, service, onError, onComplete, onAborte
|
|
|
44
46
|
}
|
|
45
47
|
catch (err) {
|
|
46
48
|
const elapsed = Date.now() - startTime;
|
|
47
|
-
const remainingTime = Math.max(0,
|
|
49
|
+
const remainingTime = Math.max(0, MINIMUM_PROCESSING_TIME - elapsed);
|
|
48
50
|
await new Promise((resolve) => setTimeout(resolve, remainingTime));
|
|
49
51
|
if (mounted) {
|
|
50
|
-
const errorMessage = err
|
|
52
|
+
const errorMessage = formatErrorMessage(err);
|
|
51
53
|
setIsLoading(false);
|
|
52
54
|
if (onError) {
|
|
53
55
|
onError(errorMessage);
|
package/dist/ui/Command.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useState } from 'react';
|
|
3
|
-
import { Box, Text
|
|
3
|
+
import { Box, Text } from 'ink';
|
|
4
|
+
import { TaskType } from '../types/types.js';
|
|
4
5
|
import { Colors } from '../services/colors.js';
|
|
6
|
+
import { useInput } from '../services/keyboard.js';
|
|
7
|
+
import { formatErrorMessage } from '../services/messages.js';
|
|
5
8
|
import { Spinner } from './Spinner.js';
|
|
6
9
|
const MIN_PROCESSING_TIME = 1000; // purely for visual effect
|
|
7
10
|
export function Command({ command, state, service, children, onError, onComplete, onAborted, }) {
|
|
@@ -29,7 +32,16 @@ export function Command({ command, state, service, children, onError, onComplete
|
|
|
29
32
|
async function process(svc) {
|
|
30
33
|
const startTime = Date.now();
|
|
31
34
|
try {
|
|
32
|
-
|
|
35
|
+
let result = await svc.processWithTool(command, 'plan');
|
|
36
|
+
// If all tasks are config type, delegate to CONFIG tool
|
|
37
|
+
const allConfig = result.tasks.length > 0 &&
|
|
38
|
+
result.tasks.every((task) => task.type === TaskType.Config);
|
|
39
|
+
if (allConfig) {
|
|
40
|
+
// Extract query from first config task params, default to 'app'
|
|
41
|
+
const query = result.tasks[0].params?.query || 'app';
|
|
42
|
+
// Call CONFIG tool to get specific config keys
|
|
43
|
+
result = await svc.processWithTool(query, 'config');
|
|
44
|
+
}
|
|
33
45
|
const elapsed = Date.now() - startTime;
|
|
34
46
|
const remainingTime = Math.max(0, MIN_PROCESSING_TIME - elapsed);
|
|
35
47
|
await new Promise((resolve) => setTimeout(resolve, remainingTime));
|
|
@@ -43,7 +55,7 @@ export function Command({ command, state, service, children, onError, onComplete
|
|
|
43
55
|
const remainingTime = Math.max(0, MIN_PROCESSING_TIME - elapsed);
|
|
44
56
|
await new Promise((resolve) => setTimeout(resolve, remainingTime));
|
|
45
57
|
if (mounted) {
|
|
46
|
-
const errorMessage = err
|
|
58
|
+
const errorMessage = formatErrorMessage(err);
|
|
47
59
|
setIsLoading(false);
|
|
48
60
|
if (onError) {
|
|
49
61
|
onError(errorMessage);
|
package/dist/ui/Component.js
CHANGED
|
@@ -49,7 +49,7 @@ export const Component = React.memo(function Component({ def, debug, }) {
|
|
|
49
49
|
case ComponentName.Introspect: {
|
|
50
50
|
const props = def.props;
|
|
51
51
|
const state = def.state;
|
|
52
|
-
return _jsx(Introspect, { ...props, state: state });
|
|
52
|
+
return _jsx(Introspect, { ...props, state: state, debug: debug });
|
|
53
53
|
}
|
|
54
54
|
case ComponentName.Report:
|
|
55
55
|
return _jsx(Report, { ...def.props });
|
package/dist/ui/Config.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import React from 'react';
|
|
3
|
-
import { Box, Text, useFocus
|
|
3
|
+
import { Box, Text, useFocus } from 'ink';
|
|
4
4
|
import TextInput from 'ink-text-input';
|
|
5
5
|
import { Colors } from '../services/colors.js';
|
|
6
|
+
import { useInput } from '../services/keyboard.js';
|
|
6
7
|
export var StepType;
|
|
7
8
|
(function (StepType) {
|
|
8
9
|
StepType["Text"] = "text";
|
|
@@ -20,11 +21,13 @@ function TextStep({ value, placeholder, validate, onChange, onSubmit, }) {
|
|
|
20
21
|
}
|
|
21
22
|
};
|
|
22
23
|
const handleSubmit = (value) => {
|
|
23
|
-
if
|
|
24
|
+
// Use placeholder if input is empty
|
|
25
|
+
const finalValue = value || placeholder || '';
|
|
26
|
+
if (!validate(finalValue)) {
|
|
24
27
|
setValidationFailed(true);
|
|
25
28
|
return;
|
|
26
29
|
}
|
|
27
|
-
onSubmit(
|
|
30
|
+
onSubmit(finalValue);
|
|
28
31
|
};
|
|
29
32
|
// Handle input manually when validation fails
|
|
30
33
|
useInput((input, key) => {
|
package/dist/ui/Confirm.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import React from 'react';
|
|
3
|
-
import { Box, Text
|
|
3
|
+
import { Box, Text } from 'ink';
|
|
4
4
|
import { Colors } from '../services/colors.js';
|
|
5
|
+
import { useInput } from '../services/keyboard.js';
|
|
5
6
|
export function Confirm({ message, state, onConfirmed, onCancelled, }) {
|
|
6
7
|
const done = state?.done ?? false;
|
|
7
8
|
const isCurrent = done === false;
|
package/dist/ui/Introspect.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useState } from 'react';
|
|
3
|
-
import { Box, Text
|
|
3
|
+
import { Box, Text } from 'ink';
|
|
4
4
|
import { Colors, getTextColor } from '../services/colors.js';
|
|
5
|
+
import { useInput } from '../services/keyboard.js';
|
|
6
|
+
import { formatErrorMessage } from '../services/messages.js';
|
|
5
7
|
import { Spinner } from './Spinner.js';
|
|
6
8
|
const MIN_PROCESSING_TIME = 1000;
|
|
7
9
|
const BUILT_IN_CAPABILITIES = new Set([
|
|
@@ -12,26 +14,32 @@ const BUILT_IN_CAPABILITIES = new Set([
|
|
|
12
14
|
'EXECUTE',
|
|
13
15
|
'REPORT',
|
|
14
16
|
]);
|
|
17
|
+
const INDIRECT_CAPABILITIES = new Set(['PLAN', 'REPORT']);
|
|
15
18
|
function parseCapabilityFromTask(task) {
|
|
16
19
|
// Parse "NAME: Description" format from task.action
|
|
17
20
|
const colonIndex = task.action.indexOf(':');
|
|
18
21
|
if (colonIndex === -1) {
|
|
22
|
+
const upperName = task.action.toUpperCase();
|
|
19
23
|
return {
|
|
20
24
|
name: task.action,
|
|
21
25
|
description: '',
|
|
22
|
-
isBuiltIn: BUILT_IN_CAPABILITIES.has(
|
|
26
|
+
isBuiltIn: BUILT_IN_CAPABILITIES.has(upperName),
|
|
27
|
+
isIndirect: INDIRECT_CAPABILITIES.has(upperName),
|
|
23
28
|
};
|
|
24
29
|
}
|
|
25
30
|
const name = task.action.substring(0, colonIndex).trim();
|
|
26
31
|
const description = task.action.substring(colonIndex + 1).trim();
|
|
27
|
-
const
|
|
32
|
+
const upperName = name.toUpperCase();
|
|
33
|
+
const isBuiltIn = BUILT_IN_CAPABILITIES.has(upperName);
|
|
34
|
+
const isIndirect = INDIRECT_CAPABILITIES.has(upperName);
|
|
28
35
|
return {
|
|
29
36
|
name,
|
|
30
37
|
description,
|
|
31
38
|
isBuiltIn,
|
|
39
|
+
isIndirect,
|
|
32
40
|
};
|
|
33
41
|
}
|
|
34
|
-
export function Introspect({ tasks, state, service, children, onError, onComplete, onAborted, }) {
|
|
42
|
+
export function Introspect({ tasks, state, service, children, debug = false, onError, onComplete, onAborted, }) {
|
|
35
43
|
const done = state?.done ?? false;
|
|
36
44
|
const isCurrent = done === false;
|
|
37
45
|
const [error, setError] = useState(null);
|
|
@@ -66,7 +74,12 @@ export function Introspect({ tasks, state, service, children, onError, onComplet
|
|
|
66
74
|
await new Promise((resolve) => setTimeout(resolve, remainingTime));
|
|
67
75
|
if (mounted) {
|
|
68
76
|
// Parse capabilities from returned tasks
|
|
69
|
-
|
|
77
|
+
let capabilities = result.tasks.map(parseCapabilityFromTask);
|
|
78
|
+
// Filter out internal capabilities when not in debug mode
|
|
79
|
+
if (!debug) {
|
|
80
|
+
capabilities = capabilities.filter((cap) => cap.name.toUpperCase() !== 'PLAN' &&
|
|
81
|
+
cap.name.toUpperCase() !== 'REPORT');
|
|
82
|
+
}
|
|
70
83
|
setIsLoading(false);
|
|
71
84
|
onComplete?.(result.message, capabilities);
|
|
72
85
|
}
|
|
@@ -76,7 +89,7 @@ export function Introspect({ tasks, state, service, children, onError, onComplet
|
|
|
76
89
|
const remainingTime = Math.max(0, MIN_PROCESSING_TIME - elapsed);
|
|
77
90
|
await new Promise((resolve) => setTimeout(resolve, remainingTime));
|
|
78
91
|
if (mounted) {
|
|
79
|
-
const errorMessage = err
|
|
92
|
+
const errorMessage = formatErrorMessage(err);
|
|
80
93
|
setIsLoading(false);
|
|
81
94
|
if (onError) {
|
|
82
95
|
onError(errorMessage);
|
|
@@ -91,7 +104,7 @@ export function Introspect({ tasks, state, service, children, onError, onComplet
|
|
|
91
104
|
return () => {
|
|
92
105
|
mounted = false;
|
|
93
106
|
};
|
|
94
|
-
}, [tasks, done, service]);
|
|
107
|
+
}, [tasks, done, service, debug, onComplete, onError]);
|
|
95
108
|
// Don't render wrapper when done and nothing to show
|
|
96
109
|
if (!isLoading && !error && !children) {
|
|
97
110
|
return null;
|
package/dist/ui/Main.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import React from 'react';
|
|
3
|
-
import { useInput } from 'ink';
|
|
4
3
|
import { FeedbackType } from '../types/types.js';
|
|
5
4
|
import { createAnthropicService, } from '../services/anthropic.js';
|
|
6
5
|
import { getConfigurationRequiredMessage, hasValidAnthropicKey, loadConfig, loadDebugSetting, saveDebugSetting, } from '../services/configuration.js';
|
|
6
|
+
import { registerGlobalShortcut } from '../services/keyboard.js';
|
|
7
7
|
import { getCancellationMessage } from '../services/messages.js';
|
|
8
8
|
import { createCommandDefinition, createConfigDefinition, createFeedback, createMessage, createWelcomeDefinition, isStateless, markAsDone, } from '../services/components.js';
|
|
9
9
|
import { exitApp } from '../services/process.js';
|
|
@@ -31,17 +31,17 @@ export const Main = ({ app, command }) => {
|
|
|
31
31
|
React.useEffect(() => {
|
|
32
32
|
timelineRef.current = timeline;
|
|
33
33
|
}, [timeline]);
|
|
34
|
-
//
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
34
|
+
// Register global keyboard shortcuts
|
|
35
|
+
React.useEffect(() => {
|
|
36
|
+
// Shift+Tab: Toggle debug mode
|
|
37
|
+
registerGlobalShortcut('shift+tab', () => {
|
|
38
38
|
setIsDebug((prev) => {
|
|
39
39
|
const newValue = !prev;
|
|
40
40
|
saveDebugSetting(newValue);
|
|
41
41
|
return newValue;
|
|
42
42
|
});
|
|
43
|
-
}
|
|
44
|
-
},
|
|
43
|
+
});
|
|
44
|
+
}, []);
|
|
45
45
|
const addToTimeline = React.useCallback((...items) => {
|
|
46
46
|
setTimeline((timeline) => [...timeline, ...items]);
|
|
47
47
|
}, []);
|
|
@@ -84,7 +84,7 @@ export const Main = ({ app, command }) => {
|
|
|
84
84
|
const handleAnswerAborted = React.useCallback(createAnswerAbortedHandler(handleAborted), [handleAborted]);
|
|
85
85
|
const handleAnswerError = React.useCallback((error) => setQueue(createAnswerErrorHandler(addToTimeline)(error)), [addToTimeline]);
|
|
86
86
|
const handleAnswerComplete = React.useCallback((answer) => setQueue(createAnswerCompleteHandler(addToTimeline)(answer)), [addToTimeline]);
|
|
87
|
-
const handleExecutionConfirmed = React.useCallback(() => setQueue(createExecutionConfirmedHandler(timelineRef, addToTimeline, service, handleIntrospectError, handleIntrospectComplete, handleIntrospectAborted, handleAnswerError, handleAnswerComplete, handleAnswerAborted)()), [
|
|
87
|
+
const handleExecutionConfirmed = React.useCallback(() => setQueue(createExecutionConfirmedHandler(timelineRef, addToTimeline, service, handleIntrospectError, handleIntrospectComplete, handleIntrospectAborted, handleAnswerError, handleAnswerComplete, handleAnswerAborted, setQueue)()), [
|
|
88
88
|
addToTimeline,
|
|
89
89
|
service,
|
|
90
90
|
handleIntrospectError,
|
package/dist/ui/Plan.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useState } from 'react';
|
|
3
|
-
import { Box
|
|
3
|
+
import { Box } from 'ink';
|
|
4
4
|
import { TaskType } from '../types/types.js';
|
|
5
5
|
import { getTaskColors } from '../services/colors.js';
|
|
6
|
+
import { useInput } from '../services/keyboard.js';
|
|
6
7
|
import { Label } from './Label.js';
|
|
7
8
|
import { List } from './List.js';
|
|
8
9
|
function taskToListItem(task, highlightedChildIndex = null, isDefineTaskWithoutSelection = false, isCurrent = false) {
|
package/dist/ui/Refinement.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Box
|
|
2
|
+
import { Box } from 'ink';
|
|
3
|
+
import { useInput } from '../services/keyboard.js';
|
|
3
4
|
import { Message } from './Message.js';
|
|
4
5
|
import { Spinner } from './Spinner.js';
|
|
5
6
|
export const Refinement = ({ text, state, onAborted }) => {
|
package/dist/ui/Report.js
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
3
|
import { Colors } from '../services/colors.js';
|
|
4
|
-
function CapabilityItem({ name, description, isBuiltIn }) {
|
|
5
|
-
const color =
|
|
4
|
+
function CapabilityItem({ name, description, isBuiltIn, isIndirect, }) {
|
|
5
|
+
const color = isIndirect
|
|
6
|
+
? Colors.Origin.Indirect
|
|
7
|
+
: isBuiltIn
|
|
8
|
+
? Colors.Origin.BuiltIn
|
|
9
|
+
: Colors.Origin.UserProvided;
|
|
6
10
|
return (_jsxs(Box, { children: [_jsx(Text, { children: "- " }), _jsx(Text, { color: color, children: name }), _jsxs(Text, { children: [" - ", description] })] }));
|
|
7
11
|
}
|
|
8
12
|
export function Report({ message, capabilities }) {
|
|
9
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: message }), _jsx(Box, { flexDirection: "column", marginLeft: 2, marginTop: 1, children: capabilities.map((capability, index) => (_jsx(CapabilityItem, { name: capability.name, description: capability.description, isBuiltIn: capability.isBuiltIn }, index))) })] }));
|
|
13
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: message }), _jsx(Box, { flexDirection: "column", marginLeft: 2, marginTop: 1, children: capabilities.map((capability, index) => (_jsx(CapabilityItem, { name: capability.name, description: capability.description, isBuiltIn: capability.isBuiltIn, isIndirect: capability.isIndirect }, index))) })] }));
|
|
10
14
|
}
|