keystone-cli 0.1.1 → 0.3.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 +69 -16
- package/package.json +14 -3
- package/src/cli.ts +183 -84
- package/src/db/workflow-db.ts +0 -7
- package/src/expression/evaluator.test.ts +46 -0
- package/src/expression/evaluator.ts +36 -0
- package/src/parser/agent-parser.test.ts +10 -0
- package/src/parser/agent-parser.ts +13 -5
- package/src/parser/config-schema.ts +24 -5
- package/src/parser/schema.ts +1 -1
- package/src/parser/workflow-parser.ts +5 -9
- package/src/runner/llm-adapter.test.ts +0 -8
- package/src/runner/llm-adapter.ts +33 -10
- package/src/runner/llm-executor.test.ts +230 -96
- package/src/runner/llm-executor.ts +9 -4
- package/src/runner/mcp-client.test.ts +204 -88
- package/src/runner/mcp-client.ts +349 -22
- package/src/runner/mcp-manager.test.ts +73 -15
- package/src/runner/mcp-manager.ts +84 -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.test.ts +2 -2
- package/src/runner/step-executor.ts +31 -16
- package/src/runner/tool-integration.test.ts +21 -14
- package/src/runner/workflow-runner.ts +34 -7
- 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/auth-manager.test.ts +86 -0
- package/src/utils/auth-manager.ts +89 -0
- package/src/utils/config-loader.test.ts +32 -2
- package/src/utils/config-loader.ts +11 -1
- package/src/utils/mermaid.test.ts +27 -3
|
@@ -78,10 +78,46 @@ export class ExpressionEvaluator {
|
|
|
78
78
|
// Extract the expression content between ${{ and }}
|
|
79
79
|
const expr = match.replace(/^\$\{\{\s*|\s*\}\}$/g, '');
|
|
80
80
|
const result = ExpressionEvaluator.evaluateExpression(expr, context);
|
|
81
|
+
|
|
82
|
+
if (result === null || result === undefined) {
|
|
83
|
+
return '';
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (typeof result === 'object' && result !== null) {
|
|
87
|
+
// Special handling for shell command results to avoid [object Object] or JSON in commands
|
|
88
|
+
if (
|
|
89
|
+
'stdout' in result &&
|
|
90
|
+
'exitCode' in result &&
|
|
91
|
+
typeof (result as Record<string, unknown>).stdout === 'string'
|
|
92
|
+
) {
|
|
93
|
+
return (result as Record<string, unknown>).stdout.trim();
|
|
94
|
+
}
|
|
95
|
+
return JSON.stringify(result, null, 2);
|
|
96
|
+
}
|
|
97
|
+
|
|
81
98
|
return String(result);
|
|
82
99
|
});
|
|
83
100
|
}
|
|
84
101
|
|
|
102
|
+
/**
|
|
103
|
+
* Evaluate a string and ensure the result is a string.
|
|
104
|
+
* Objects and arrays are stringified to JSON.
|
|
105
|
+
* null and undefined return an empty string.
|
|
106
|
+
*/
|
|
107
|
+
static evaluateString(template: string, context: ExpressionContext): string {
|
|
108
|
+
const result = ExpressionEvaluator.evaluate(template, context);
|
|
109
|
+
|
|
110
|
+
if (result === null || result === undefined) {
|
|
111
|
+
return '';
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (typeof result === 'string') {
|
|
115
|
+
return result;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return JSON.stringify(result, null, 2);
|
|
119
|
+
}
|
|
120
|
+
|
|
85
121
|
/**
|
|
86
122
|
* Evaluate a single expression (without the ${{ }} wrapper)
|
|
87
123
|
* This is public to support transform expressions in shell steps
|
|
@@ -63,6 +63,16 @@ tools:
|
|
|
63
63
|
expect(agent.tools[0].execution.id).toBe('tool-tool-without-id');
|
|
64
64
|
});
|
|
65
65
|
|
|
66
|
+
it('should parse single-line frontmatter', () => {
|
|
67
|
+
const agentContent = '---name: single-line---\nPrompt';
|
|
68
|
+
const filePath = join(tempDir, 'single-line.md');
|
|
69
|
+
writeFileSync(filePath, agentContent);
|
|
70
|
+
|
|
71
|
+
const agent = parseAgent(filePath);
|
|
72
|
+
expect(agent.name).toBe('single-line');
|
|
73
|
+
expect(agent.systemPrompt).toBe('Prompt');
|
|
74
|
+
});
|
|
75
|
+
|
|
66
76
|
it('should throw error for missing frontmatter', () => {
|
|
67
77
|
const agentContent = 'Just some content without frontmatter';
|
|
68
78
|
const filePath = join(tempDir, 'invalid-format.md');
|
|
@@ -6,7 +6,8 @@ import { type Agent, AgentSchema } from './schema';
|
|
|
6
6
|
|
|
7
7
|
export function parseAgent(filePath: string): Agent {
|
|
8
8
|
const content = readFileSync(filePath, 'utf8');
|
|
9
|
-
|
|
9
|
+
// Flexible regex to handle both standard and single-line frontmatter
|
|
10
|
+
const match = content.match(/^---[\r\n]*([\s\S]*?)[\r\n]*---(?:\r?\n?([\s\S]*))?$/);
|
|
10
11
|
|
|
11
12
|
if (!match) {
|
|
12
13
|
throw new Error(`Invalid agent format in ${filePath}. Missing frontmatter.`);
|
|
@@ -43,11 +44,18 @@ export function parseAgent(filePath: string): Agent {
|
|
|
43
44
|
return result.data;
|
|
44
45
|
}
|
|
45
46
|
|
|
46
|
-
export function resolveAgentPath(agentName: string): string {
|
|
47
|
-
const possiblePaths = [
|
|
47
|
+
export function resolveAgentPath(agentName: string, baseDir?: string): string {
|
|
48
|
+
const possiblePaths: string[] = [];
|
|
49
|
+
|
|
50
|
+
if (baseDir) {
|
|
51
|
+
possiblePaths.push(join(baseDir, 'agents', `${agentName}.md`));
|
|
52
|
+
possiblePaths.push(join(baseDir, '..', 'agents', `${agentName}.md`));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
possiblePaths.push(
|
|
48
56
|
join(process.cwd(), '.keystone', 'workflows', 'agents', `${agentName}.md`),
|
|
49
|
-
join(homedir(), '.keystone', 'workflows', 'agents', `${agentName}.md`)
|
|
50
|
-
|
|
57
|
+
join(homedir(), '.keystone', 'workflows', 'agents', `${agentName}.md`)
|
|
58
|
+
);
|
|
51
59
|
|
|
52
60
|
for (const path of possiblePaths) {
|
|
53
61
|
if (existsSync(path)) {
|
|
@@ -42,11 +42,30 @@ export const ConfigSchema = z.object({
|
|
|
42
42
|
workflows_directory: z.string().default('workflows'),
|
|
43
43
|
mcp_servers: z
|
|
44
44
|
.record(
|
|
45
|
-
z.
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
45
|
+
z.discriminatedUnion('type', [
|
|
46
|
+
z.object({
|
|
47
|
+
type: z.literal('local').default('local'),
|
|
48
|
+
command: z.string(),
|
|
49
|
+
args: z.array(z.string()).optional(),
|
|
50
|
+
env: z.record(z.string()).optional(),
|
|
51
|
+
url: z.string().url().optional(),
|
|
52
|
+
oauth: z
|
|
53
|
+
.object({
|
|
54
|
+
scope: z.string().optional(),
|
|
55
|
+
})
|
|
56
|
+
.optional(),
|
|
57
|
+
}),
|
|
58
|
+
z.object({
|
|
59
|
+
type: z.literal('remote'),
|
|
60
|
+
url: z.string().url(),
|
|
61
|
+
headers: z.record(z.string()).optional(),
|
|
62
|
+
oauth: z
|
|
63
|
+
.object({
|
|
64
|
+
scope: z.string().optional(),
|
|
65
|
+
})
|
|
66
|
+
.optional(),
|
|
67
|
+
}),
|
|
68
|
+
])
|
|
50
69
|
)
|
|
51
70
|
.default({}),
|
|
52
71
|
});
|
package/src/parser/schema.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
-
import { join } from 'node:path';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
3
|
import * as yaml from 'js-yaml';
|
|
4
4
|
import { z } from 'zod';
|
|
5
5
|
import { ExpressionEvaluator } from '../expression/evaluator.ts';
|
|
@@ -15,6 +15,7 @@ export class WorkflowParser {
|
|
|
15
15
|
const content = readFileSync(path, 'utf-8');
|
|
16
16
|
const raw = yaml.load(content);
|
|
17
17
|
const workflow = WorkflowSchema.parse(raw);
|
|
18
|
+
const workflowDir = dirname(path);
|
|
18
19
|
|
|
19
20
|
// Resolve implicit dependencies from expressions
|
|
20
21
|
WorkflowParser.resolveImplicitDependencies(workflow);
|
|
@@ -23,7 +24,7 @@ export class WorkflowParser {
|
|
|
23
24
|
WorkflowParser.validateDAG(workflow);
|
|
24
25
|
|
|
25
26
|
// Validate agents exist
|
|
26
|
-
WorkflowParser.validateAgents(workflow);
|
|
27
|
+
WorkflowParser.validateAgents(workflow, workflowDir);
|
|
27
28
|
|
|
28
29
|
// Validate finally block
|
|
29
30
|
WorkflowParser.validateFinally(workflow);
|
|
@@ -121,12 +122,12 @@ export class WorkflowParser {
|
|
|
121
122
|
/**
|
|
122
123
|
* Validate that all agents referenced in LLM steps exist
|
|
123
124
|
*/
|
|
124
|
-
private static validateAgents(workflow: Workflow): void {
|
|
125
|
+
private static validateAgents(workflow: Workflow, baseDir?: string): void {
|
|
125
126
|
const allSteps = [...workflow.steps, ...(workflow.finally || [])];
|
|
126
127
|
for (const step of allSteps) {
|
|
127
128
|
if (step.type === 'llm') {
|
|
128
129
|
try {
|
|
129
|
-
resolveAgentPath(step.agent);
|
|
130
|
+
resolveAgentPath(step.agent, baseDir);
|
|
130
131
|
} catch (error) {
|
|
131
132
|
throw new Error(`Agent "${step.agent}" referenced in step "${step.id}" not found.`);
|
|
132
133
|
}
|
|
@@ -180,11 +181,6 @@ export class WorkflowParser {
|
|
|
180
181
|
}
|
|
181
182
|
}
|
|
182
183
|
|
|
183
|
-
// Initialize in-degree
|
|
184
|
-
for (const step of workflow.steps) {
|
|
185
|
-
inDegree.set(step.id, 0);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
184
|
// Calculate in-degree
|
|
189
185
|
// In-degree = number of dependencies a step has
|
|
190
186
|
for (const step of workflow.steps) {
|
|
@@ -268,14 +268,6 @@ describe('CopilotAdapter', () => {
|
|
|
268
268
|
await expect(adapter.chat([])).rejects.toThrow(/GitHub Copilot token not found/);
|
|
269
269
|
spy.mockRestore();
|
|
270
270
|
});
|
|
271
|
-
|
|
272
|
-
it('should throw error if token not found (duplicated)', async () => {
|
|
273
|
-
const spy = spyOn(AuthManager, 'getCopilotToken').mockResolvedValue(undefined);
|
|
274
|
-
|
|
275
|
-
const adapter = new CopilotAdapter();
|
|
276
|
-
await expect(adapter.chat([])).rejects.toThrow(/GitHub Copilot token not found/);
|
|
277
|
-
spy.mockRestore();
|
|
278
|
-
});
|
|
279
271
|
});
|
|
280
272
|
|
|
281
273
|
describe('getAdapter', () => {
|
|
@@ -141,19 +141,42 @@ export class AnthropicAdapter implements LLMAdapter {
|
|
|
141
141
|
role: 'assistant',
|
|
142
142
|
content: [
|
|
143
143
|
...(m.content ? [{ type: 'text' as const, text: m.content }] : []),
|
|
144
|
-
...m.tool_calls.map((tc) =>
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
144
|
+
...m.tool_calls.map((tc) => {
|
|
145
|
+
let input = {};
|
|
146
|
+
try {
|
|
147
|
+
input =
|
|
148
|
+
typeof tc.function.arguments === 'string'
|
|
149
|
+
? JSON.parse(tc.function.arguments)
|
|
150
|
+
: tc.function.arguments;
|
|
151
|
+
} catch (e) {
|
|
152
|
+
console.error(`Failed to parse tool arguments: ${tc.function.arguments}`);
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
type: 'tool_use' as const,
|
|
156
|
+
id: tc.id,
|
|
157
|
+
name: tc.function.name,
|
|
158
|
+
input,
|
|
159
|
+
};
|
|
160
|
+
}),
|
|
150
161
|
],
|
|
151
162
|
});
|
|
152
163
|
} else {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
164
|
+
const role = m.role as 'user' | 'assistant';
|
|
165
|
+
const lastMsg = anthropicMessages[anthropicMessages.length - 1];
|
|
166
|
+
|
|
167
|
+
if (
|
|
168
|
+
lastMsg &&
|
|
169
|
+
lastMsg.role === role &&
|
|
170
|
+
typeof lastMsg.content === 'string' &&
|
|
171
|
+
typeof m.content === 'string'
|
|
172
|
+
) {
|
|
173
|
+
lastMsg.content += `\n\n${m.content}`;
|
|
174
|
+
} else {
|
|
175
|
+
anthropicMessages.push({
|
|
176
|
+
role,
|
|
177
|
+
content: m.content || '',
|
|
178
|
+
});
|
|
179
|
+
}
|
|
157
180
|
}
|
|
158
181
|
}
|
|
159
182
|
|