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.
- package/README.md +52 -15
- 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
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, it, mock, spyOn } from 'bun:test';
|
|
2
|
-
import { MCPManager } from './mcp-manager';
|
|
3
|
-
import { MCPClient } from './mcp-client';
|
|
4
|
-
import { ConfigLoader } from '../utils/config-loader';
|
|
5
2
|
import * as child_process from 'node:child_process';
|
|
6
|
-
import { Readable, Writable } from 'node:stream';
|
|
7
3
|
import { EventEmitter } from 'node:events';
|
|
4
|
+
import { Readable, Writable } from 'node:stream';
|
|
5
|
+
import { ConfigLoader } from '../utils/config-loader';
|
|
6
|
+
import { MCPClient, type MCPResponse } from './mcp-client';
|
|
7
|
+
import { MCPManager } from './mcp-manager';
|
|
8
|
+
|
|
9
|
+
import type { Config } from '../parser/config-schema';
|
|
8
10
|
|
|
9
11
|
describe('MCPManager', () => {
|
|
10
12
|
let spawnSpy: ReturnType<typeof spyOn>;
|
|
@@ -15,7 +17,7 @@ describe('MCPManager', () => {
|
|
|
15
17
|
const mockProcess = Object.assign(new EventEmitter(), {
|
|
16
18
|
stdout: new Readable({ read() {} }),
|
|
17
19
|
stdin: new Writable({
|
|
18
|
-
write(_chunk, _encoding, cb) {
|
|
20
|
+
write(_chunk, _encoding, cb: (error?: Error | null) => void) {
|
|
19
21
|
cb();
|
|
20
22
|
},
|
|
21
23
|
}),
|
|
@@ -35,6 +37,7 @@ describe('MCPManager', () => {
|
|
|
35
37
|
ConfigLoader.setConfig({
|
|
36
38
|
mcp_servers: {
|
|
37
39
|
'test-server': {
|
|
40
|
+
type: 'local',
|
|
38
41
|
command: 'node',
|
|
39
42
|
args: ['test.js'],
|
|
40
43
|
env: { FOO: 'bar' },
|
|
@@ -43,7 +46,9 @@ describe('MCPManager', () => {
|
|
|
43
46
|
providers: {},
|
|
44
47
|
model_mappings: {},
|
|
45
48
|
default_provider: 'openai',
|
|
46
|
-
|
|
49
|
+
storage: { retention_days: 30 },
|
|
50
|
+
workflows_directory: 'workflows',
|
|
51
|
+
} as unknown as Config);
|
|
47
52
|
|
|
48
53
|
const manager = new MCPManager();
|
|
49
54
|
const servers = manager.getGlobalServers();
|
|
@@ -56,13 +61,16 @@ describe('MCPManager', () => {
|
|
|
56
61
|
ConfigLoader.setConfig({
|
|
57
62
|
mcp_servers: {
|
|
58
63
|
'test-server': {
|
|
64
|
+
type: 'local',
|
|
59
65
|
command: 'node',
|
|
60
66
|
},
|
|
61
67
|
},
|
|
62
68
|
providers: {},
|
|
63
69
|
model_mappings: {},
|
|
64
70
|
default_provider: 'openai',
|
|
65
|
-
|
|
71
|
+
storage: { retention_days: 30 },
|
|
72
|
+
workflows_directory: 'workflows',
|
|
73
|
+
} as unknown as Config);
|
|
66
74
|
|
|
67
75
|
const initSpy = spyOn(MCPClient.prototype, 'initialize').mockResolvedValue({
|
|
68
76
|
result: { protocolVersion: '1.0' },
|
|
@@ -99,6 +107,7 @@ describe('MCPManager', () => {
|
|
|
99
107
|
const manager = new MCPManager();
|
|
100
108
|
const client = await manager.getClient({
|
|
101
109
|
name: 'adhoc',
|
|
110
|
+
type: 'local',
|
|
102
111
|
command: 'node',
|
|
103
112
|
});
|
|
104
113
|
|
|
@@ -114,30 +123,79 @@ describe('MCPManager', () => {
|
|
|
114
123
|
expect(client).toBeUndefined();
|
|
115
124
|
});
|
|
116
125
|
|
|
126
|
+
it('should handle concurrent connection requests without double-spawning', async () => {
|
|
127
|
+
ConfigLoader.setConfig({
|
|
128
|
+
mcp_servers: {
|
|
129
|
+
'concurrent-server': {
|
|
130
|
+
type: 'local',
|
|
131
|
+
command: 'node',
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
providers: {},
|
|
135
|
+
model_mappings: {},
|
|
136
|
+
default_provider: 'openai',
|
|
137
|
+
storage: { retention_days: 30 },
|
|
138
|
+
workflows_directory: 'workflows',
|
|
139
|
+
} as unknown as Config);
|
|
140
|
+
|
|
141
|
+
// Mock initialize to take some time
|
|
142
|
+
let initCalls = 0;
|
|
143
|
+
const initSpy = spyOn(MCPClient.prototype, 'initialize').mockImplementation(async () => {
|
|
144
|
+
initCalls++;
|
|
145
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
146
|
+
return {
|
|
147
|
+
result: { protocolVersion: '1.0' },
|
|
148
|
+
jsonrpc: '2.0',
|
|
149
|
+
id: 0,
|
|
150
|
+
} as MCPResponse;
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const manager = new MCPManager();
|
|
154
|
+
|
|
155
|
+
// Fire off multiple requests concurrently
|
|
156
|
+
const p1 = manager.getClient('concurrent-server');
|
|
157
|
+
const p2 = manager.getClient('concurrent-server');
|
|
158
|
+
const p3 = manager.getClient('concurrent-server');
|
|
159
|
+
|
|
160
|
+
const [c1, c2, c3] = await Promise.all([p1, p2, p3]);
|
|
161
|
+
|
|
162
|
+
expect(c1).toBeDefined();
|
|
163
|
+
expect(c1).toBe(c2);
|
|
164
|
+
expect(c1).toBe(c3);
|
|
165
|
+
expect(initCalls).toBe(1); // Crucial: only one initialization
|
|
166
|
+
|
|
167
|
+
initSpy.mockRestore();
|
|
168
|
+
});
|
|
169
|
+
|
|
117
170
|
it('should handle connection failure', async () => {
|
|
118
171
|
ConfigLoader.setConfig({
|
|
119
172
|
mcp_servers: {
|
|
120
173
|
'fail-server': {
|
|
174
|
+
type: 'local',
|
|
121
175
|
command: 'fail',
|
|
122
176
|
},
|
|
123
177
|
},
|
|
124
178
|
providers: {},
|
|
125
179
|
model_mappings: {},
|
|
126
180
|
default_provider: 'openai',
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
181
|
+
storage: { retention_days: 30 },
|
|
182
|
+
workflows_directory: 'workflows',
|
|
183
|
+
} as unknown as Config);
|
|
184
|
+
|
|
185
|
+
const createLocalSpy = spyOn(MCPClient, 'createLocal').mockImplementation(
|
|
186
|
+
async (_cmd: string) => {
|
|
187
|
+
const client = Object.create(MCPClient.prototype);
|
|
188
|
+
spyOn(client, 'initialize').mockRejectedValue(new Error('Connection failed'));
|
|
189
|
+
spyOn(client, 'stop').mockReturnValue(undefined);
|
|
190
|
+
return client;
|
|
191
|
+
}
|
|
131
192
|
);
|
|
132
|
-
const stopSpy = spyOn(MCPClient.prototype, 'stop').mockReturnValue(undefined);
|
|
133
193
|
|
|
134
194
|
const manager = new MCPManager();
|
|
135
195
|
const client = await manager.getClient('fail-server');
|
|
136
196
|
|
|
137
197
|
expect(client).toBeUndefined();
|
|
138
|
-
expect(stopSpy).toHaveBeenCalled();
|
|
139
198
|
|
|
140
|
-
|
|
141
|
-
stopSpy.mockRestore();
|
|
199
|
+
createLocalSpy.mockRestore();
|
|
142
200
|
});
|
|
143
201
|
});
|
|
@@ -1,16 +1,20 @@
|
|
|
1
|
-
import { MCPClient } from './mcp-client';
|
|
2
1
|
import { ConfigLoader } from '../utils/config-loader';
|
|
2
|
+
import { MCPClient } from './mcp-client';
|
|
3
3
|
import type { Logger } from './workflow-runner';
|
|
4
4
|
|
|
5
5
|
export interface MCPServerConfig {
|
|
6
6
|
name: string;
|
|
7
|
-
|
|
7
|
+
type?: 'local' | 'remote';
|
|
8
|
+
command?: string;
|
|
8
9
|
args?: string[];
|
|
9
10
|
env?: Record<string, string>;
|
|
11
|
+
url?: string;
|
|
12
|
+
headers?: Record<string, string>;
|
|
10
13
|
}
|
|
11
14
|
|
|
12
15
|
export class MCPManager {
|
|
13
16
|
private clients: Map<string, MCPClient> = new Map();
|
|
17
|
+
private connectionPromises: Map<string, Promise<MCPClient | undefined>> = new Map();
|
|
14
18
|
private sharedServers: Map<string, MCPServerConfig> = new Map();
|
|
15
19
|
|
|
16
20
|
constructor() {
|
|
@@ -23,10 +27,8 @@ export class MCPManager {
|
|
|
23
27
|
for (const [name, server] of Object.entries(config.mcp_servers)) {
|
|
24
28
|
this.sharedServers.set(name, {
|
|
25
29
|
name,
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
env: server.env,
|
|
29
|
-
});
|
|
30
|
+
...server,
|
|
31
|
+
} as MCPServerConfig);
|
|
30
32
|
}
|
|
31
33
|
}
|
|
32
34
|
}
|
|
@@ -49,23 +51,47 @@ export class MCPManager {
|
|
|
49
51
|
}
|
|
50
52
|
|
|
51
53
|
const key = this.getServerKey(config);
|
|
54
|
+
|
|
55
|
+
// Check if we already have a client
|
|
52
56
|
if (this.clients.has(key)) {
|
|
53
57
|
return this.clients.get(key);
|
|
54
58
|
}
|
|
55
59
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
await client.initialize();
|
|
60
|
-
this.clients.set(key, client);
|
|
61
|
-
return client;
|
|
62
|
-
} catch (error) {
|
|
63
|
-
logger.error(
|
|
64
|
-
` ā Failed to connect to MCP server ${config.name}: ${error instanceof Error ? error.message : String(error)}`
|
|
65
|
-
);
|
|
66
|
-
client.stop();
|
|
67
|
-
return undefined;
|
|
60
|
+
// Check if we are already connecting
|
|
61
|
+
if (this.connectionPromises.has(key)) {
|
|
62
|
+
return this.connectionPromises.get(key);
|
|
68
63
|
}
|
|
64
|
+
|
|
65
|
+
// Start a new connection and cache the promise
|
|
66
|
+
const connectionPromise = (async () => {
|
|
67
|
+
logger.log(` š Connecting to MCP server: ${config.name} (${config.type || 'local'})`);
|
|
68
|
+
|
|
69
|
+
let client: MCPClient;
|
|
70
|
+
try {
|
|
71
|
+
if (config.type === 'remote') {
|
|
72
|
+
if (!config.url) throw new Error('Remote MCP server missing URL');
|
|
73
|
+
client = await MCPClient.createRemote(config.url, config.headers || {});
|
|
74
|
+
} else {
|
|
75
|
+
if (!config.command) throw new Error('Local MCP server missing command');
|
|
76
|
+
client = await MCPClient.createLocal(config.command, config.args || [], config.env || {});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
await client.initialize();
|
|
80
|
+
this.clients.set(key, client);
|
|
81
|
+
return client;
|
|
82
|
+
} catch (error) {
|
|
83
|
+
logger.error(
|
|
84
|
+
` ā Failed to connect to MCP server ${config.name}: ${error instanceof Error ? error.message : String(error)}`
|
|
85
|
+
);
|
|
86
|
+
return undefined;
|
|
87
|
+
} finally {
|
|
88
|
+
// Remove promise from cache once settled
|
|
89
|
+
this.connectionPromises.delete(key);
|
|
90
|
+
}
|
|
91
|
+
})();
|
|
92
|
+
|
|
93
|
+
this.connectionPromises.set(key, connectionPromise);
|
|
94
|
+
return connectionPromise;
|
|
69
95
|
}
|
|
70
96
|
|
|
71
97
|
private getServerKey(config: MCPServerConfig): string {
|
|
@@ -219,7 +219,7 @@ describe('MCPServer', () => {
|
|
|
219
219
|
const writeSpy = spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
220
220
|
const consoleSpy = spyOn(console, 'error').mockImplementation(() => {});
|
|
221
221
|
|
|
222
|
-
|
|
222
|
+
const startPromise = server.start();
|
|
223
223
|
|
|
224
224
|
// Simulate stdin data
|
|
225
225
|
const message = {
|
|
@@ -236,6 +236,9 @@ describe('MCPServer', () => {
|
|
|
236
236
|
const output = JSON.parse(writeSpy.mock.calls[0][0] as string);
|
|
237
237
|
expect(output.id).toBe(9);
|
|
238
238
|
|
|
239
|
+
process.stdin.emit('close');
|
|
240
|
+
await startPromise;
|
|
241
|
+
|
|
239
242
|
writeSpy.mockRestore();
|
|
240
243
|
consoleSpy.mockRestore();
|
|
241
244
|
});
|
package/src/runner/mcp-server.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as readline from 'node:readline';
|
|
2
|
+
import pkg from '../../package.json' with { type: 'json' };
|
|
2
3
|
import { WorkflowDb } from '../db/workflow-db';
|
|
3
4
|
import { WorkflowParser } from '../parser/workflow-parser';
|
|
4
5
|
import { generateMermaidGraph } from '../utils/mermaid';
|
|
@@ -26,21 +27,32 @@ export class MCPServer {
|
|
|
26
27
|
terminal: false,
|
|
27
28
|
});
|
|
28
29
|
|
|
29
|
-
|
|
30
|
-
|
|
30
|
+
return new Promise<void>((resolve) => {
|
|
31
|
+
rl.on('line', async (line) => {
|
|
32
|
+
if (!line.trim()) return;
|
|
31
33
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
try {
|
|
35
|
+
const message = JSON.parse(line) as MCPMessage;
|
|
36
|
+
const response = await this.handleMessage(message);
|
|
37
|
+
if (response) {
|
|
38
|
+
process.stdout.write(`${JSON.stringify(response)}\n`);
|
|
39
|
+
}
|
|
40
|
+
} catch (error) {
|
|
41
|
+
console.error('Error handling MCP message:', error);
|
|
37
42
|
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
rl.on('close', () => {
|
|
46
|
+
this.stop();
|
|
47
|
+
resolve();
|
|
48
|
+
});
|
|
41
49
|
});
|
|
42
50
|
}
|
|
43
51
|
|
|
52
|
+
stop() {
|
|
53
|
+
this.db.close();
|
|
54
|
+
}
|
|
55
|
+
|
|
44
56
|
private async handleMessage(message: MCPMessage) {
|
|
45
57
|
const { method, params, id } = message;
|
|
46
58
|
|
|
@@ -56,7 +68,7 @@ export class MCPServer {
|
|
|
56
68
|
},
|
|
57
69
|
serverInfo: {
|
|
58
70
|
name: 'keystone-mcp',
|
|
59
|
-
version:
|
|
71
|
+
version: pkg.version,
|
|
60
72
|
},
|
|
61
73
|
},
|
|
62
74
|
};
|
|
@@ -172,6 +184,7 @@ export class MCPServer {
|
|
|
172
184
|
const runner = new WorkflowRunner(workflow, {
|
|
173
185
|
inputs,
|
|
174
186
|
logger,
|
|
187
|
+
preventExit: true,
|
|
175
188
|
});
|
|
176
189
|
|
|
177
190
|
// Note: This waits for completion. For long workflows, we might want to
|
|
@@ -337,6 +350,7 @@ export class MCPServer {
|
|
|
337
350
|
const runner = new WorkflowRunner(workflow, {
|
|
338
351
|
resumeRunId: run_id,
|
|
339
352
|
logger,
|
|
353
|
+
preventExit: true,
|
|
340
354
|
});
|
|
341
355
|
|
|
342
356
|
let outputs: Record<string, unknown> | undefined;
|
|
@@ -88,7 +88,7 @@ export async function executeShell(
|
|
|
88
88
|
logger: Logger = console
|
|
89
89
|
): Promise<ShellResult> {
|
|
90
90
|
// Evaluate the command string
|
|
91
|
-
const command = ExpressionEvaluator.
|
|
91
|
+
const command = ExpressionEvaluator.evaluateString(step.run, context);
|
|
92
92
|
|
|
93
93
|
// Check for potential shell injection risks
|
|
94
94
|
if (detectShellInjectionRisk(command)) {
|
|
@@ -101,12 +101,12 @@ export async function executeShell(
|
|
|
101
101
|
const env: Record<string, string> = {};
|
|
102
102
|
if (step.env) {
|
|
103
103
|
for (const [key, value] of Object.entries(step.env)) {
|
|
104
|
-
env[key] = ExpressionEvaluator.
|
|
104
|
+
env[key] = ExpressionEvaluator.evaluateString(value, context);
|
|
105
105
|
}
|
|
106
106
|
}
|
|
107
107
|
|
|
108
108
|
// Set working directory if specified
|
|
109
|
-
const cwd = step.dir ?
|
|
109
|
+
const cwd = step.dir ? ExpressionEvaluator.evaluateString(step.dir, context) : undefined;
|
|
110
110
|
|
|
111
111
|
try {
|
|
112
112
|
// Execute command using sh -c to allow shell parsing
|
|
@@ -84,11 +84,12 @@ export async function executeStep(
|
|
|
84
84
|
// Apply transformation if specified and step succeeded
|
|
85
85
|
if (step.transform && result.status === 'success') {
|
|
86
86
|
const transformContext = {
|
|
87
|
-
// Provide raw output properties (like stdout, data) directly in context
|
|
88
|
-
// Fix: Spread output FIRST, then context to prevent shadowing
|
|
87
|
+
// 1. Provide raw output properties (like stdout, data) directly in context for convenience
|
|
89
88
|
...(typeof result.output === 'object' && result.output !== null ? result.output : {}),
|
|
90
|
-
output
|
|
89
|
+
// 2. Add core context (inputs, secrets, etc.). This takes priority over output properties for security.
|
|
91
90
|
...context,
|
|
91
|
+
// 3. Explicitly add 'output' so it refers to the current step's result even if context or output properties have a collision.
|
|
92
|
+
output: result.output,
|
|
92
93
|
};
|
|
93
94
|
|
|
94
95
|
try {
|
|
@@ -159,7 +160,7 @@ async function executeFileStep(
|
|
|
159
160
|
context: ExpressionContext,
|
|
160
161
|
_logger: Logger
|
|
161
162
|
): Promise<StepResult> {
|
|
162
|
-
const path = ExpressionEvaluator.
|
|
163
|
+
const path = ExpressionEvaluator.evaluateString(step.path, context);
|
|
163
164
|
|
|
164
165
|
switch (step.op) {
|
|
165
166
|
case 'read': {
|
|
@@ -178,7 +179,7 @@ async function executeFileStep(
|
|
|
178
179
|
if (!step.content) {
|
|
179
180
|
throw new Error('Content is required for write operation');
|
|
180
181
|
}
|
|
181
|
-
const content = ExpressionEvaluator.
|
|
182
|
+
const content = ExpressionEvaluator.evaluateString(step.content, context);
|
|
182
183
|
const bytes = await Bun.write(path, content);
|
|
183
184
|
return {
|
|
184
185
|
output: { path, bytes },
|
|
@@ -190,7 +191,7 @@ async function executeFileStep(
|
|
|
190
191
|
if (!step.content) {
|
|
191
192
|
throw new Error('Content is required for append operation');
|
|
192
193
|
}
|
|
193
|
-
const content = ExpressionEvaluator.
|
|
194
|
+
const content = ExpressionEvaluator.evaluateString(step.content, context);
|
|
194
195
|
|
|
195
196
|
// Use Node.js fs for efficient append operation
|
|
196
197
|
const fs = await import('node:fs/promises');
|
|
@@ -215,13 +216,13 @@ async function executeRequestStep(
|
|
|
215
216
|
context: ExpressionContext,
|
|
216
217
|
_logger: Logger
|
|
217
218
|
): Promise<StepResult> {
|
|
218
|
-
const url = ExpressionEvaluator.
|
|
219
|
+
const url = ExpressionEvaluator.evaluateString(step.url, context);
|
|
219
220
|
|
|
220
221
|
// Evaluate headers
|
|
221
222
|
const headers: Record<string, string> = {};
|
|
222
223
|
if (step.headers) {
|
|
223
224
|
for (const [key, value] of Object.entries(step.headers)) {
|
|
224
|
-
headers[key] = ExpressionEvaluator.
|
|
225
|
+
headers[key] = ExpressionEvaluator.evaluateString(value, context);
|
|
225
226
|
}
|
|
226
227
|
}
|
|
227
228
|
|
|
@@ -290,7 +291,7 @@ async function executeHumanStep(
|
|
|
290
291
|
context: ExpressionContext,
|
|
291
292
|
logger: Logger
|
|
292
293
|
): Promise<StepResult> {
|
|
293
|
-
const message = ExpressionEvaluator.
|
|
294
|
+
const message = ExpressionEvaluator.evaluateString(step.message, context);
|
|
294
295
|
|
|
295
296
|
// If not a TTY (e.g. MCP server), suspend execution
|
|
296
297
|
if (!process.stdin.isTTY) {
|
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
import { afterAll, beforeAll, describe, expect, it, mock } from 'bun:test';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { executeLlmStep } from './llm-executor';
|
|
5
|
-
import type { LlmStep, Step } from '../parser/schema';
|
|
2
|
+
import { mkdirSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
6
4
|
import type { ExpressionContext } from '../expression/evaluator';
|
|
5
|
+
import type { LlmStep, Step } from '../parser/schema';
|
|
6
|
+
import {
|
|
7
|
+
AnthropicAdapter,
|
|
8
|
+
CopilotAdapter,
|
|
9
|
+
type LLMMessage,
|
|
10
|
+
type LLMResponse,
|
|
11
|
+
type LLMTool,
|
|
12
|
+
OpenAIAdapter,
|
|
13
|
+
} from './llm-adapter';
|
|
14
|
+
import { executeLlmStep } from './llm-executor';
|
|
15
|
+
import { MCPClient, type MCPResponse } from './mcp-client';
|
|
7
16
|
import type { StepResult } from './step-executor';
|
|
8
|
-
import { join } from 'node:path';
|
|
9
|
-
import { mkdirSync, writeFileSync, unlinkSync } from 'node:fs';
|
|
10
17
|
|
|
11
18
|
interface MockToolCall {
|
|
12
19
|
function: {
|
|
@@ -54,16 +61,16 @@ Test system prompt`;
|
|
|
54
61
|
};
|
|
55
62
|
});
|
|
56
63
|
|
|
57
|
-
OpenAIAdapter.prototype.chat = mockChat as
|
|
58
|
-
CopilotAdapter.prototype.chat = mockChat as
|
|
59
|
-
AnthropicAdapter.prototype.chat = mockChat as
|
|
64
|
+
OpenAIAdapter.prototype.chat = mockChat as unknown as typeof originalOpenAIChat;
|
|
65
|
+
CopilotAdapter.prototype.chat = mockChat as unknown as typeof originalCopilotChat;
|
|
66
|
+
AnthropicAdapter.prototype.chat = mockChat as unknown as typeof originalAnthropicChat;
|
|
60
67
|
|
|
61
68
|
// Use mock.module for MCPClient
|
|
62
69
|
const originalInitialize = MCPClient.prototype.initialize;
|
|
63
70
|
const originalListTools = MCPClient.prototype.listTools;
|
|
64
71
|
const originalStop = MCPClient.prototype.stop;
|
|
65
72
|
|
|
66
|
-
const mockInitialize = mock(async () => ({}) as
|
|
73
|
+
const mockInitialize = mock(async () => ({}) as MCPResponse);
|
|
67
74
|
const mockListTools = mock(async () => [
|
|
68
75
|
{
|
|
69
76
|
name: 'mcp-tool',
|
|
@@ -141,16 +148,16 @@ Test system prompt`;
|
|
|
141
148
|
};
|
|
142
149
|
});
|
|
143
150
|
|
|
144
|
-
OpenAIAdapter.prototype.chat = mockChat as
|
|
145
|
-
CopilotAdapter.prototype.chat = mockChat as
|
|
146
|
-
AnthropicAdapter.prototype.chat = mockChat as
|
|
151
|
+
OpenAIAdapter.prototype.chat = mockChat as unknown as typeof originalOpenAIChat;
|
|
152
|
+
CopilotAdapter.prototype.chat = mockChat as unknown as typeof originalCopilotChat;
|
|
153
|
+
AnthropicAdapter.prototype.chat = mockChat as unknown as typeof originalAnthropicChat;
|
|
147
154
|
|
|
148
155
|
const originalInitialize = MCPClient.prototype.initialize;
|
|
149
156
|
const originalListTools = MCPClient.prototype.listTools;
|
|
150
157
|
const originalCallTool = MCPClient.prototype.callTool;
|
|
151
158
|
const originalStop = MCPClient.prototype.stop;
|
|
152
159
|
|
|
153
|
-
const mockInitialize = mock(async () => ({}) as
|
|
160
|
+
const mockInitialize = mock(async () => ({}) as MCPResponse);
|
|
154
161
|
const mockListTools = mock(async () => [
|
|
155
162
|
{
|
|
156
163
|
name: 'mcp-tool',
|
|
@@ -45,6 +45,7 @@ export interface RunOptions {
|
|
|
45
45
|
resumeRunId?: string;
|
|
46
46
|
logger?: Logger;
|
|
47
47
|
mcpManager?: MCPManager;
|
|
48
|
+
preventExit?: boolean; // Defaults to false
|
|
48
49
|
}
|
|
49
50
|
|
|
50
51
|
export interface StepContext {
|
|
@@ -76,9 +77,12 @@ export class WorkflowRunner {
|
|
|
76
77
|
private restored = false;
|
|
77
78
|
private logger: Logger;
|
|
78
79
|
private mcpManager: MCPManager;
|
|
80
|
+
private options: RunOptions;
|
|
81
|
+
private signalHandler?: (signal: string) => void;
|
|
79
82
|
|
|
80
83
|
constructor(workflow: Workflow, options: RunOptions = {}) {
|
|
81
84
|
this.workflow = workflow;
|
|
85
|
+
this.options = options;
|
|
82
86
|
this.db = new WorkflowDb(options.dbPath);
|
|
83
87
|
this.secrets = this.loadSecrets();
|
|
84
88
|
this.redactor = new Redactor(this.secrets);
|
|
@@ -257,7 +261,7 @@ export class WorkflowRunner {
|
|
|
257
261
|
* Setup signal handlers for graceful shutdown
|
|
258
262
|
*/
|
|
259
263
|
private setupSignalHandlers(): void {
|
|
260
|
-
const
|
|
264
|
+
const handler = async (signal: string) => {
|
|
261
265
|
this.logger.log(`\n\nš Received ${signal}. Cleaning up...`);
|
|
262
266
|
try {
|
|
263
267
|
await this.db.updateRunStatus(
|
|
@@ -267,15 +271,30 @@ export class WorkflowRunner {
|
|
|
267
271
|
`Cancelled by user (${signal})`
|
|
268
272
|
);
|
|
269
273
|
this.logger.log('ā Run status updated to failed');
|
|
270
|
-
this.db.close();
|
|
271
274
|
} catch (error) {
|
|
272
275
|
this.logger.error('Error during cleanup:', error);
|
|
273
276
|
}
|
|
274
|
-
|
|
277
|
+
|
|
278
|
+
// Only exit if not embedded
|
|
279
|
+
if (!this.options.preventExit) {
|
|
280
|
+
process.exit(130);
|
|
281
|
+
}
|
|
275
282
|
};
|
|
276
283
|
|
|
277
|
-
|
|
278
|
-
|
|
284
|
+
this.signalHandler = handler;
|
|
285
|
+
|
|
286
|
+
process.on('SIGINT', handler);
|
|
287
|
+
process.on('SIGTERM', handler);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Remove signal handlers
|
|
292
|
+
*/
|
|
293
|
+
private removeSignalHandlers(): void {
|
|
294
|
+
if (this.signalHandler) {
|
|
295
|
+
process.removeListener('SIGINT', this.signalHandler);
|
|
296
|
+
process.removeListener('SIGTERM', this.signalHandler);
|
|
297
|
+
}
|
|
279
298
|
}
|
|
280
299
|
|
|
281
300
|
/**
|
|
@@ -853,6 +872,7 @@ export class WorkflowRunner {
|
|
|
853
872
|
await this.db.updateRunStatus(this.runId, 'failed', undefined, errorMsg);
|
|
854
873
|
throw error;
|
|
855
874
|
} finally {
|
|
875
|
+
this.removeSignalHandlers();
|
|
856
876
|
await this.runFinally();
|
|
857
877
|
await this.mcpManager.stopAll();
|
|
858
878
|
this.db.close();
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: explore
|
|
3
|
+
description: Agent for exploring and understanding codebases
|
|
4
|
+
model: claude-sonnet-4.5
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Explore Agent
|
|
8
|
+
|
|
9
|
+
You are an expert at exploring and understanding new codebases. Your role is to map out the structure, identify key components, and understand how the system works.
|
|
10
|
+
|
|
11
|
+
## Core Competencies
|
|
12
|
+
|
|
13
|
+
### Codebase Exploration
|
|
14
|
+
- Directory structure analysis
|
|
15
|
+
- Key file identification
|
|
16
|
+
- Entry point discovery
|
|
17
|
+
- Configuration analysis
|
|
18
|
+
|
|
19
|
+
### Architectural Mapping
|
|
20
|
+
- Component identification
|
|
21
|
+
- Service boundaries
|
|
22
|
+
- Data flow mapping
|
|
23
|
+
- Dependency analysis
|
|
24
|
+
|
|
25
|
+
### Pattern Recognition
|
|
26
|
+
- Coding conventions
|
|
27
|
+
- Design patterns in use
|
|
28
|
+
- Framework-specific idioms
|
|
29
|
+
- Error handling patterns
|
|
30
|
+
|
|
31
|
+
## Exploration Process
|
|
32
|
+
|
|
33
|
+
1. **Scan Structure** - Understand the high-level directory organization
|
|
34
|
+
2. **Identify Entry Points** - Find where the application or specific features start
|
|
35
|
+
3. **Trace Flows** - Follow the data or execution path for key use cases
|
|
36
|
+
4. **Analyze Configuration** - Understand how the system is set up and tuned
|
|
37
|
+
5. **Map Dependencies** - Identify internal and external connections
|
|
38
|
+
|
|
39
|
+
## Output Format
|
|
40
|
+
|
|
41
|
+
### Exploration Summary
|
|
42
|
+
- **Key Files & Directories**: Most important parts of the codebase
|
|
43
|
+
- **Architecture Overview**: How the pieces fit together
|
|
44
|
+
- **Notable Patterns**: Consistent ways the code is written
|
|
45
|
+
- **Dependencies**: Critical internal and external links
|
|
46
|
+
- **Concerns/Complexity**: Areas that might be difficult to work with
|
|
47
|
+
|
|
48
|
+
## Guidelines
|
|
49
|
+
|
|
50
|
+
- Focus on the big picture first, then dive into details
|
|
51
|
+
- Identify both what is there and what is missing
|
|
52
|
+
- Look for consistency and deviations
|
|
53
|
+
- Provide clear references to files and directories
|
|
54
|
+
- Summarize findings for technical and non-technical audiences
|