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.
Files changed (57) hide show
  1. package/README.md +69 -16
  2. package/package.json +14 -3
  3. package/src/cli.ts +183 -84
  4. package/src/db/workflow-db.ts +0 -7
  5. package/src/expression/evaluator.test.ts +46 -0
  6. package/src/expression/evaluator.ts +36 -0
  7. package/src/parser/agent-parser.test.ts +10 -0
  8. package/src/parser/agent-parser.ts +13 -5
  9. package/src/parser/config-schema.ts +24 -5
  10. package/src/parser/schema.ts +1 -1
  11. package/src/parser/workflow-parser.ts +5 -9
  12. package/src/runner/llm-adapter.test.ts +0 -8
  13. package/src/runner/llm-adapter.ts +33 -10
  14. package/src/runner/llm-executor.test.ts +230 -96
  15. package/src/runner/llm-executor.ts +9 -4
  16. package/src/runner/mcp-client.test.ts +204 -88
  17. package/src/runner/mcp-client.ts +349 -22
  18. package/src/runner/mcp-manager.test.ts +73 -15
  19. package/src/runner/mcp-manager.ts +84 -18
  20. package/src/runner/mcp-server.test.ts +4 -1
  21. package/src/runner/mcp-server.ts +25 -11
  22. package/src/runner/shell-executor.ts +3 -3
  23. package/src/runner/step-executor.test.ts +2 -2
  24. package/src/runner/step-executor.ts +31 -16
  25. package/src/runner/tool-integration.test.ts +21 -14
  26. package/src/runner/workflow-runner.ts +34 -7
  27. package/src/templates/agents/explore.md +54 -0
  28. package/src/templates/agents/general.md +8 -0
  29. package/src/templates/agents/keystone-architect.md +54 -0
  30. package/src/templates/agents/my-agent.md +3 -0
  31. package/src/templates/agents/summarizer.md +28 -0
  32. package/src/templates/agents/test-agent.md +10 -0
  33. package/src/templates/approval-process.yaml +36 -0
  34. package/src/templates/basic-inputs.yaml +19 -0
  35. package/src/templates/basic-shell.yaml +20 -0
  36. package/src/templates/batch-processor.yaml +43 -0
  37. package/src/templates/cleanup-finally.yaml +22 -0
  38. package/src/templates/composition-child.yaml +13 -0
  39. package/src/templates/composition-parent.yaml +14 -0
  40. package/src/templates/data-pipeline.yaml +38 -0
  41. package/src/templates/full-feature-demo.yaml +64 -0
  42. package/src/templates/human-interaction.yaml +12 -0
  43. package/src/templates/invalid.yaml +5 -0
  44. package/src/templates/llm-agent.yaml +8 -0
  45. package/src/templates/loop-parallel.yaml +37 -0
  46. package/src/templates/retry-policy.yaml +36 -0
  47. package/src/templates/scaffold-feature.yaml +48 -0
  48. package/src/templates/state.db +0 -0
  49. package/src/templates/state.db-shm +0 -0
  50. package/src/templates/state.db-wal +0 -0
  51. package/src/templates/stop-watch.yaml +17 -0
  52. package/src/templates/workflow.db +0 -0
  53. package/src/utils/auth-manager.test.ts +86 -0
  54. package/src/utils/auth-manager.ts +89 -0
  55. package/src/utils/config-loader.test.ts +32 -2
  56. package/src/utils/config-loader.ts +11 -1
  57. 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
- const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n([\s\S]*))?$/);
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.object({
46
- command: z.string(),
47
- args: z.array(z.string()).optional(),
48
- env: z.record(z.string()).optional(),
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
  });
@@ -3,7 +3,7 @@ import { z } from 'zod';
3
3
  // ===== Input/Output Schema =====
4
4
 
5
5
  const InputSchema = z.object({
6
- type: z.string(),
6
+ type: z.enum(['string', 'number', 'boolean', 'array', 'object']),
7
7
  default: z.any().optional(),
8
8
  description: z.string().optional(),
9
9
  });
@@ -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
- type: 'tool_use' as const,
146
- id: tc.id,
147
- name: tc.function.name,
148
- input: JSON.parse(tc.function.arguments),
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
- anthropicMessages.push({
154
- role: m.role as 'user' | 'assistant',
155
- content: m.content || '',
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