prompt-language-shell 0.2.2 → 0.2.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/config/EXECUTE.md +700 -0
- package/dist/config/PLAN.md +160 -28
- package/dist/index.js +3 -3
- package/dist/services/anthropic.js +6 -2
- package/dist/services/skills.js +7 -2
- package/dist/services/skills.test.js +115 -0
- package/dist/services/tool-registry.js +1 -2
- package/dist/tools/plan.tool.js +5 -1
- package/dist/types/components.js +1 -0
- package/dist/ui/Column.js +6 -0
- package/dist/ui/Command.js +70 -20
- package/dist/ui/Component.js +20 -0
- package/dist/ui/Config.js +44 -0
- package/dist/ui/List.js +7 -0
- package/dist/ui/Main.js +20 -9
- package/dist/ui/Panel.js +5 -0
- package/dist/ui/Separator.js +6 -0
- package/dist/ui/Welcome.js +18 -9
- package/dist/ui/renderComponent.js +3 -3
- package/package.json +2 -1
- package/dist/config/SYSTEM.md +0 -156
- package/dist/services/claude.js +0 -54
- package/dist/ui/CommandProcessor.js +0 -31
package/dist/config/PLAN.md
CHANGED
|
@@ -25,31 +25,115 @@ toward any particular domain and can be validated to work correctly across
|
|
|
25
25
|
all scenarios. Do NOT assume or infer domain-specific context unless
|
|
26
26
|
explicitly provided in skills or user requests.
|
|
27
27
|
|
|
28
|
-
##
|
|
28
|
+
## Response Format
|
|
29
|
+
|
|
30
|
+
Every response MUST include an introductory message before the task list.
|
|
31
|
+
This message should introduce the PLAN, not the execution itself.
|
|
29
32
|
|
|
30
|
-
|
|
31
|
-
|
|
33
|
+
**Critical rules:**
|
|
34
|
+
- The message is MANDATORY - every single response must include one
|
|
35
|
+
- NEVER repeat the same message - each response should use different wording
|
|
36
|
+
- Must be a SINGLE sentence, maximum 64 characters (including the colon)
|
|
37
|
+
- The message introduces the plan/steps that follow, NOT the action itself
|
|
38
|
+
- ALWAYS end the message with a colon (:)
|
|
39
|
+
- Match the tone to the request (professional, helpful, reassuring)
|
|
40
|
+
- Avoid formulaic patterns - vary your phrasing naturally
|
|
41
|
+
|
|
42
|
+
**Correct examples (introducing the plan):**
|
|
43
|
+
- "Here is my plan:"
|
|
44
|
+
- "Here's what I'll do:"
|
|
45
|
+
- "Let me break this down:"
|
|
46
|
+
- "I've planned the following steps:"
|
|
47
|
+
- "Here's how I'll approach this:"
|
|
48
|
+
- "Here are the steps I'll take:"
|
|
49
|
+
- "This is my plan:"
|
|
50
|
+
- "Let me outline the approach:"
|
|
51
|
+
- "Here's the plan:"
|
|
52
|
+
|
|
53
|
+
**DO NOT:**
|
|
54
|
+
- Use the exact same phrase repeatedly
|
|
55
|
+
- Create overly long or verbose introductions
|
|
56
|
+
- Include unnecessary pleasantries or apologies
|
|
57
|
+
- Use the same sentence structure every time
|
|
58
|
+
- Phrase it as if you're executing (use "plan" language, not "doing" language)
|
|
59
|
+
- Forget the colon at the end
|
|
60
|
+
|
|
61
|
+
Remember: You are presenting a PLAN, not performing the action. The message
|
|
62
|
+
should naturally lead into a list of planned steps. Always end with a colon.
|
|
32
63
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
64
|
+
## Skills Integration
|
|
65
|
+
|
|
66
|
+
Skills define the ONLY operations you can execute. If skills are provided in
|
|
67
|
+
the "Available Skills" section below, you MUST use ONLY those skills for
|
|
68
|
+
executable operations.
|
|
69
|
+
|
|
70
|
+
**Skills are EXHAUSTIVE and EXCLUSIVE**
|
|
71
|
+
- The list of available skills is COMPLETE
|
|
72
|
+
- If an action verb does NOT have a matching skill, it CANNOT be executed
|
|
73
|
+
- You MUST create an "ignore" type task for ANY verb without a matching skill
|
|
74
|
+
- There are NO implicit or assumed operations
|
|
75
|
+
- **DO NOT infer follow-up actions based on context**
|
|
76
|
+
- **DO NOT assume operations even if they seem logically related to a matched skill**
|
|
77
|
+
- Example: If only a "backup" skill exists, and user says "backup and restore",
|
|
78
|
+
you create tasks from backup skill + one "ignore" task for "restore"
|
|
79
|
+
|
|
80
|
+
**STRICT SKILL MATCHING RULES:**
|
|
81
|
+
|
|
82
|
+
1. **Identify skill match:** For each action verb in the user's request,
|
|
83
|
+
check if a corresponding skill exists
|
|
84
|
+
- If a skill exists → use that skill
|
|
85
|
+
- If NO skill exists → create "ignore" type task
|
|
86
|
+
- **NEVER create execute tasks for unmatched verbs under ANY circumstances**
|
|
87
|
+
- This includes common verbs like "analyze", "validate", "initialize",
|
|
88
|
+
"configure", "setup" if no corresponding skill exists
|
|
89
|
+
- Do NOT infer or assume operations - only use explicitly defined skills
|
|
90
|
+
|
|
91
|
+
2. **Check for Execution section (CRITICAL):**
|
|
92
|
+
- If the skill has an "Execution" section, you MUST use it as the
|
|
93
|
+
authoritative source for task commands
|
|
94
|
+
- Each line in the Execution section corresponds to one task
|
|
95
|
+
- Extract the exact command or operation from each Execution line
|
|
96
|
+
- Replace parameter placeholders (e.g., {TARGET}, {ENV}) with specified values
|
|
97
|
+
- The action field must reference the specific command from Execution
|
|
98
|
+
- **IMPORTANT**: Once you determine the execution steps from the skill,
|
|
99
|
+
you MUST verify that each step matches a command present in the
|
|
100
|
+
Execution section. If a step does NOT have a corresponding command in
|
|
101
|
+
the Execution section, it should NOT be included in the task list.
|
|
102
|
+
- If no Execution section exists, fall back to the Steps section
|
|
103
|
+
|
|
104
|
+
3. **Handle skill parameters:**
|
|
105
|
+
- Check if the skill has parameters (e.g., {PROJECT}) or describes multiple
|
|
106
|
+
variants in its description
|
|
107
|
+
- If skill requires parameters and user didn't specify which variant:
|
|
108
|
+
Create a "define" type task with options listing all variants from the
|
|
40
109
|
skill description
|
|
41
|
-
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
- Replace parameter placeholders
|
|
110
|
+
- If user specified the variant or skill has no parameters:
|
|
111
|
+
Extract the individual steps from the skill's "Execution" or "Steps"
|
|
112
|
+
section (prefer Execution if available)
|
|
113
|
+
- Replace ALL parameter placeholders with the specified value
|
|
114
|
+
|
|
115
|
+
4. **Handle partial execution:**
|
|
116
|
+
- Keywords indicating partial execution: "only", "just", specific verbs
|
|
117
|
+
that match individual step names
|
|
118
|
+
- Consult the skill's Description section for guidance on which steps are
|
|
119
|
+
optional or conditional
|
|
120
|
+
- Example: If description says "initialization only required for clean
|
|
121
|
+
builds" and user says "rebuild cache", skip initialization steps
|
|
122
|
+
- Only extract steps that align with the user's specific request
|
|
123
|
+
|
|
124
|
+
5. **Create task definitions:**
|
|
45
125
|
- Create a task definition for each step with:
|
|
46
126
|
- action: clear, professional description starting with a capital letter
|
|
47
|
-
- type: category of operation (if the skill specifies it or you
|
|
48
|
-
can infer it)
|
|
127
|
+
- type: category of operation (if the skill specifies it or you can infer it)
|
|
49
128
|
- params: any specific parameters mentioned in the step
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
6.
|
|
129
|
+
- NEVER replace the skill's detailed steps with a generic restatement
|
|
130
|
+
|
|
131
|
+
6. **Handle additional requirements beyond the skill:**
|
|
132
|
+
- If the user's query includes additional requirements beyond the skill,
|
|
133
|
+
check if those requirements match OTHER available skills
|
|
134
|
+
- If they match a skill → append tasks from that skill
|
|
135
|
+
- If they do NOT match any skill → append "ignore" type task
|
|
136
|
+
- NEVER create generic execute tasks for unmatched requirements
|
|
53
137
|
|
|
54
138
|
Example 1 - Skill with parameter, variant specified:
|
|
55
139
|
- Skill has {PROJECT} parameter with variants: Alpha, Beta, Gamma
|
|
@@ -73,6 +157,28 @@ Example 3 - Skill without parameters:
|
|
|
73
157
|
- Correct: Four tasks (the three from skill + one for report generation)
|
|
74
158
|
- WRONG: Two tasks ("run tests", "generate a report")
|
|
75
159
|
|
|
160
|
+
Example 4 - NEGATIVE: Unmatched verb after matched skill:
|
|
161
|
+
- ONLY skill available: "backup" (with steps: connect, export, save)
|
|
162
|
+
- User: "backup data and archive it"
|
|
163
|
+
- CORRECT: Three tasks from backup skill + one "ignore" type task with
|
|
164
|
+
action "Ignore unknown 'archive' request"
|
|
165
|
+
- WRONG: Three tasks from backup skill + one execute task "Archive the
|
|
166
|
+
backed up data"
|
|
167
|
+
|
|
168
|
+
Example 5 - NEGATIVE: Multiple unmatched verbs:
|
|
169
|
+
- ONLY skill available: "sync" (with steps: connect, transfer, verify)
|
|
170
|
+
- User: "sync files and encrypt them, then notify me"
|
|
171
|
+
- CORRECT: Three tasks from sync skill + one "ignore" for "encrypt" +
|
|
172
|
+
one "ignore" for "notify"
|
|
173
|
+
- WRONG: Creating execute tasks for "encrypt" or "notify"
|
|
174
|
+
|
|
175
|
+
Example 6 - NEGATIVE: Context inference prohibition:
|
|
176
|
+
- ONLY skill available: "process" (with steps: load, transform, output)
|
|
177
|
+
- User: "process dataset and validate results"
|
|
178
|
+
- CORRECT: Three tasks from process skill + one "ignore" type task for
|
|
179
|
+
"validate"
|
|
180
|
+
- WRONG: Adding an execute task like "Validate the processed dataset results"
|
|
181
|
+
|
|
76
182
|
### Skills and Unclear Requests
|
|
77
183
|
|
|
78
184
|
When a request is vague and could match multiple skills or multiple operations
|
|
@@ -84,6 +190,10 @@ derived from available skills:
|
|
|
84
190
|
parameters
|
|
85
191
|
3. Present these as concrete options, NOT generic categories
|
|
86
192
|
4. Each option should be something the user can directly select and execute
|
|
193
|
+
5. Format options WITHOUT brackets. Use commas to separate extra information
|
|
194
|
+
instead. For example:
|
|
195
|
+
- CORRECT: "Build project Alpha, the legacy version"
|
|
196
|
+
- WRONG: "Build project Alpha (the legacy version)"
|
|
87
197
|
|
|
88
198
|
Example:
|
|
89
199
|
- Available skills: "Build Product" (variant A, variant B), "Deploy
|
|
@@ -128,12 +238,16 @@ Examples that should be aborted as offensive:
|
|
|
128
238
|
- Extract steps from the matching skill and create tasks for each step
|
|
129
239
|
|
|
130
240
|
3. **Logical consequences** - Infer natural workflow steps:
|
|
131
|
-
- "
|
|
132
|
-
Most likely means "
|
|
133
|
-
"
|
|
241
|
+
- "backup" and "sync" skills exist, user says "backup and upload" →
|
|
242
|
+
Most likely means "backup and sync" since "upload" often means
|
|
243
|
+
"sync" after backup
|
|
134
244
|
- Use context and available skills to infer the logical interpretation
|
|
135
245
|
- IMPORTANT: Only infer if matching skills exist. If no matching skill
|
|
136
246
|
exists, use "ignore" type
|
|
247
|
+
- **Strict skill matching:** For action verbs representing executable
|
|
248
|
+
operations, you MUST have a matching skill. If a user requests an action
|
|
249
|
+
that has no corresponding skill, create an "ignore" type task. Do NOT
|
|
250
|
+
create generic "execute" type tasks for commands without matching skills.
|
|
137
251
|
|
|
138
252
|
**For requests with unclear subject:**
|
|
139
253
|
|
|
@@ -252,6 +366,21 @@ steps to answer:
|
|
|
252
366
|
1. Identify each individual task or step
|
|
253
367
|
2. Break complex questions into separate, simpler task definitions
|
|
254
368
|
3. Create a task definition for each distinct operation
|
|
369
|
+
4. **For each operation, independently check if it matches a skill:**
|
|
370
|
+
- If operation matches a skill → extract skill steps
|
|
371
|
+
- If operation does NOT match a skill → create "ignore" type task
|
|
372
|
+
- **CRITICAL: Do NOT infer context or create generic execute tasks for
|
|
373
|
+
unmatched operations**
|
|
374
|
+
- Even if an unmatched operation appears after a matched skill, treat it
|
|
375
|
+
independently
|
|
376
|
+
- Do NOT create tasks like "Verify the processed X" or "Check X results"
|
|
377
|
+
for unmatched operations
|
|
378
|
+
- The ONLY valid types for unmatched operations are "ignore" or "answer"
|
|
379
|
+
(for information requests)
|
|
380
|
+
- Example: "process files and validate" where only "process" has a skill
|
|
381
|
+
→ Create tasks from process skill + create "ignore" type for "validate"
|
|
382
|
+
- Example: "deploy service and monitor" where only "deploy" has a skill
|
|
383
|
+
→ Create tasks from deploy skill + create "ignore" type for "monitor"
|
|
255
384
|
|
|
256
385
|
When breaking down complex questions:
|
|
257
386
|
|
|
@@ -440,7 +569,8 @@ Examples showing proper use of skills and disambiguation:
|
|
|
440
569
|
- "build" with build skill requiring {PROJECT} parameter (Alpha, Beta, Gamma,
|
|
441
570
|
Delta) → One task: type "define", action "Clarify which project to build",
|
|
442
571
|
params { options: ["Build Alpha", "Build Beta", "Build Gamma", "Build
|
|
443
|
-
Delta"] }
|
|
572
|
+
Delta"] }. NOTE: If variants have descriptions, format as "Build Alpha, the
|
|
573
|
+
legacy version" NOT "Build Alpha (the legacy version)"
|
|
444
574
|
- "build Alpha" with same build skill → Three tasks extracted from skill
|
|
445
575
|
steps: "Navigate to the Alpha project's root directory", "Execute the Alpha
|
|
446
576
|
project generation script", "Compile the Alpha source code"
|
|
@@ -451,11 +581,13 @@ Examples showing proper use of skills and disambiguation:
|
|
|
451
581
|
environment", "Deploy to canary environment"] }
|
|
452
582
|
- "deploy all" with deploy skill (staging, production) → Two tasks: one for
|
|
453
583
|
staging deployment, one for production deployment
|
|
454
|
-
- "
|
|
455
|
-
+
|
|
456
|
-
- "
|
|
457
|
-
|
|
458
|
-
|
|
584
|
+
- "backup and restore" with backup and restore skills → Create tasks from
|
|
585
|
+
backup skill + restore skill
|
|
586
|
+
- "backup photos and verify" with backup skill (has {TYPE} parameter) but NO
|
|
587
|
+
verify skill → Two tasks from backup skill (with {TYPE}=photos) + one
|
|
588
|
+
"ignore" type for unknown "verify"
|
|
589
|
+
- "analyze data and generate report" with analyze skill but NO generate skill →
|
|
590
|
+
Tasks from analyze skill + one "ignore" type for unknown "generate"
|
|
459
591
|
|
|
460
592
|
### Correct Examples: Requests Without Matching Skills
|
|
461
593
|
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
-
import {
|
|
4
|
-
import { fileURLToPath } from 'url';
|
|
3
|
+
import { existsSync, readFileSync } from 'fs';
|
|
5
4
|
import { dirname, join } from 'path';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
6
|
import { render, Text } from 'ink';
|
|
7
|
-
import {
|
|
7
|
+
import { ConfigError, configExists, loadConfig, saveConfig, } from './services/config.js';
|
|
8
8
|
import { createAnthropicService } from './services/anthropic.js';
|
|
9
9
|
import { Main } from './ui/Main.js';
|
|
10
10
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import Anthropic from '@anthropic-ai/sdk';
|
|
2
|
-
import {
|
|
2
|
+
import { formatSkillsForPrompt, loadSkills } from './skills.js';
|
|
3
3
|
import { toolRegistry } from './tool-registry.js';
|
|
4
4
|
export class AnthropicService {
|
|
5
5
|
client;
|
|
@@ -40,8 +40,11 @@ export class AnthropicService {
|
|
|
40
40
|
throw new Error('Expected tool_use response from Claude API');
|
|
41
41
|
}
|
|
42
42
|
const content = response.content[0];
|
|
43
|
-
// Extract and validate tasks
|
|
43
|
+
// Extract and validate message and tasks
|
|
44
44
|
const input = content.input;
|
|
45
|
+
if (!input.message || typeof input.message !== 'string') {
|
|
46
|
+
throw new Error('Invalid tool response: missing or invalid message field');
|
|
47
|
+
}
|
|
45
48
|
if (!input.tasks || !Array.isArray(input.tasks)) {
|
|
46
49
|
throw new Error('Invalid tool response: missing or invalid tasks array');
|
|
47
50
|
}
|
|
@@ -53,6 +56,7 @@ export class AnthropicService {
|
|
|
53
56
|
});
|
|
54
57
|
const isDebug = process.env.DEBUG === 'true';
|
|
55
58
|
return {
|
|
59
|
+
message: input.message,
|
|
56
60
|
tasks: input.tasks,
|
|
57
61
|
systemPrompt: isDebug ? systemPrompt : undefined,
|
|
58
62
|
};
|
package/dist/services/skills.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { join } from 'path';
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from 'fs';
|
|
3
2
|
import { homedir } from 'os';
|
|
3
|
+
import { join } from 'path';
|
|
4
4
|
/**
|
|
5
5
|
* Get the path to the skills directory
|
|
6
6
|
*/
|
|
@@ -46,6 +46,11 @@ export function formatSkillsForPrompt(skills) {
|
|
|
46
46
|
The following skills define domain-specific workflows. When the user's
|
|
47
47
|
query matches a skill, incorporate the skill's steps into your plan.
|
|
48
48
|
|
|
49
|
+
**IMPORTANT**: When creating options from skill descriptions, do NOT use
|
|
50
|
+
brackets for additional information. Use commas instead. For example:
|
|
51
|
+
- CORRECT: "Build project Alpha, the legacy version"
|
|
52
|
+
- WRONG: "Build project Alpha (the legacy version)"
|
|
53
|
+
|
|
49
54
|
`;
|
|
50
55
|
const skillsContent = skills.join('\n\n');
|
|
51
56
|
return header + skillsContent;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { mkdirSync, writeFileSync, rmSync, existsSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { tmpdir } from 'os';
|
|
5
|
+
import { getSkillsDirectory, loadSkills, formatSkillsForPrompt, } from './skills.js';
|
|
6
|
+
describe('skills service', () => {
|
|
7
|
+
let originalHome;
|
|
8
|
+
let tempHome;
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
// Mock HOME to point to temp directory
|
|
11
|
+
originalHome = process.env.HOME;
|
|
12
|
+
tempHome = join(tmpdir(), `pls-home-test-${Date.now()}`);
|
|
13
|
+
mkdirSync(tempHome, { recursive: true });
|
|
14
|
+
process.env.HOME = tempHome;
|
|
15
|
+
// Create .pls/skills directory structure
|
|
16
|
+
const plsDir = join(tempHome, '.pls');
|
|
17
|
+
mkdirSync(plsDir, { recursive: true });
|
|
18
|
+
mkdirSync(join(plsDir, 'skills'), { recursive: true });
|
|
19
|
+
});
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
// Restore original HOME first
|
|
22
|
+
if (originalHome !== undefined) {
|
|
23
|
+
process.env.HOME = originalHome;
|
|
24
|
+
}
|
|
25
|
+
// Clean up temp directory
|
|
26
|
+
if (existsSync(tempHome)) {
|
|
27
|
+
rmSync(tempHome, { recursive: true, force: true });
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
describe('getSkillsDirectory', () => {
|
|
31
|
+
it('returns path to .pls/skills in home directory', () => {
|
|
32
|
+
const skillsDir = getSkillsDirectory();
|
|
33
|
+
expect(skillsDir).toContain('.pls');
|
|
34
|
+
expect(skillsDir).toContain('skills');
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
describe('loadSkills', () => {
|
|
38
|
+
it('returns empty array when skills directory does not exist', () => {
|
|
39
|
+
// Remove the skills directory
|
|
40
|
+
const skillsDir = getSkillsDirectory();
|
|
41
|
+
if (existsSync(skillsDir)) {
|
|
42
|
+
rmSync(skillsDir, { recursive: true, force: true });
|
|
43
|
+
}
|
|
44
|
+
const skills = loadSkills();
|
|
45
|
+
expect(skills).toEqual([]);
|
|
46
|
+
});
|
|
47
|
+
it('returns empty array when skills directory is empty', () => {
|
|
48
|
+
const skills = loadSkills();
|
|
49
|
+
expect(skills).toEqual([]);
|
|
50
|
+
});
|
|
51
|
+
it('loads single skill file', () => {
|
|
52
|
+
const skillsDir = getSkillsDirectory();
|
|
53
|
+
const skillContent = `### Name
|
|
54
|
+
Build Opera
|
|
55
|
+
|
|
56
|
+
### Description
|
|
57
|
+
Run Opera Desktop browser build
|
|
58
|
+
|
|
59
|
+
### Steps
|
|
60
|
+
Navigate to the project directory, run the project generation script, run the compilation`;
|
|
61
|
+
writeFileSync(join(skillsDir, 'opera.md'), skillContent, 'utf-8');
|
|
62
|
+
const skills = loadSkills();
|
|
63
|
+
expect(skills).toHaveLength(1);
|
|
64
|
+
expect(skills[0]).toBe(skillContent);
|
|
65
|
+
});
|
|
66
|
+
it('loads multiple skill files', () => {
|
|
67
|
+
const skillsDir = getSkillsDirectory();
|
|
68
|
+
const skill1 = 'Skill 1 content';
|
|
69
|
+
const skill2 = 'Skill 2 content';
|
|
70
|
+
writeFileSync(join(skillsDir, 'skill1.md'), skill1, 'utf-8');
|
|
71
|
+
writeFileSync(join(skillsDir, 'skill2.md'), skill2, 'utf-8');
|
|
72
|
+
const skills = loadSkills();
|
|
73
|
+
expect(skills).toHaveLength(2);
|
|
74
|
+
expect(skills).toContain(skill1);
|
|
75
|
+
expect(skills).toContain(skill2);
|
|
76
|
+
});
|
|
77
|
+
it('ignores non-markdown files', () => {
|
|
78
|
+
const skillsDir = getSkillsDirectory();
|
|
79
|
+
writeFileSync(join(skillsDir, 'skill.md'), 'Skill content', 'utf-8');
|
|
80
|
+
writeFileSync(join(skillsDir, 'readme.txt'), 'Not a skill', 'utf-8');
|
|
81
|
+
writeFileSync(join(skillsDir, 'data.json'), '{}', 'utf-8');
|
|
82
|
+
const skills = loadSkills();
|
|
83
|
+
expect(skills).toHaveLength(1);
|
|
84
|
+
expect(skills[0]).toBe('Skill content');
|
|
85
|
+
});
|
|
86
|
+
it('handles both .md and .MD extensions', () => {
|
|
87
|
+
const skillsDir = getSkillsDirectory();
|
|
88
|
+
writeFileSync(join(skillsDir, 'skill1.md'), 'Lowercase', 'utf-8');
|
|
89
|
+
writeFileSync(join(skillsDir, 'skill2.MD'), 'Uppercase', 'utf-8');
|
|
90
|
+
const skills = loadSkills();
|
|
91
|
+
expect(skills).toHaveLength(2);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
describe('formatSkillsForPrompt', () => {
|
|
95
|
+
it('returns empty string when no skills', () => {
|
|
96
|
+
const formatted = formatSkillsForPrompt([]);
|
|
97
|
+
expect(formatted).toBe('');
|
|
98
|
+
});
|
|
99
|
+
it('formats single skill with header', () => {
|
|
100
|
+
const skills = ['Skill 1 content'];
|
|
101
|
+
const formatted = formatSkillsForPrompt(skills);
|
|
102
|
+
expect(formatted).toContain('## Available Skills');
|
|
103
|
+
expect(formatted).toContain('The following skills define domain-specific workflows');
|
|
104
|
+
expect(formatted).toContain('Skill 1 content');
|
|
105
|
+
});
|
|
106
|
+
it('formats multiple skills separated by blank lines', () => {
|
|
107
|
+
const skills = ['Skill 1', 'Skill 2', 'Skill 3'];
|
|
108
|
+
const formatted = formatSkillsForPrompt(skills);
|
|
109
|
+
expect(formatted).toContain('Skill 1');
|
|
110
|
+
expect(formatted).toContain('Skill 2');
|
|
111
|
+
expect(formatted).toContain('Skill 3');
|
|
112
|
+
expect(formatted).toContain('\n\n');
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
});
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { readFileSync } from 'fs';
|
|
2
|
-
import { resolve } from 'path';
|
|
2
|
+
import { dirname, resolve } from 'path';
|
|
3
3
|
import { fileURLToPath } from 'url';
|
|
4
|
-
import { dirname } from 'path';
|
|
5
4
|
const __filename = fileURLToPath(import.meta.url);
|
|
6
5
|
const __dirname = dirname(__filename);
|
|
7
6
|
class ToolRegistry {
|
package/dist/tools/plan.tool.js
CHANGED
|
@@ -4,6 +4,10 @@ export const planTool = {
|
|
|
4
4
|
input_schema: {
|
|
5
5
|
type: 'object',
|
|
6
6
|
properties: {
|
|
7
|
+
message: {
|
|
8
|
+
type: 'string',
|
|
9
|
+
description: 'Introductory reply to display before the task list. Must be a single sentence, maximum 64 characters (including the colon at the end). Vary this naturally - try to use a different phrase each time.',
|
|
10
|
+
},
|
|
7
11
|
tasks: {
|
|
8
12
|
type: 'array',
|
|
9
13
|
description: 'Array of planned tasks to execute',
|
|
@@ -27,6 +31,6 @@ export const planTool = {
|
|
|
27
31
|
},
|
|
28
32
|
},
|
|
29
33
|
},
|
|
30
|
-
required: ['tasks'],
|
|
34
|
+
required: ['message', 'tasks'],
|
|
31
35
|
},
|
|
32
36
|
};
|
package/dist/types/components.js
CHANGED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Box } from 'ink';
|
|
3
|
+
import { Component } from './Component.js';
|
|
4
|
+
export const Column = ({ items }) => {
|
|
5
|
+
return (_jsx(Box, { marginTop: 1, flexDirection: "column", gap: 1, children: items.map((item, index) => (_jsx(Box, { children: _jsx(Component, { def: item }) }, index))) }));
|
|
6
|
+
};
|
package/dist/ui/Command.js
CHANGED
|
@@ -2,27 +2,79 @@ import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-run
|
|
|
2
2
|
import { useEffect, useState } from 'react';
|
|
3
3
|
import { Box, Text } from 'ink';
|
|
4
4
|
import { TaskType } from '../types/components.js';
|
|
5
|
+
import { List } from './List.js';
|
|
6
|
+
import { Separator } from './Separator.js';
|
|
5
7
|
import { Spinner } from './Spinner.js';
|
|
6
8
|
const MIN_PROCESSING_TIME = 1000; // purely for visual effect
|
|
7
|
-
|
|
8
|
-
|
|
9
|
+
// Color palette
|
|
10
|
+
const ColorPalette = {
|
|
11
|
+
[TaskType.Config]: {
|
|
12
|
+
description: '#ffffff', // white
|
|
13
|
+
type: '#5c9ccc', // cyan
|
|
14
|
+
},
|
|
15
|
+
[TaskType.Plan]: {
|
|
16
|
+
description: '#ffffff', // white
|
|
17
|
+
type: '#cc5c9c', // magenta
|
|
18
|
+
},
|
|
19
|
+
[TaskType.Execute]: {
|
|
20
|
+
description: '#ffffff', // white
|
|
21
|
+
type: '#4a9a7a', // green
|
|
22
|
+
},
|
|
23
|
+
[TaskType.Answer]: {
|
|
24
|
+
description: '#ffffff', // white
|
|
25
|
+
type: '#9c5ccc', // purple
|
|
26
|
+
},
|
|
27
|
+
[TaskType.Report]: {
|
|
28
|
+
description: '#ffffff', // white
|
|
29
|
+
type: '#cc9c5c', // orange
|
|
30
|
+
},
|
|
31
|
+
[TaskType.Define]: {
|
|
32
|
+
description: '#ffffff', // white
|
|
33
|
+
type: '#cc9c5c', // amber
|
|
34
|
+
},
|
|
35
|
+
[TaskType.Ignore]: {
|
|
36
|
+
description: '#cccc5c', // yellow
|
|
37
|
+
type: '#cc7a5c', // orange
|
|
38
|
+
},
|
|
39
|
+
[TaskType.Select]: {
|
|
40
|
+
description: '#888888', // grey
|
|
41
|
+
type: '#5c8cbc', // steel blue
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
function taskToListItem(task) {
|
|
45
|
+
const colors = ColorPalette[task.type];
|
|
46
|
+
const item = {
|
|
47
|
+
description: {
|
|
48
|
+
text: task.action,
|
|
49
|
+
color: colors.description,
|
|
50
|
+
},
|
|
51
|
+
type: {
|
|
52
|
+
text: task.type,
|
|
53
|
+
color: colors.type,
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
// Add children for Define tasks with options
|
|
57
|
+
if (task.type === TaskType.Define && Array.isArray(task.params?.options)) {
|
|
58
|
+
const selectColors = ColorPalette[TaskType.Select];
|
|
59
|
+
item.children = task.params.options.map((option) => ({
|
|
60
|
+
description: {
|
|
61
|
+
text: String(option),
|
|
62
|
+
color: selectColors.description,
|
|
63
|
+
},
|
|
64
|
+
type: {
|
|
65
|
+
text: TaskType.Select,
|
|
66
|
+
color: selectColors.type,
|
|
67
|
+
},
|
|
68
|
+
}));
|
|
69
|
+
}
|
|
70
|
+
return item;
|
|
9
71
|
}
|
|
10
|
-
function
|
|
11
|
-
if (taskType === TaskType.Ignore)
|
|
12
|
-
return 'red';
|
|
13
|
-
if (taskType === TaskType.Define)
|
|
14
|
-
return 'blue';
|
|
15
|
-
return 'greenBright';
|
|
16
|
-
}
|
|
17
|
-
function shouldDimTaskType(taskType) {
|
|
18
|
-
return taskType !== TaskType.Define;
|
|
19
|
-
}
|
|
20
|
-
export function Command({ command, state, service, tasks, error: errorProp, systemPrompt: systemPromptProp, }) {
|
|
72
|
+
export function Command({ command, state, service, error: errorProp, children, }) {
|
|
21
73
|
const done = state?.done ?? false;
|
|
22
|
-
const [processedTasks, setProcessedTasks] = useState(tasks || []);
|
|
23
|
-
const [systemPrompt, setSystemPrompt] = useState(systemPromptProp);
|
|
24
74
|
const [error, setError] = useState(state?.error || errorProp || null);
|
|
25
75
|
const [isLoading, setIsLoading] = useState(state?.isLoading ?? !done);
|
|
76
|
+
const [message, setMessage] = useState('');
|
|
77
|
+
const [tasks, setTasks] = useState([]);
|
|
26
78
|
useEffect(() => {
|
|
27
79
|
// Skip processing if done (showing historical/final state)
|
|
28
80
|
if (done) {
|
|
@@ -43,8 +95,8 @@ export function Command({ command, state, service, tasks, error: errorProp, syst
|
|
|
43
95
|
const remainingTime = Math.max(0, MIN_PROCESSING_TIME - elapsed);
|
|
44
96
|
await new Promise((resolve) => setTimeout(resolve, remainingTime));
|
|
45
97
|
if (mounted) {
|
|
46
|
-
|
|
47
|
-
|
|
98
|
+
setMessage(result.message);
|
|
99
|
+
setTasks(result.tasks);
|
|
48
100
|
setIsLoading(false);
|
|
49
101
|
}
|
|
50
102
|
}
|
|
@@ -63,7 +115,5 @@ export function Command({ command, state, service, tasks, error: errorProp, syst
|
|
|
63
115
|
mounted = false;
|
|
64
116
|
};
|
|
65
117
|
}, [command, done, service]);
|
|
66
|
-
return (_jsxs(Box, { alignSelf: "flex-start", marginBottom: 1, flexDirection: "column", children: [_jsxs(Box, { children: [_jsxs(Text, { color: "gray", children: ["> pls ", command] }), isLoading && (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsx(Spinner, {})] }))] }), error && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "red", children: ["Error: ", error] }) })),
|
|
67
|
-
task.params?.options &&
|
|
68
|
-
Array.isArray(task.params.options) && (_jsx(Box, { flexDirection: "column", marginLeft: 4, children: task.params.options.map((option, optIndex) => (_jsx(Box, { children: _jsxs(Text, { color: "whiteBright", dimColor: true, children: ["- ", String(option)] }) }, optIndex))) })))] }, index))) }))] }));
|
|
118
|
+
return (_jsxs(Box, { alignSelf: "flex-start", marginBottom: 1, flexDirection: "column", children: [_jsxs(Box, { children: [_jsxs(Text, { color: "gray", children: ["> pls ", command] }), isLoading && (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsx(Spinner, {})] }))] }), error && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "red", children: ["Error: ", error] }) })), !isLoading && tasks.length > 0 && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [message && (_jsxs(Box, { marginBottom: 1, children: [_jsxs(Text, { children: [" ", message] }), _jsx(Separator, { color: "#9c5ccc" }), _jsx(Text, { color: "#9c5ccc", children: "plan" })] })), _jsx(List, { items: tasks.map(taskToListItem) })] })), children] }));
|
|
69
119
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Command } from './Command.js';
|
|
3
|
+
import { Config } from './Config.js';
|
|
4
|
+
import { Welcome } from './Welcome.js';
|
|
5
|
+
export function Component({ def }) {
|
|
6
|
+
switch (def.name) {
|
|
7
|
+
case 'welcome':
|
|
8
|
+
return _jsx(Welcome, { ...def.props });
|
|
9
|
+
case 'config': {
|
|
10
|
+
const props = def.props;
|
|
11
|
+
const state = def.state;
|
|
12
|
+
return _jsx(Config, { ...props, state: state });
|
|
13
|
+
}
|
|
14
|
+
case 'command': {
|
|
15
|
+
const props = def.props;
|
|
16
|
+
const state = def.state;
|
|
17
|
+
return _jsx(Command, { ...props, state: state });
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { Box, Text } from 'ink';
|
|
4
|
+
import TextInput from 'ink-text-input';
|
|
5
|
+
export function Config({ steps, state, onFinished }) {
|
|
6
|
+
const done = state?.done ?? false;
|
|
7
|
+
const [step, setStep] = React.useState(done ? steps.length : 0);
|
|
8
|
+
const [values, setValues] = React.useState(() => {
|
|
9
|
+
const initial = {};
|
|
10
|
+
steps.forEach((step) => {
|
|
11
|
+
if (step.value !== null) {
|
|
12
|
+
initial[step.key] = step.value;
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
return initial;
|
|
16
|
+
});
|
|
17
|
+
const [inputValue, setInputValue] = React.useState('');
|
|
18
|
+
const handleSubmit = (value) => {
|
|
19
|
+
const currentStepConfig = steps[step];
|
|
20
|
+
const finalValue = value.trim() || currentStepConfig.value || '';
|
|
21
|
+
const newValues = { ...values, [currentStepConfig.key]: finalValue };
|
|
22
|
+
setValues(newValues);
|
|
23
|
+
setInputValue('');
|
|
24
|
+
if (step === steps.length - 1) {
|
|
25
|
+
// Last step completed
|
|
26
|
+
if (onFinished) {
|
|
27
|
+
onFinished(newValues);
|
|
28
|
+
}
|
|
29
|
+
setStep(steps.length);
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
setStep(step + 1);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
return (_jsxs(Box, { flexDirection: "column", marginLeft: 1, children: [steps.map((stepConfig, index) => {
|
|
36
|
+
const isCurrentStep = index === step && !done;
|
|
37
|
+
const isCompleted = index < step || done;
|
|
38
|
+
const shouldShow = isCompleted || isCurrentStep;
|
|
39
|
+
if (!shouldShow) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: index === 0 ? 0 : 1, children: [_jsx(Box, { children: _jsxs(Text, { children: [stepConfig.description, ":"] }) }), _jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: "> " }), isCurrentStep ? (_jsx(TextInput, { value: inputValue, onChange: setInputValue, onSubmit: handleSubmit })) : (_jsx(Text, { dimColor: true, children: values[stepConfig.key] || '' }))] })] }, stepConfig.key));
|
|
43
|
+
}), step === steps.length && !done && (_jsx(Box, { marginY: 1, children: _jsx(Text, { color: "green", children: "\u2713 Configuration complete" }) }))] }));
|
|
44
|
+
}
|