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
|
@@ -1,16 +1,23 @@
|
|
|
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>;
|
|
13
|
+
oauth?: {
|
|
14
|
+
scope?: string;
|
|
15
|
+
};
|
|
10
16
|
}
|
|
11
17
|
|
|
12
18
|
export class MCPManager {
|
|
13
19
|
private clients: Map<string, MCPClient> = new Map();
|
|
20
|
+
private connectionPromises: Map<string, Promise<MCPClient | undefined>> = new Map();
|
|
14
21
|
private sharedServers: Map<string, MCPServerConfig> = new Map();
|
|
15
22
|
|
|
16
23
|
constructor() {
|
|
@@ -23,10 +30,8 @@ export class MCPManager {
|
|
|
23
30
|
for (const [name, server] of Object.entries(config.mcp_servers)) {
|
|
24
31
|
this.sharedServers.set(name, {
|
|
25
32
|
name,
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
env: server.env,
|
|
29
|
-
});
|
|
33
|
+
...server,
|
|
34
|
+
} as MCPServerConfig);
|
|
30
35
|
}
|
|
31
36
|
}
|
|
32
37
|
}
|
|
@@ -49,23 +54,84 @@ export class MCPManager {
|
|
|
49
54
|
}
|
|
50
55
|
|
|
51
56
|
const key = this.getServerKey(config);
|
|
57
|
+
|
|
58
|
+
// Check if we already have a client
|
|
52
59
|
if (this.clients.has(key)) {
|
|
53
60
|
return this.clients.get(key);
|
|
54
61
|
}
|
|
55
62
|
|
|
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;
|
|
63
|
+
// Check if we are already connecting
|
|
64
|
+
if (this.connectionPromises.has(key)) {
|
|
65
|
+
return this.connectionPromises.get(key);
|
|
68
66
|
}
|
|
67
|
+
|
|
68
|
+
// Start a new connection and cache the promise
|
|
69
|
+
const connectionPromise = (async () => {
|
|
70
|
+
logger.log(` š Connecting to MCP server: ${config.name} (${config.type || 'local'})`);
|
|
71
|
+
|
|
72
|
+
let client: MCPClient;
|
|
73
|
+
try {
|
|
74
|
+
if (config.type === 'remote') {
|
|
75
|
+
if (!config.url) throw new Error('Remote MCP server missing URL');
|
|
76
|
+
|
|
77
|
+
const headers = { ...(config.headers || {}) };
|
|
78
|
+
|
|
79
|
+
if (config.oauth) {
|
|
80
|
+
const { AuthManager } = await import('../utils/auth-manager');
|
|
81
|
+
const auth = AuthManager.load();
|
|
82
|
+
const token = auth.mcp_tokens?.[config.name]?.access_token;
|
|
83
|
+
|
|
84
|
+
if (!token) {
|
|
85
|
+
throw new Error(
|
|
86
|
+
`MCP server ${config.name} requires OAuth. Please run "keystone mcp login ${config.name}" first.`
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
headers.Authorization = `Bearer ${token}`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
client = await MCPClient.createRemote(config.url, headers);
|
|
94
|
+
} else {
|
|
95
|
+
if (!config.command) throw new Error('Local MCP server missing command');
|
|
96
|
+
|
|
97
|
+
const env = { ...(config.env || {}) };
|
|
98
|
+
|
|
99
|
+
if (config.oauth) {
|
|
100
|
+
const { AuthManager } = await import('../utils/auth-manager');
|
|
101
|
+
const auth = AuthManager.load();
|
|
102
|
+
const token = auth.mcp_tokens?.[config.name]?.access_token;
|
|
103
|
+
|
|
104
|
+
if (!token) {
|
|
105
|
+
throw new Error(
|
|
106
|
+
`MCP server ${config.name} requires OAuth. Please run "keystone mcp login ${config.name}" first.`
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Pass token to the local proxy via environment variables
|
|
111
|
+
// Most proxies expect AUTHORIZATION or MCP_TOKEN
|
|
112
|
+
env.AUTHORIZATION = `Bearer ${token}`;
|
|
113
|
+
env.MCP_TOKEN = token;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
client = await MCPClient.createLocal(config.command, config.args || [], env);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
await client.initialize();
|
|
120
|
+
this.clients.set(key, client);
|
|
121
|
+
return client;
|
|
122
|
+
} catch (error) {
|
|
123
|
+
logger.error(
|
|
124
|
+
` ā Failed to connect to MCP server ${config.name}: ${error instanceof Error ? error.message : String(error)}`
|
|
125
|
+
);
|
|
126
|
+
return undefined;
|
|
127
|
+
} finally {
|
|
128
|
+
// Remove promise from cache once settled
|
|
129
|
+
this.connectionPromises.delete(key);
|
|
130
|
+
}
|
|
131
|
+
})();
|
|
132
|
+
|
|
133
|
+
this.connectionPromises.set(key, connectionPromise);
|
|
134
|
+
return connectionPromise;
|
|
69
135
|
}
|
|
70
136
|
|
|
71
137
|
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
|
|
@@ -374,7 +374,7 @@ describe('step-executor', () => {
|
|
|
374
374
|
const step: WorkflowStep = {
|
|
375
375
|
id: 'w1',
|
|
376
376
|
type: 'workflow',
|
|
377
|
-
|
|
377
|
+
path: 'child.yaml',
|
|
378
378
|
};
|
|
379
379
|
// @ts-ignore
|
|
380
380
|
const executeWorkflowFn = mock(() =>
|
|
@@ -392,7 +392,7 @@ describe('step-executor', () => {
|
|
|
392
392
|
const step: WorkflowStep = {
|
|
393
393
|
id: 'w1',
|
|
394
394
|
type: 'workflow',
|
|
395
|
-
|
|
395
|
+
path: 'child.yaml',
|
|
396
396
|
};
|
|
397
397
|
const result = await executeStep(step, context);
|
|
398
398
|
expect(result.status).toBe('failed');
|
|
@@ -42,7 +42,8 @@ export async function executeStep(
|
|
|
42
42
|
context: ExpressionContext,
|
|
43
43
|
logger: Logger = console,
|
|
44
44
|
executeWorkflowFn?: (step: WorkflowStep, context: ExpressionContext) => Promise<StepResult>,
|
|
45
|
-
mcpManager?: MCPManager
|
|
45
|
+
mcpManager?: MCPManager,
|
|
46
|
+
workflowDir?: string
|
|
46
47
|
): Promise<StepResult> {
|
|
47
48
|
try {
|
|
48
49
|
let result: StepResult;
|
|
@@ -66,9 +67,10 @@ export async function executeStep(
|
|
|
66
67
|
result = await executeLlmStep(
|
|
67
68
|
step,
|
|
68
69
|
context,
|
|
69
|
-
(s, c) => executeStep(s, c, logger, executeWorkflowFn, mcpManager),
|
|
70
|
+
(s, c) => executeStep(s, c, logger, executeWorkflowFn, mcpManager, workflowDir),
|
|
70
71
|
logger,
|
|
71
|
-
mcpManager
|
|
72
|
+
mcpManager,
|
|
73
|
+
workflowDir
|
|
72
74
|
);
|
|
73
75
|
break;
|
|
74
76
|
case 'workflow':
|
|
@@ -84,11 +86,12 @@ export async function executeStep(
|
|
|
84
86
|
// Apply transformation if specified and step succeeded
|
|
85
87
|
if (step.transform && result.status === 'success') {
|
|
86
88
|
const transformContext = {
|
|
87
|
-
// Provide raw output properties (like stdout, data) directly in context
|
|
88
|
-
// Fix: Spread output FIRST, then context to prevent shadowing
|
|
89
|
+
// 1. Provide raw output properties (like stdout, data) directly in context for convenience
|
|
89
90
|
...(typeof result.output === 'object' && result.output !== null ? result.output : {}),
|
|
90
|
-
output
|
|
91
|
+
// 2. Add core context (inputs, secrets, etc.). This takes priority over output properties for security.
|
|
91
92
|
...context,
|
|
93
|
+
// 3. Explicitly add 'output' so it refers to the current step's result even if context or output properties have a collision.
|
|
94
|
+
output: result.output,
|
|
92
95
|
};
|
|
93
96
|
|
|
94
97
|
try {
|
|
@@ -159,7 +162,7 @@ async function executeFileStep(
|
|
|
159
162
|
context: ExpressionContext,
|
|
160
163
|
_logger: Logger
|
|
161
164
|
): Promise<StepResult> {
|
|
162
|
-
const path = ExpressionEvaluator.
|
|
165
|
+
const path = ExpressionEvaluator.evaluateString(step.path, context);
|
|
163
166
|
|
|
164
167
|
switch (step.op) {
|
|
165
168
|
case 'read': {
|
|
@@ -178,7 +181,14 @@ async function executeFileStep(
|
|
|
178
181
|
if (!step.content) {
|
|
179
182
|
throw new Error('Content is required for write operation');
|
|
180
183
|
}
|
|
181
|
-
const content = ExpressionEvaluator.
|
|
184
|
+
const content = ExpressionEvaluator.evaluateString(step.content, context);
|
|
185
|
+
|
|
186
|
+
// Ensure parent directory exists
|
|
187
|
+
const fs = await import('node:fs/promises');
|
|
188
|
+
const pathModule = await import('node:path');
|
|
189
|
+
const dir = pathModule.dirname(path);
|
|
190
|
+
await fs.mkdir(dir, { recursive: true });
|
|
191
|
+
|
|
182
192
|
const bytes = await Bun.write(path, content);
|
|
183
193
|
return {
|
|
184
194
|
output: { path, bytes },
|
|
@@ -190,10 +200,15 @@ async function executeFileStep(
|
|
|
190
200
|
if (!step.content) {
|
|
191
201
|
throw new Error('Content is required for append operation');
|
|
192
202
|
}
|
|
193
|
-
const content = ExpressionEvaluator.
|
|
203
|
+
const content = ExpressionEvaluator.evaluateString(step.content, context);
|
|
194
204
|
|
|
195
|
-
//
|
|
205
|
+
// Ensure parent directory exists
|
|
196
206
|
const fs = await import('node:fs/promises');
|
|
207
|
+
const pathModule = await import('node:path');
|
|
208
|
+
const dir = pathModule.dirname(path);
|
|
209
|
+
await fs.mkdir(dir, { recursive: true });
|
|
210
|
+
|
|
211
|
+
// Use Node.js fs for efficient append operation
|
|
197
212
|
await fs.appendFile(path, content, 'utf-8');
|
|
198
213
|
|
|
199
214
|
return {
|
|
@@ -215,13 +230,13 @@ async function executeRequestStep(
|
|
|
215
230
|
context: ExpressionContext,
|
|
216
231
|
_logger: Logger
|
|
217
232
|
): Promise<StepResult> {
|
|
218
|
-
const url = ExpressionEvaluator.
|
|
233
|
+
const url = ExpressionEvaluator.evaluateString(step.url, context);
|
|
219
234
|
|
|
220
235
|
// Evaluate headers
|
|
221
236
|
const headers: Record<string, string> = {};
|
|
222
237
|
if (step.headers) {
|
|
223
238
|
for (const [key, value] of Object.entries(step.headers)) {
|
|
224
|
-
headers[key] = ExpressionEvaluator.
|
|
239
|
+
headers[key] = ExpressionEvaluator.evaluateString(value, context);
|
|
225
240
|
}
|
|
226
241
|
}
|
|
227
242
|
|
|
@@ -290,7 +305,7 @@ async function executeHumanStep(
|
|
|
290
305
|
context: ExpressionContext,
|
|
291
306
|
logger: Logger
|
|
292
307
|
): Promise<StepResult> {
|
|
293
|
-
const message = ExpressionEvaluator.
|
|
308
|
+
const message = ExpressionEvaluator.evaluateString(step.message, context);
|
|
294
309
|
|
|
295
310
|
// If not a TTY (e.g. MCP server), suspend execution
|
|
296
311
|
if (!process.stdin.isTTY) {
|
|
@@ -309,10 +324,10 @@ async function executeHumanStep(
|
|
|
309
324
|
try {
|
|
310
325
|
if (step.inputType === 'confirm') {
|
|
311
326
|
logger.log(`\nā ${message}`);
|
|
312
|
-
|
|
313
|
-
|
|
327
|
+
const answer = await rl.question('Confirm? (Y/n): ');
|
|
328
|
+
const isConfirmed = answer.toLowerCase() !== 'n';
|
|
314
329
|
return {
|
|
315
|
-
output:
|
|
330
|
+
output: isConfirmed,
|
|
316
331
|
status: 'success',
|
|
317
332
|
};
|
|
318
333
|
}
|
|
@@ -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',
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
2
3
|
import { WorkflowDb } from '../db/workflow-db.ts';
|
|
3
4
|
import type { ExpressionContext } from '../expression/evaluator.ts';
|
|
4
5
|
import { ExpressionEvaluator } from '../expression/evaluator.ts';
|
|
@@ -45,6 +46,8 @@ export interface RunOptions {
|
|
|
45
46
|
resumeRunId?: string;
|
|
46
47
|
logger?: Logger;
|
|
47
48
|
mcpManager?: MCPManager;
|
|
49
|
+
preventExit?: boolean; // Defaults to false
|
|
50
|
+
workflowDir?: string;
|
|
48
51
|
}
|
|
49
52
|
|
|
50
53
|
export interface StepContext {
|
|
@@ -76,9 +79,12 @@ export class WorkflowRunner {
|
|
|
76
79
|
private restored = false;
|
|
77
80
|
private logger: Logger;
|
|
78
81
|
private mcpManager: MCPManager;
|
|
82
|
+
private options: RunOptions;
|
|
83
|
+
private signalHandler?: (signal: string) => void;
|
|
79
84
|
|
|
80
85
|
constructor(workflow: Workflow, options: RunOptions = {}) {
|
|
81
86
|
this.workflow = workflow;
|
|
87
|
+
this.options = options;
|
|
82
88
|
this.db = new WorkflowDb(options.dbPath);
|
|
83
89
|
this.secrets = this.loadSecrets();
|
|
84
90
|
this.redactor = new Redactor(this.secrets);
|
|
@@ -257,7 +263,7 @@ export class WorkflowRunner {
|
|
|
257
263
|
* Setup signal handlers for graceful shutdown
|
|
258
264
|
*/
|
|
259
265
|
private setupSignalHandlers(): void {
|
|
260
|
-
const
|
|
266
|
+
const handler = async (signal: string) => {
|
|
261
267
|
this.logger.log(`\n\nš Received ${signal}. Cleaning up...`);
|
|
262
268
|
try {
|
|
263
269
|
await this.db.updateRunStatus(
|
|
@@ -267,15 +273,30 @@ export class WorkflowRunner {
|
|
|
267
273
|
`Cancelled by user (${signal})`
|
|
268
274
|
);
|
|
269
275
|
this.logger.log('ā Run status updated to failed');
|
|
270
|
-
this.db.close();
|
|
271
276
|
} catch (error) {
|
|
272
277
|
this.logger.error('Error during cleanup:', error);
|
|
273
278
|
}
|
|
274
|
-
|
|
279
|
+
|
|
280
|
+
// Only exit if not embedded
|
|
281
|
+
if (!this.options.preventExit) {
|
|
282
|
+
process.exit(130);
|
|
283
|
+
}
|
|
275
284
|
};
|
|
276
285
|
|
|
277
|
-
|
|
278
|
-
|
|
286
|
+
this.signalHandler = handler;
|
|
287
|
+
|
|
288
|
+
process.on('SIGINT', handler);
|
|
289
|
+
process.on('SIGTERM', handler);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Remove signal handlers
|
|
294
|
+
*/
|
|
295
|
+
private removeSignalHandlers(): void {
|
|
296
|
+
if (this.signalHandler) {
|
|
297
|
+
process.removeListener('SIGINT', this.signalHandler);
|
|
298
|
+
process.removeListener('SIGTERM', this.signalHandler);
|
|
299
|
+
}
|
|
279
300
|
}
|
|
280
301
|
|
|
281
302
|
/**
|
|
@@ -437,7 +458,8 @@ export class WorkflowRunner {
|
|
|
437
458
|
context,
|
|
438
459
|
this.logger,
|
|
439
460
|
this.executeSubWorkflow.bind(this),
|
|
440
|
-
this.mcpManager
|
|
461
|
+
this.mcpManager,
|
|
462
|
+
this.options.workflowDir
|
|
441
463
|
);
|
|
442
464
|
if (result.status === 'failed') {
|
|
443
465
|
throw new Error(result.error || 'Step failed');
|
|
@@ -681,6 +703,7 @@ export class WorkflowRunner {
|
|
|
681
703
|
): Promise<StepResult> {
|
|
682
704
|
const workflowPath = WorkflowRegistry.resolvePath(step.path);
|
|
683
705
|
const workflow = WorkflowParser.loadWorkflow(workflowPath);
|
|
706
|
+
const subWorkflowDir = dirname(workflowPath);
|
|
684
707
|
|
|
685
708
|
// Evaluate inputs for the sub-workflow
|
|
686
709
|
const inputs: Record<string, unknown> = {};
|
|
@@ -697,6 +720,7 @@ export class WorkflowRunner {
|
|
|
697
720
|
dbPath: this.db.dbPath,
|
|
698
721
|
logger: this.logger,
|
|
699
722
|
mcpManager: this.mcpManager,
|
|
723
|
+
workflowDir: subWorkflowDir,
|
|
700
724
|
});
|
|
701
725
|
|
|
702
726
|
try {
|
|
@@ -853,8 +877,11 @@ export class WorkflowRunner {
|
|
|
853
877
|
await this.db.updateRunStatus(this.runId, 'failed', undefined, errorMsg);
|
|
854
878
|
throw error;
|
|
855
879
|
} finally {
|
|
880
|
+
this.removeSignalHandlers();
|
|
856
881
|
await this.runFinally();
|
|
857
|
-
|
|
882
|
+
if (!this.options.mcpManager) {
|
|
883
|
+
await this.mcpManager.stopAll();
|
|
884
|
+
}
|
|
858
885
|
this.db.close();
|
|
859
886
|
}
|
|
860
887
|
}
|
|
@@ -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
|