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
@@ -1,8 +1,8 @@
1
1
  import { afterEach, beforeEach, describe, expect, it, mock, spyOn } from 'bun:test';
2
- import { MCPClient } from './mcp-client';
3
2
  import * as child_process from 'node:child_process';
4
3
  import { EventEmitter } from 'node:events';
5
4
  import { Readable, Writable } from 'node:stream';
5
+ import { MCPClient } from './mcp-client';
6
6
 
7
7
  interface MockProcess extends EventEmitter {
8
8
  stdout: Readable;
@@ -11,112 +11,228 @@ interface MockProcess extends EventEmitter {
11
11
  }
12
12
 
13
13
  describe('MCPClient', () => {
14
- let mockProcess: MockProcess;
15
- let spawnSpy: ReturnType<typeof spyOn>;
14
+ describe('Local Transport', () => {
15
+ let mockProcess: MockProcess;
16
+ let spawnSpy: ReturnType<typeof spyOn>;
17
+
18
+ beforeEach(() => {
19
+ const emitter = new EventEmitter();
20
+ const stdout = new Readable({
21
+ read() {},
22
+ });
23
+ const stdin = new Writable({
24
+ write(_chunk, _encoding, callback) {
25
+ callback();
26
+ },
27
+ });
28
+ const kill = mock(() => {});
16
29
 
17
- beforeEach(() => {
18
- const emitter = new EventEmitter();
19
- const stdout = new Readable({
20
- read() {},
30
+ mockProcess = Object.assign(emitter, { stdout, stdin, kill }) as unknown as MockProcess;
31
+
32
+ spawnSpy = spyOn(child_process, 'spawn').mockReturnValue(
33
+ mockProcess as unknown as child_process.ChildProcess
34
+ );
21
35
  });
22
- const stdin = new Writable({
23
- write(_chunk, _encoding, callback) {
24
- callback();
25
- },
36
+
37
+ afterEach(() => {
38
+ spawnSpy.mockRestore();
26
39
  });
27
- const kill = mock(() => {});
28
40
 
29
- mockProcess = Object.assign(emitter, { stdout, stdin, kill }) as unknown as MockProcess;
41
+ it('should initialize correctly', async () => {
42
+ const client = await MCPClient.createLocal('node', ['server.js']);
43
+ const initPromise = client.initialize();
44
+
45
+ // Simulate server response
46
+ mockProcess.stdout.push(
47
+ `${JSON.stringify({
48
+ jsonrpc: '2.0',
49
+ id: 0,
50
+ result: { protocolVersion: '2024-11-05' },
51
+ })}\n`
52
+ );
53
+
54
+ const response = await initPromise;
55
+ expect(response.result?.protocolVersion).toBe('2024-11-05');
56
+ expect(spawnSpy).toHaveBeenCalledWith('node', ['server.js'], expect.any(Object));
57
+ });
30
58
 
31
- spawnSpy = spyOn(child_process, 'spawn').mockReturnValue(
32
- mockProcess as unknown as child_process.ChildProcess
33
- );
34
- });
59
+ it('should list tools', async () => {
60
+ const client = await MCPClient.createLocal('node');
61
+ const listPromise = client.listTools();
62
+
63
+ mockProcess.stdout.push(
64
+ `${JSON.stringify({
65
+ jsonrpc: '2.0',
66
+ id: 0,
67
+ result: {
68
+ tools: [{ name: 'test-tool', inputSchema: {} }],
69
+ },
70
+ })}\n`
71
+ );
72
+
73
+ const tools = await listPromise;
74
+ expect(tools).toHaveLength(1);
75
+ expect(tools[0].name).toBe('test-tool');
76
+ });
35
77
 
36
- afterEach(() => {
37
- spawnSpy.mockRestore();
38
- });
78
+ it('should call tool', async () => {
79
+ const client = await MCPClient.createLocal('node');
80
+ const callPromise = client.callTool('my-tool', { arg: 1 });
39
81
 
40
- it('should initialize correctly', async () => {
41
- const client = new MCPClient('node', ['server.js']);
42
- const initPromise = client.initialize();
43
-
44
- // Simulate server response
45
- mockProcess.stdout.push(
46
- `${JSON.stringify({
47
- jsonrpc: '2.0',
48
- id: 0,
49
- result: { protocolVersion: '2024-11-05' },
50
- })}\n`
51
- );
52
-
53
- const response = await initPromise;
54
- expect(response.result?.protocolVersion).toBe('2024-11-05');
55
- expect(spawnSpy).toHaveBeenCalledWith('node', ['server.js'], expect.any(Object));
56
- });
82
+ mockProcess.stdout.push(
83
+ `${JSON.stringify({
84
+ jsonrpc: '2.0',
85
+ id: 0,
86
+ result: { content: [{ type: 'text', text: 'success' }] },
87
+ })}\n`
88
+ );
57
89
 
58
- it('should list tools', async () => {
59
- const client = new MCPClient('node');
60
- const listPromise = client.listTools();
90
+ const result = await callPromise;
91
+ expect((result as { content: Array<{ text: string }> }).content[0].text).toBe('success');
92
+ });
61
93
 
62
- mockProcess.stdout.push(
63
- `${JSON.stringify({
64
- jsonrpc: '2.0',
65
- id: 0,
66
- result: {
67
- tools: [{ name: 'test-tool', inputSchema: {} }],
68
- },
69
- })}\n`
70
- );
94
+ it('should handle tool call error', async () => {
95
+ const client = await MCPClient.createLocal('node');
96
+ const callPromise = client.callTool('my-tool', {});
71
97
 
72
- const tools = await listPromise;
73
- expect(tools).toHaveLength(1);
74
- expect(tools[0].name).toBe('test-tool');
75
- });
98
+ mockProcess.stdout.push(
99
+ `${JSON.stringify({
100
+ jsonrpc: '2.0',
101
+ id: 0,
102
+ error: { code: -32000, message: 'Tool error' },
103
+ })}\n`
104
+ );
105
+
106
+ await expect(callPromise).rejects.toThrow(/MCP tool call failed/);
107
+ });
76
108
 
77
- it('should call tool', async () => {
78
- const client = new MCPClient('node');
79
- const callPromise = client.callTool('my-tool', { arg: 1 });
109
+ it('should timeout on request', async () => {
110
+ // Set a very short timeout for testing
111
+ const client = await MCPClient.createLocal('node', [], {}, 10);
112
+ const requestPromise = client.initialize();
80
113
 
81
- mockProcess.stdout.push(
82
- `${JSON.stringify({
83
- jsonrpc: '2.0',
84
- id: 0,
85
- result: { content: [{ type: 'text', text: 'success' }] },
86
- })}\n`
87
- );
114
+ // Don't push any response to stdout
115
+ await expect(requestPromise).rejects.toThrow(/MCP request timeout/);
116
+ });
88
117
 
89
- const result = await callPromise;
90
- expect((result as { content: Array<{ text: string }> }).content[0].text).toBe('success');
118
+ it('should stop the process', async () => {
119
+ const client = await MCPClient.createLocal('node');
120
+ client.stop();
121
+ expect(mockProcess.kill).toHaveBeenCalled();
122
+ });
91
123
  });
92
124
 
93
- it('should handle tool call error', async () => {
94
- const client = new MCPClient('node');
95
- const callPromise = client.callTool('my-tool', {});
125
+ describe('SSE Transport', () => {
126
+ it('should connect and receive endpoint', async () => {
127
+ let controller: ReadableStreamDefaultController;
128
+ const stream = new ReadableStream({
129
+ start(c) {
130
+ controller = c;
131
+ controller.enqueue(new TextEncoder().encode('event: endpoint\ndata: /endpoint\n\n'));
132
+ },
133
+ });
134
+
135
+ const fetchMock = spyOn(global, 'fetch').mockImplementation((url) => {
136
+ if (url === 'http://localhost:8080/sse') {
137
+ return Promise.resolve(new Response(stream));
138
+ }
139
+ return Promise.resolve(new Response(JSON.stringify({ ok: true })));
140
+ });
141
+
142
+ const clientPromise = MCPClient.createRemote('http://localhost:8080/sse');
143
+
144
+ const client = await clientPromise;
145
+ expect(client).toBeDefined();
146
+
147
+ const initPromise = client.initialize();
148
+
149
+ // Simulate message event (response from server)
150
+ if (controller) {
151
+ controller.enqueue(
152
+ new TextEncoder().encode(
153
+ `data: ${JSON.stringify({
154
+ jsonrpc: '2.0',
155
+ id: 0,
156
+ result: { protocolVersion: '2024-11-05' },
157
+ })}\n\n`
158
+ )
159
+ );
160
+ }
161
+
162
+ const response = await initPromise;
163
+ expect(response.result?.protocolVersion).toBe('2024-11-05');
164
+ expect(fetchMock).toHaveBeenCalledWith('http://localhost:8080/endpoint', expect.any(Object));
165
+
166
+ client.stop();
167
+ fetchMock.mockRestore();
168
+ });
96
169
 
97
- mockProcess.stdout.push(
98
- `${JSON.stringify({
99
- jsonrpc: '2.0',
100
- id: 0,
101
- error: { code: -32000, message: 'Tool error' },
102
- })}\n`
103
- );
170
+ it('should handle SSE with multiple events and chunked data', async () => {
171
+ let controller: ReadableStreamDefaultController;
172
+ const stream = new ReadableStream({
173
+ start(c) {
174
+ controller = c;
175
+ // Send endpoint event
176
+ controller.enqueue(new TextEncoder().encode('event: endpoint\n'));
177
+ controller.enqueue(new TextEncoder().encode('data: /endpoint\n\n'));
178
+ },
179
+ });
180
+
181
+ const fetchMock = spyOn(global, 'fetch').mockImplementation((url) => {
182
+ if (url === 'http://localhost:8080/sse') {
183
+ return Promise.resolve(new Response(stream));
184
+ }
185
+ return Promise.resolve(new Response(JSON.stringify({ ok: true })));
186
+ });
187
+
188
+ const client = await MCPClient.createRemote('http://localhost:8080/sse');
189
+
190
+ // We can't easily hook into onMessage without reaching into internals
191
+ // Instead, we'll test that initialize resolves correctly when the response arrives
192
+ const initPromise = client.initialize();
193
+
194
+ // Enqueue data in chunks
195
+ if (controller) {
196
+ controller.enqueue(new TextEncoder().encode('data: {"jsonrpc":"2.0","id":0,'));
197
+ controller.enqueue(
198
+ new TextEncoder().encode('"result":{"protocolVersion":"2024-11-05"}}\n\n')
199
+ );
200
+
201
+ // Send another event (just to test dispatching doesn't crash)
202
+ controller.enqueue(
203
+ new TextEncoder().encode(
204
+ 'event: message\ndata: {"jsonrpc":"2.0","id":99,"result":"ignored"}\n\n'
205
+ )
206
+ );
207
+
208
+ // Send empty line
209
+ controller.enqueue(new TextEncoder().encode('\n'));
210
+
211
+ controller.close();
212
+ }
213
+
214
+ const response = await initPromise;
215
+ expect(response.result?.protocolVersion).toBe('2024-11-05');
216
+
217
+ client.stop();
218
+ fetchMock.mockRestore();
219
+ });
104
220
 
105
- await expect(callPromise).rejects.toThrow(/MCP tool call failed/);
106
- });
221
+ it('should handle SSE connection failure', async () => {
222
+ const fetchMock = spyOn(global, 'fetch').mockImplementation(() =>
223
+ Promise.resolve(
224
+ new Response(null, {
225
+ status: 500,
226
+ statusText: 'Internal Server Error',
227
+ })
228
+ )
229
+ );
107
230
 
108
- it('should timeout on request', async () => {
109
- // Set a very short timeout for testing
110
- const client = new MCPClient('node', [], {}, 10);
111
- const requestPromise = client.initialize();
231
+ const clientPromise = MCPClient.createRemote('http://localhost:8080/sse');
112
232
 
113
- // Don't push any response to stdout
114
- await expect(requestPromise).rejects.toThrow(/MCP request timeout/);
115
- });
233
+ await expect(clientPromise).rejects.toThrow(/SSE connection failed: 500/);
116
234
 
117
- it('should stop the process', () => {
118
- const client = new MCPClient('node');
119
- client.stop();
120
- expect(mockProcess.kill).toHaveBeenCalled();
235
+ fetchMock.mockRestore();
236
+ });
121
237
  });
122
238
  });