keystone-cli 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +326 -59
- package/package.json +1 -1
- package/src/cli.ts +90 -81
- package/src/db/workflow-db.ts +0 -7
- package/src/expression/evaluator.test.ts +42 -0
- package/src/expression/evaluator.ts +28 -0
- package/src/parser/agent-parser.test.ts +10 -0
- package/src/parser/agent-parser.ts +2 -1
- package/src/parser/config-schema.ts +13 -5
- package/src/parser/workflow-parser.ts +0 -5
- package/src/runner/llm-adapter.test.ts +0 -8
- package/src/runner/llm-adapter.ts +33 -10
- package/src/runner/llm-executor.test.ts +59 -18
- package/src/runner/llm-executor.ts +1 -1
- package/src/runner/mcp-client.test.ts +166 -88
- package/src/runner/mcp-client.ts +156 -22
- package/src/runner/mcp-manager.test.ts +73 -15
- package/src/runner/mcp-manager.ts +44 -18
- package/src/runner/mcp-server.test.ts +4 -1
- package/src/runner/mcp-server.ts +25 -11
- package/src/runner/shell-executor.ts +3 -3
- package/src/runner/step-executor.ts +10 -9
- package/src/runner/tool-integration.test.ts +21 -14
- package/src/runner/workflow-runner.ts +25 -5
- package/src/templates/agents/explore.md +54 -0
- package/src/templates/agents/general.md +8 -0
- package/src/templates/agents/keystone-architect.md +54 -0
- package/src/templates/agents/my-agent.md +3 -0
- package/src/templates/agents/summarizer.md +28 -0
- package/src/templates/agents/test-agent.md +10 -0
- package/src/templates/approval-process.yaml +36 -0
- package/src/templates/basic-inputs.yaml +19 -0
- package/src/templates/basic-shell.yaml +20 -0
- package/src/templates/batch-processor.yaml +43 -0
- package/src/templates/cleanup-finally.yaml +22 -0
- package/src/templates/composition-child.yaml +13 -0
- package/src/templates/composition-parent.yaml +14 -0
- package/src/templates/data-pipeline.yaml +38 -0
- package/src/templates/full-feature-demo.yaml +64 -0
- package/src/templates/human-interaction.yaml +12 -0
- package/src/templates/invalid.yaml +5 -0
- package/src/templates/llm-agent.yaml +8 -0
- package/src/templates/loop-parallel.yaml +37 -0
- package/src/templates/retry-policy.yaml +36 -0
- package/src/templates/scaffold-feature.yaml +48 -0
- package/src/templates/state.db +0 -0
- package/src/templates/state.db-shm +0 -0
- package/src/templates/state.db-wal +0 -0
- package/src/templates/stop-watch.yaml +17 -0
- package/src/templates/workflow.db +0 -0
- package/src/utils/config-loader.test.ts +2 -2
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
name: batch-processor
|
|
2
|
+
description: "Process multiple files in parallel"
|
|
3
|
+
|
|
4
|
+
inputs:
|
|
5
|
+
target_dir: { type: string, default: "./data" }
|
|
6
|
+
|
|
7
|
+
outputs:
|
|
8
|
+
processed_count: ${{ steps.process_files.outputs.length }}
|
|
9
|
+
|
|
10
|
+
env:
|
|
11
|
+
API_KEY: ${{ secrets.API_KEY }}
|
|
12
|
+
|
|
13
|
+
steps:
|
|
14
|
+
# 1. Dynamic Input Generation
|
|
15
|
+
- id: list_files
|
|
16
|
+
type: shell
|
|
17
|
+
run: "ls ${{ inputs.target_dir }}/*.txt"
|
|
18
|
+
# Extract stdout lines into an array
|
|
19
|
+
transform: "stdout.split('\\n').filter(Boolean)"
|
|
20
|
+
|
|
21
|
+
# 2. Matrix/Looping (The Missing Piece)
|
|
22
|
+
- id: process_files
|
|
23
|
+
type: llm
|
|
24
|
+
needs: [list_files]
|
|
25
|
+
# Iterates over the array from the previous step
|
|
26
|
+
foreach: ${{ steps.list_files.output }}
|
|
27
|
+
concurrency: 5
|
|
28
|
+
# Robustness (The Missing Piece)
|
|
29
|
+
retry:
|
|
30
|
+
count: 3
|
|
31
|
+
backoff: "exponential"
|
|
32
|
+
timeout: 30000 # 30s limit per item
|
|
33
|
+
# Step Definition
|
|
34
|
+
agent: summarizer
|
|
35
|
+
prompt: "Summarize this file: ${{ item }}"
|
|
36
|
+
|
|
37
|
+
# 3. Conditional Logic
|
|
38
|
+
- id: notify
|
|
39
|
+
type: request
|
|
40
|
+
needs: [process_files]
|
|
41
|
+
if: ${{ steps.process_files.items.every(s => s.status == 'success') }}
|
|
42
|
+
method: POST
|
|
43
|
+
url: "https://webhook.site/..."
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
name: cleanup-finally
|
|
2
|
+
description: "Test the finally block"
|
|
3
|
+
|
|
4
|
+
steps:
|
|
5
|
+
- id: step1
|
|
6
|
+
type: shell
|
|
7
|
+
run: echo "Main step 1"
|
|
8
|
+
|
|
9
|
+
- id: fail_step
|
|
10
|
+
type: shell
|
|
11
|
+
run: "exit 1"
|
|
12
|
+
if: ${{ inputs.should_fail }}
|
|
13
|
+
|
|
14
|
+
finally:
|
|
15
|
+
- id: cleanup
|
|
16
|
+
type: shell
|
|
17
|
+
run: echo "Cleanup task executed"
|
|
18
|
+
|
|
19
|
+
- id: cleanup_with_dep
|
|
20
|
+
type: shell
|
|
21
|
+
needs: [cleanup]
|
|
22
|
+
run: echo "Cleanup with dependency executed"
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
name: composition-child
|
|
2
|
+
description: "A simple child workflow"
|
|
3
|
+
|
|
4
|
+
inputs:
|
|
5
|
+
name: { type: string }
|
|
6
|
+
|
|
7
|
+
outputs:
|
|
8
|
+
result: "Hello from child, ${{ inputs.name }}!"
|
|
9
|
+
|
|
10
|
+
steps:
|
|
11
|
+
- id: echo_step
|
|
12
|
+
type: shell
|
|
13
|
+
run: echo "Processing ${{ inputs.name }}"
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
name: composition-parent
|
|
2
|
+
description: "A parent workflow that calls a child workflow"
|
|
3
|
+
|
|
4
|
+
steps:
|
|
5
|
+
- id: run_child
|
|
6
|
+
type: workflow
|
|
7
|
+
path: "workflows/composition-child.yaml"
|
|
8
|
+
inputs:
|
|
9
|
+
name: "Keystone User"
|
|
10
|
+
|
|
11
|
+
- id: print_result
|
|
12
|
+
type: shell
|
|
13
|
+
needs: [run_child]
|
|
14
|
+
run: echo "Child workflow result - ${{ steps.run_child.outputs.result }}"
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
name: data-pipeline
|
|
2
|
+
description: "A workflow that demonstrates file operations and cleanup with 'finally'"
|
|
3
|
+
|
|
4
|
+
inputs:
|
|
5
|
+
input_file: { type: string, default: "input.txt" }
|
|
6
|
+
output_file: { type: string, default: "output.txt" }
|
|
7
|
+
|
|
8
|
+
steps:
|
|
9
|
+
- id: prepare_data
|
|
10
|
+
type: file
|
|
11
|
+
op: write
|
|
12
|
+
path: ${{ inputs.input_file }}
|
|
13
|
+
content: "line 1\nline 2\nline 3"
|
|
14
|
+
|
|
15
|
+
- id: read_data
|
|
16
|
+
type: file
|
|
17
|
+
op: read
|
|
18
|
+
path: ${{ inputs.input_file }}
|
|
19
|
+
needs: [prepare_data]
|
|
20
|
+
|
|
21
|
+
- id: process_data
|
|
22
|
+
type: shell
|
|
23
|
+
run: |
|
|
24
|
+
echo "${{ steps.read_data.output }}" | tr '[:lower:]' '[:upper:]'
|
|
25
|
+
transform: "stdout.trim()"
|
|
26
|
+
needs: [read_data]
|
|
27
|
+
|
|
28
|
+
- id: save_result
|
|
29
|
+
type: file
|
|
30
|
+
op: write
|
|
31
|
+
path: ${{ inputs.output_file }}
|
|
32
|
+
content: ${{ steps.process_data.output }}
|
|
33
|
+
needs: [process_data]
|
|
34
|
+
|
|
35
|
+
finally:
|
|
36
|
+
- id: cleanup
|
|
37
|
+
type: shell
|
|
38
|
+
run: rm ${{ inputs.input_file }}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
name: full-feature-demo
|
|
2
|
+
description: "A comprehensive workflow demonstrating multiple feature types"
|
|
3
|
+
|
|
4
|
+
inputs:
|
|
5
|
+
message:
|
|
6
|
+
type: string
|
|
7
|
+
default: "Hello from Keystone!"
|
|
8
|
+
|
|
9
|
+
outputs:
|
|
10
|
+
greeting: ${{ steps.greet.output }}
|
|
11
|
+
file_count: ${{ steps.count_files.output }}
|
|
12
|
+
timestamp: ${{ steps.get_date.output }}
|
|
13
|
+
|
|
14
|
+
steps:
|
|
15
|
+
# Test shell execution
|
|
16
|
+
- id: greet
|
|
17
|
+
type: shell
|
|
18
|
+
run: echo "${{ inputs.message }}"
|
|
19
|
+
|
|
20
|
+
# Test shell with transform
|
|
21
|
+
- id: get_date
|
|
22
|
+
type: shell
|
|
23
|
+
run: date +%s
|
|
24
|
+
transform: parseInt(stdout.trim())
|
|
25
|
+
|
|
26
|
+
# Test file write
|
|
27
|
+
- id: write_file
|
|
28
|
+
type: file
|
|
29
|
+
op: write
|
|
30
|
+
path: /tmp/keystone-test.txt
|
|
31
|
+
content: "Test file created at ${{ steps.get_date.output }}"
|
|
32
|
+
|
|
33
|
+
# Test file read
|
|
34
|
+
- id: read_file
|
|
35
|
+
type: file
|
|
36
|
+
op: read
|
|
37
|
+
path: /tmp/keystone-test.txt
|
|
38
|
+
needs: [write_file]
|
|
39
|
+
|
|
40
|
+
# Test shell with dependencies
|
|
41
|
+
- id: count_files
|
|
42
|
+
type: shell
|
|
43
|
+
needs: [write_file]
|
|
44
|
+
run: ls /tmp/keystone-*.txt | wc -l
|
|
45
|
+
transform: parseInt(stdout.trim())
|
|
46
|
+
|
|
47
|
+
# Test conditional execution
|
|
48
|
+
- id: success_message
|
|
49
|
+
type: shell
|
|
50
|
+
if: ${{ steps.count_files.output > 0 }}
|
|
51
|
+
needs: [count_files]
|
|
52
|
+
run: echo "Found files!"
|
|
53
|
+
|
|
54
|
+
# Test HTTP request
|
|
55
|
+
- id: api_test
|
|
56
|
+
type: request
|
|
57
|
+
url: https://httpbin.org/get
|
|
58
|
+
method: GET
|
|
59
|
+
|
|
60
|
+
# Test sleep
|
|
61
|
+
- id: wait
|
|
62
|
+
type: sleep
|
|
63
|
+
duration: 100
|
|
64
|
+
needs: [api_test]
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
name: human-interaction
|
|
2
|
+
description: A workflow that prompts the user for input and uses it in another step.
|
|
3
|
+
|
|
4
|
+
steps:
|
|
5
|
+
- id: ask_name
|
|
6
|
+
type: human
|
|
7
|
+
message: "What is your name?"
|
|
8
|
+
inputType: text
|
|
9
|
+
|
|
10
|
+
- id: greet
|
|
11
|
+
type: shell
|
|
12
|
+
run: echo "Hello, ${{ escape(steps.ask_name.output) }}!"
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
name: loop-parallel
|
|
2
|
+
description: "Test the foreach race condition fix and .every() support"
|
|
3
|
+
|
|
4
|
+
outputs:
|
|
5
|
+
all_success: ${{ steps.process_items.items.every(s => s.status == 'success') }}
|
|
6
|
+
item_count: ${{ steps.process_items.outputs.length }}
|
|
7
|
+
first_output: ${{ steps.process_items.items[0].output }}
|
|
8
|
+
|
|
9
|
+
steps:
|
|
10
|
+
# Generate test data
|
|
11
|
+
- id: generate_items
|
|
12
|
+
type: shell
|
|
13
|
+
run: "echo 'item1\nitem2\nitem3\nitem4\nitem5'"
|
|
14
|
+
transform: "stdout.split('\\n').filter(Boolean)"
|
|
15
|
+
|
|
16
|
+
# Process items with concurrency (tests race condition fix)
|
|
17
|
+
- id: process_items
|
|
18
|
+
type: shell
|
|
19
|
+
needs: [generate_items]
|
|
20
|
+
foreach: ${{ steps.generate_items.output }}
|
|
21
|
+
concurrency: 3
|
|
22
|
+
run: "echo 'Processing: ${{ item }}' && sleep 0.1"
|
|
23
|
+
transform: "stdout.trim()"
|
|
24
|
+
|
|
25
|
+
# Test conditional using .every() (tests aggregation fix)
|
|
26
|
+
- id: success_notification
|
|
27
|
+
type: shell
|
|
28
|
+
needs: [process_items]
|
|
29
|
+
if: ${{ steps.process_items.items.every(s => s.status == 'success') }}
|
|
30
|
+
run: "echo 'All items processed successfully!'"
|
|
31
|
+
|
|
32
|
+
# Test accessing individual item status
|
|
33
|
+
- id: check_first
|
|
34
|
+
type: shell
|
|
35
|
+
needs: [process_items]
|
|
36
|
+
if: ${{ steps.process_items.items[0].status == 'success' }}
|
|
37
|
+
run: "echo 'First item was successful'"
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
name: retry-policy
|
|
2
|
+
description: "Test retry and timeout features"
|
|
3
|
+
|
|
4
|
+
steps:
|
|
5
|
+
# Test retry with a command that fails first few times
|
|
6
|
+
- id: flaky_command
|
|
7
|
+
type: shell
|
|
8
|
+
run: |
|
|
9
|
+
if [ ! -f /tmp/keystone-retry-test ]; then
|
|
10
|
+
echo "1" > /tmp/keystone-retry-test
|
|
11
|
+
exit 1
|
|
12
|
+
else
|
|
13
|
+
count=$(cat /tmp/keystone-retry-test)
|
|
14
|
+
count=$((count + 1))
|
|
15
|
+
echo $count > /tmp/keystone-retry-test
|
|
16
|
+
if [ $count -lt 3 ]; then
|
|
17
|
+
exit 1
|
|
18
|
+
fi
|
|
19
|
+
echo "Success after $count attempts"
|
|
20
|
+
fi
|
|
21
|
+
retry:
|
|
22
|
+
count: 3
|
|
23
|
+
backoff: exponential
|
|
24
|
+
|
|
25
|
+
# Test timeout (should complete before timeout)
|
|
26
|
+
- id: quick_task
|
|
27
|
+
type: shell
|
|
28
|
+
run: sleep 0.1 && echo "Completed"
|
|
29
|
+
timeout: 5000
|
|
30
|
+
needs: [flaky_command]
|
|
31
|
+
|
|
32
|
+
# Cleanup
|
|
33
|
+
- id: cleanup
|
|
34
|
+
type: shell
|
|
35
|
+
run: rm -f /tmp/keystone-retry-test
|
|
36
|
+
needs: [quick_task]
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
name: scaffold-feature
|
|
2
|
+
description: "Autonomously build new Keystone workflows and agents"
|
|
3
|
+
|
|
4
|
+
steps:
|
|
5
|
+
- id: get_requirements
|
|
6
|
+
type: human
|
|
7
|
+
message: "Describe the workflow you want to build:"
|
|
8
|
+
inputType: text
|
|
9
|
+
|
|
10
|
+
- id: design
|
|
11
|
+
type: llm
|
|
12
|
+
agent: keystone-architect
|
|
13
|
+
needs: [get_requirements]
|
|
14
|
+
prompt: |
|
|
15
|
+
The user wants to build the following:
|
|
16
|
+
<user_requirements>
|
|
17
|
+
${{ steps.get_requirements.output }}
|
|
18
|
+
</user_requirements>
|
|
19
|
+
Generate the necessary Keystone workflow and optionally create an agent where appropriate.
|
|
20
|
+
schema:
|
|
21
|
+
type: object
|
|
22
|
+
properties:
|
|
23
|
+
files:
|
|
24
|
+
type: array
|
|
25
|
+
items:
|
|
26
|
+
type: object
|
|
27
|
+
properties:
|
|
28
|
+
path:
|
|
29
|
+
type: string
|
|
30
|
+
content:
|
|
31
|
+
type: string
|
|
32
|
+
required: [path, content]
|
|
33
|
+
required: [files]
|
|
34
|
+
|
|
35
|
+
- id: write_files
|
|
36
|
+
type: file
|
|
37
|
+
needs: [design]
|
|
38
|
+
foreach: ${{ steps.design.output.files }}
|
|
39
|
+
op: write
|
|
40
|
+
path: ${{ item.path }}
|
|
41
|
+
content: ${{ item.content }}
|
|
42
|
+
|
|
43
|
+
- id: summary
|
|
44
|
+
type: shell
|
|
45
|
+
needs: [write_files]
|
|
46
|
+
run: |
|
|
47
|
+
echo "Scaffolding complete. Files created:"
|
|
48
|
+
echo "${{ steps.design.output.files.map(f => f.path).join('\n') }}"
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
name: stop-watch
|
|
2
|
+
description: "A simple stopwatch workflow"
|
|
3
|
+
steps:
|
|
4
|
+
- id: get_duration
|
|
5
|
+
type: human
|
|
6
|
+
message: "Please enter the duration (in seconds) for the stopwatch."
|
|
7
|
+
inputType: text
|
|
8
|
+
- id: waiting_period
|
|
9
|
+
type: sleep
|
|
10
|
+
needs: [get_duration]
|
|
11
|
+
duration: ${{ Number(steps.get_duration.output) * 1000 }}
|
|
12
|
+
- id: notify_completion
|
|
13
|
+
type: human
|
|
14
|
+
needs: [waiting_period]
|
|
15
|
+
message: "The stopwatch is complete."
|
|
16
|
+
inputType: confirm
|
|
17
|
+
outputs: {}
|
|
File without changes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { describe, expect, it
|
|
2
|
-
import { ConfigLoader } from './config-loader';
|
|
1
|
+
import { afterEach, describe, expect, it } from 'bun:test';
|
|
3
2
|
import type { Config } from '../parser/config-schema';
|
|
3
|
+
import { ConfigLoader } from './config-loader';
|
|
4
4
|
|
|
5
5
|
describe('ConfigLoader', () => {
|
|
6
6
|
afterEach(() => {
|