keystone-cli 0.1.1 → 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.
Files changed (51) hide show
  1. package/README.md +52 -15
  2. package/package.json +1 -1
  3. package/src/cli.ts +90 -81
  4. package/src/db/workflow-db.ts +0 -7
  5. package/src/expression/evaluator.test.ts +42 -0
  6. package/src/expression/evaluator.ts +28 -0
  7. package/src/parser/agent-parser.test.ts +10 -0
  8. package/src/parser/agent-parser.ts +2 -1
  9. package/src/parser/config-schema.ts +13 -5
  10. package/src/parser/workflow-parser.ts +0 -5
  11. package/src/runner/llm-adapter.test.ts +0 -8
  12. package/src/runner/llm-adapter.ts +33 -10
  13. package/src/runner/llm-executor.test.ts +59 -18
  14. package/src/runner/llm-executor.ts +1 -1
  15. package/src/runner/mcp-client.test.ts +166 -88
  16. package/src/runner/mcp-client.ts +156 -22
  17. package/src/runner/mcp-manager.test.ts +73 -15
  18. package/src/runner/mcp-manager.ts +44 -18
  19. package/src/runner/mcp-server.test.ts +4 -1
  20. package/src/runner/mcp-server.ts +25 -11
  21. package/src/runner/shell-executor.ts +3 -3
  22. package/src/runner/step-executor.ts +10 -9
  23. package/src/runner/tool-integration.test.ts +21 -14
  24. package/src/runner/workflow-runner.ts +25 -5
  25. package/src/templates/agents/explore.md +54 -0
  26. package/src/templates/agents/general.md +8 -0
  27. package/src/templates/agents/keystone-architect.md +54 -0
  28. package/src/templates/agents/my-agent.md +3 -0
  29. package/src/templates/agents/summarizer.md +28 -0
  30. package/src/templates/agents/test-agent.md +10 -0
  31. package/src/templates/approval-process.yaml +36 -0
  32. package/src/templates/basic-inputs.yaml +19 -0
  33. package/src/templates/basic-shell.yaml +20 -0
  34. package/src/templates/batch-processor.yaml +43 -0
  35. package/src/templates/cleanup-finally.yaml +22 -0
  36. package/src/templates/composition-child.yaml +13 -0
  37. package/src/templates/composition-parent.yaml +14 -0
  38. package/src/templates/data-pipeline.yaml +38 -0
  39. package/src/templates/full-feature-demo.yaml +64 -0
  40. package/src/templates/human-interaction.yaml +12 -0
  41. package/src/templates/invalid.yaml +5 -0
  42. package/src/templates/llm-agent.yaml +8 -0
  43. package/src/templates/loop-parallel.yaml +37 -0
  44. package/src/templates/retry-policy.yaml +36 -0
  45. package/src/templates/scaffold-feature.yaml +48 -0
  46. package/src/templates/state.db +0 -0
  47. package/src/templates/state.db-shm +0 -0
  48. package/src/templates/state.db-wal +0 -0
  49. package/src/templates/stop-watch.yaml +17 -0
  50. package/src/templates/workflow.db +0 -0
  51. package/src/utils/config-loader.test.ts +2 -2
@@ -0,0 +1,54 @@
1
+ ---
2
+ name: keystone-architect
3
+ description: "Expert at designing Keystone workflows and agents"
4
+ model: gpt-4o
5
+ ---
6
+
7
+ # Role
8
+ You are the Keystone Architect. Your goal is to design and generate high-quality Keystone workflows (.yaml) and agents (.md). You understand the underlying schema and expression syntax perfectly.
9
+
10
+ # Knowledge Base
11
+
12
+ ## Workflow Schema (.yaml)
13
+ - **name**: Unique identifier for the workflow.
14
+ - **inputs**: Map of `{ type: string, default: any, description: string }` under the `inputs` key.
15
+ - **outputs**: Map of expressions (e.g., `${{ steps.id.output }}`) under the `outputs` key.
16
+ - **steps**: Array of step objects. Each step MUST have an `id` and a `type`:
17
+ - **shell**: `{ id, type: 'shell', run, dir, env, transform }`
18
+ - **llm**: `{ id, type: 'llm', agent, prompt, schema }`
19
+ - **workflow**: `{ id, type: 'workflow', path, inputs }`
20
+ - **file**: `{ id, type: 'file', path, op: 'read'|'write'|'append', content }`
21
+ - **request**: `{ id, type: 'request', url, method, body, headers }`
22
+ - **human**: `{ id, type: 'human', message, inputType: 'confirm'|'text' }`
23
+ - **sleep**: `{ id, type: 'sleep', duration }`
24
+ - **Common Step Fields**: `needs` (array of IDs), `if` (expression), `retry`, `foreach`, `concurrency`.
25
+ - **IMPORTANT**: Steps run in **parallel** by default. To ensure sequential execution, a step must explicitly list the previous step's ID in its `needs` array.
26
+
27
+ ## Agent Schema (.md)
28
+ Markdown files with YAML frontmatter:
29
+ - **name**: Agent name.
30
+ - **model**: (Optional) e.g., `gpt-4o`, `claude-sonnet-4.5`.
31
+ - **tools**: Array of `{ name, parameters, execution }` where `execution` is a standard Step object.
32
+ - **Body**: The Markdown body is the `systemPrompt`.
33
+
34
+ ## Expression Syntax
35
+ - `${{ inputs.name }}`
36
+ - `${{ steps.id.output }}`
37
+ - `${{ steps.id.status }}`
38
+ - `${{ args.paramName }}` (used inside agent tools)
39
+ - Standard JS-like expressions: `${{ steps.count > 0 ? 'yes' : 'no' }}`
40
+
41
+ # Output Instructions
42
+ When asked to design a feature:
43
+ 1. Provide the necessary Keystone files (Workflows and Agents).
44
+ 2. **IMPORTANT**: Return ONLY a raw JSON object. Do not include markdown code blocks, preamble, or postamble.
45
+
46
+ The JSON structure must be:
47
+ {
48
+ "files": [
49
+ {
50
+ "path": "workflows/...",
51
+ "content": "..."
52
+ }
53
+ ]
54
+ }
@@ -0,0 +1,3 @@
1
+ ---
2
+ name: my-agent
3
+ ---
@@ -0,0 +1,28 @@
1
+ ---
2
+ name: summarizer
3
+ description: "Summarizes text content"
4
+ model: gpt-4o
5
+ tools:
6
+ - name: read_file
7
+ description: "Read the contents of a file"
8
+ parameters:
9
+ type: object
10
+ properties:
11
+ filepath:
12
+ type: string
13
+ description: "The path to the file to read"
14
+ required: ["filepath"]
15
+ execution:
16
+ type: file
17
+ op: read
18
+ path: "${{ args.filepath }}"
19
+ ---
20
+
21
+ # Identity
22
+ You are a concise summarizer. Your goal is to extract the key points from any text and present them in a clear, brief format.
23
+
24
+ ## Guidelines
25
+ - Focus on the main ideas and key takeaways
26
+ - Keep summaries under 3-5 sentences unless more detail is explicitly requested
27
+ - Use clear, simple language
28
+ - Maintain objectivity and accuracy
@@ -0,0 +1,10 @@
1
+ ---
2
+ name: test-agent
3
+ model: gpt-4
4
+ tools:
5
+ - name: test-tool
6
+ execution:
7
+ type: shell
8
+ run: echo "tool executed with ${{ args.val }}"
9
+ ---
10
+ You are a test agent.
@@ -0,0 +1,36 @@
1
+ name: approval-process
2
+ description: "A workflow demonstrating human-in-the-loop and conditional logic"
3
+
4
+ inputs:
5
+ request_details: { type: string, default: "Access to production database" }
6
+
7
+ steps:
8
+ - id: request_approval
9
+ type: human
10
+ message: "Do you approve the following request: ${{ inputs.request_details }}?"
11
+ inputType: confirm
12
+
13
+ - id: log_approval
14
+ type: shell
15
+ if: ${{ steps.request_approval.output == true }}
16
+ run: echo "Request approved. Proceeding with implementation."
17
+ needs: [request_approval]
18
+
19
+ - id: log_rejection
20
+ type: shell
21
+ if: ${{ steps.request_approval.output == false }}
22
+ run: echo "Request rejected. Notifying requester."
23
+ needs: [request_approval]
24
+
25
+ - id: get_rejection_reason
26
+ type: human
27
+ if: ${{ steps.request_approval.output == false }}
28
+ message: "Please provide a reason for rejection:"
29
+ inputType: text
30
+ needs: [request_approval]
31
+
32
+ - id: finalize_rejection
33
+ type: shell
34
+ if: ${{ steps.request_approval.output == false }}
35
+ run: echo "Rejection reason - ${{ steps.get_rejection_reason.output }}"
36
+ needs: [get_rejection_reason]
@@ -0,0 +1,19 @@
1
+ name: basic-inputs
2
+ description: "A simple workflow that greets a user with optional repetition"
3
+ inputs:
4
+ user_name:
5
+ type: string
6
+ description: "The name of the person to greet"
7
+ default: "World"
8
+ count:
9
+ type: number
10
+ description: "Number of times to greet"
11
+ default: 1
12
+
13
+ steps:
14
+ - id: hello
15
+ type: shell
16
+ run: |
17
+ for i in $(seq 1 ${{ inputs.count }}); do
18
+ echo "Hello, ${{ escape(inputs.user_name) }}! (Attempt $i)"
19
+ done
@@ -0,0 +1,20 @@
1
+ name: basic-shell
2
+ description: "A simple example workflow demonstrating basic features"
3
+
4
+ inputs:
5
+ greeting: { type: string, default: "Hello" }
6
+ name: { type: string, default: "World" }
7
+
8
+ outputs:
9
+ message: ${{ steps.create_message.output }}
10
+
11
+ steps:
12
+ - id: create_message
13
+ type: shell
14
+ run: echo "${{ escape(inputs.greeting) }}, ${{ escape(inputs.name) }}!"
15
+ transform: "stdout.trim()"
16
+
17
+ - id: print_message
18
+ type: shell
19
+ needs: [create_message]
20
+ run: echo "Generated message - ${{ steps.create_message.output }}"
@@ -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,5 @@
1
+ name: invalid-workflow
2
+ steps:
3
+ - id: step1
4
+ type: shell
5
+ # Missing 'run' property
@@ -0,0 +1,8 @@
1
+ name: llm-agent
2
+ description: "Test LLM step"
3
+
4
+ steps:
5
+ - id: ask_llm
6
+ type: llm
7
+ agent: summarizer
8
+ prompt: "Hello, who are you?"
@@ -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, afterEach } from 'bun:test';
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(() => {