@agentforge-ai/cli 0.5.5 → 0.7.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.
@@ -3,17 +3,17 @@
3
3
  *
4
4
  * This module provides the core chat execution pipeline:
5
5
  * 1. User sends a message → stored via mutation
6
- * 2. Convex action triggers LLM generation via OpenRouter (AI SDK)
6
+ * 2. Convex action triggers LLM generation via Mastra Agent
7
7
  * 3. Assistant response stored back in Convex
8
8
  * 4. Real-time subscription updates the UI automatically
9
9
  *
10
- * Uses the Vercel AI SDK with OpenRouter as an OpenAI-compatible provider.
11
- * No dynamic imports of @mastra/core we use the AI SDK directly in Convex
12
- * Node.js actions for maximum reliability.
10
+ * Uses Mastra-native model routing via Agent.generate() with "provider/model-name" format.
11
+ * Mastra auto-reads provider API keys from environment variables.
13
12
  */
14
13
  import { action, mutation, query } from "./_generated/server";
15
14
  import { v } from "convex/values";
16
15
  import { api } from "./_generated/api";
16
+ import { Agent } from "@mastra/core/agent";
17
17
 
18
18
  // ============================================================
19
19
  // Queries
@@ -140,7 +140,7 @@ export const addAssistantMessage = mutation({
140
140
  * 1. Looks up the agent config from the database
141
141
  * 2. Stores the user message
142
142
  * 3. Builds conversation history from the thread
143
- * 4. Calls OpenRouter (or other provider) via the AI SDK
143
+ * 4. Calls the provider via Mastra Agent.generate()
144
144
  * 5. Stores the assistant response
145
145
  * 6. Records usage metrics
146
146
  *
@@ -180,54 +180,25 @@ export const sendMessage = action({
180
180
  content: msg.content,
181
181
  }));
182
182
 
183
- // 4. Call the LLM via AI SDK
183
+ // 4. Call the LLM via Mastra Agent
184
184
  let responseText: string;
185
185
  let usageData: { promptTokens: number; completionTokens: number; totalTokens: number } | null = null;
186
186
 
187
187
  try {
188
- // Dynamically import the AI SDK (available in Convex Node.js actions)
189
- const { generateText } = await import("ai");
190
- const { createOpenAI } = await import("@ai-sdk/openai");
191
-
192
188
  // Resolve the model provider and ID
193
189
  const provider = agent.provider || "openrouter";
194
190
  const modelId = agent.model || "openai/gpt-4o-mini";
191
+ const modelKey = `${provider}/${modelId}`;
195
192
 
196
- // Create the appropriate provider instance
197
- let model;
198
- if (provider === "openrouter") {
199
- const openrouter = createOpenAI({
200
- baseURL: "https://openrouter.ai/api/v1",
201
- apiKey: process.env.OPENROUTER_API_KEY,
202
- });
203
- model = openrouter(modelId);
204
- } else if (provider === "openai") {
205
- const openai = createOpenAI({
206
- apiKey: process.env.OPENAI_API_KEY,
207
- });
208
- model = openai(modelId);
209
- } else {
210
- // Default to OpenRouter for any provider — it routes to all models
211
- const openrouter = createOpenAI({
212
- baseURL: "https://openrouter.ai/api/v1",
213
- apiKey: process.env.OPENROUTER_API_KEY,
214
- });
215
- // Use provider/model format for OpenRouter routing
216
- const routerModelId = modelId.includes("/")
217
- ? modelId
218
- : `${provider}/${modelId}`;
219
- model = openrouter(routerModelId);
220
- }
221
-
222
- // Generate the response
223
- const result = await generateText({
224
- model,
225
- system: agent.instructions || "You are a helpful AI assistant built with AgentForge.",
226
- messages: conversationMessages,
227
- ...(agent.temperature != null && { temperature: agent.temperature }),
228
- ...(agent.maxTokens != null && { maxTokens: agent.maxTokens }),
193
+ // Generate via Mastra Agent
194
+ const mastraAgent = new Agent({
195
+ name: "agentforge-executor",
196
+ instructions: agent.instructions || "You are a helpful AI assistant built with AgentForge.",
197
+ model: modelKey,
229
198
  });
230
199
 
200
+ const result = await mastraAgent.generate(conversationMessages);
201
+
231
202
  responseText = result.text;
232
203
 
233
204
  // Extract usage if available
@@ -240,7 +211,7 @@ export const sendMessage = action({
240
211
  }
241
212
  } catch (error: unknown) {
242
213
  const errorMessage = error instanceof Error ? error.message : String(error);
243
- console.error("[chat.sendMessage] LLM error:", errorMessage);
214
+ console.error("[chat.sendMessage] Mastra error:", errorMessage);
244
215
 
245
216
  // Store error as assistant message so user sees feedback
246
217
  responseText = `I encountered an error while processing your request: ${errorMessage}`;
@@ -2,20 +2,17 @@
2
2
  * Mastra Integration Actions for Convex
3
3
  *
4
4
  * These actions run in the Convex Node.js runtime and execute LLM calls
5
- * using the Vercel AI SDK with OpenRouter as the default provider.
5
+ * using Mastra-native model routing with OpenRouter as the default provider.
6
6
  *
7
7
  * Architecture:
8
8
  * - For chat: use `chat.sendMessage` (preferred entry point)
9
9
  * - For programmatic agent execution: use `mastraIntegration.executeAgent`
10
- * - Model resolution: uses AI SDK providers directly (OpenRouter, OpenAI, etc.)
11
- *
12
- * This replaces the previous broken approach of dynamically importing
13
- * @mastra/core inside Convex actions. The AI SDK is the correct way to
14
- * call LLMs from Convex Node.js actions.
10
+ * - Model resolution: uses Mastra Agent with "provider/model-name" format
15
11
  */
16
12
  import { action } from "./_generated/server";
17
13
  import { v } from "convex/values";
18
14
  import { api } from "./_generated/api";
15
+ import { Agent } from "@mastra/core/agent";
19
16
 
20
17
  // Return type for executeAgent
21
18
  type ExecuteAgentResult = {
@@ -30,54 +27,6 @@ type ExecuteAgentResult = {
30
27
  };
31
28
  };
32
29
 
33
- /**
34
- * Resolve a model instance from provider + modelId using the AI SDK.
35
- *
36
- * Supports: openrouter, openai, anthropic, google, venice, custom.
37
- * Falls back to OpenRouter for unknown providers (it routes to all models).
38
- */
39
- async function resolveModel(provider: string, modelId: string) {
40
- const { createOpenAI } = await import("@ai-sdk/openai");
41
-
42
- switch (provider) {
43
- case "openai": {
44
- const openai = createOpenAI({
45
- apiKey: process.env.OPENAI_API_KEY,
46
- });
47
- return openai(modelId);
48
- }
49
-
50
- case "anthropic": {
51
- const { createAnthropic } = await import("@ai-sdk/anthropic");
52
- const anthropic = createAnthropic({
53
- apiKey: process.env.ANTHROPIC_API_KEY,
54
- });
55
- return anthropic(modelId);
56
- }
57
-
58
- case "google": {
59
- const { createGoogleGenerativeAI } = await import("@ai-sdk/google");
60
- const google = createGoogleGenerativeAI({
61
- apiKey: process.env.GEMINI_API_KEY,
62
- });
63
- return google(modelId);
64
- }
65
-
66
- case "openrouter":
67
- default: {
68
- // OpenRouter is OpenAI-compatible and routes to all providers
69
- const openrouter = createOpenAI({
70
- baseURL: "https://openrouter.ai/api/v1",
71
- apiKey: process.env.OPENROUTER_API_KEY,
72
- });
73
- const routerModelId = modelId.includes("/")
74
- ? modelId
75
- : `${provider}/${modelId}`;
76
- return openrouter(routerModelId);
77
- }
78
- }
79
- }
80
-
81
30
  /**
82
31
  * Execute an agent with a prompt and return the response.
83
32
  *
@@ -127,12 +76,10 @@ export const executeAgent = action({
127
76
  });
128
77
 
129
78
  try {
130
- const { generateText } = await import("ai");
131
-
132
79
  // Resolve the model
133
80
  const provider = agent.provider || "openrouter";
134
81
  const modelId = agent.model || "openai/gpt-4o-mini";
135
- const model = await resolveModel(provider, modelId);
82
+ const modelKey = `${provider}/${modelId}`;
136
83
 
137
84
  // Get conversation history for context
138
85
  const messages = await ctx.runQuery(api.messages.list, { threadId });
@@ -143,15 +90,15 @@ export const executeAgent = action({
143
90
  content: m.content,
144
91
  }));
145
92
 
146
- // Execute the LLM call
147
- const result = await generateText({
148
- model,
149
- system: agent.instructions || "You are a helpful AI assistant.",
150
- messages: conversationMessages,
151
- ...(agent.temperature != null && { temperature: agent.temperature }),
152
- ...(agent.maxTokens != null && { maxTokens: agent.maxTokens }),
93
+ // Execute via Mastra Agent
94
+ const mastraAgent = new Agent({
95
+ name: "agentforge-executor",
96
+ instructions: agent.instructions || "You are a helpful AI assistant.",
97
+ model: modelKey,
153
98
  });
154
99
 
100
+ const result = await mastraAgent.generate(conversationMessages);
101
+
155
102
  const responseContent = result.text;
156
103
 
157
104
  // Add assistant message to thread
@@ -38,7 +38,7 @@ export const webSearch = createTool({
38
38
  {
39
39
  name: 'code-executor',
40
40
  displayName: 'Code Executor',
41
- description: 'Safely execute JavaScript/TypeScript code snippets in a sandboxed environment and return the output.',
41
+ description: 'Execute JavaScript/TypeScript code snippets via the AgentForge sandbox API and return the output.',
42
42
  category: 'Tools',
43
43
  version: '1.0.0',
44
44
  author: 'AgentForge',
@@ -48,18 +48,25 @@ import { z } from 'zod';
48
48
 
49
49
  export const codeExecutor = createTool({
50
50
  id: 'code-executor',
51
- description: 'Execute JavaScript code and return the result',
51
+ description: 'Execute JavaScript code via the sandbox API and return the result',
52
52
  inputSchema: z.object({
53
53
  code: z.string().describe('JavaScript code to execute'),
54
+ language: z.enum(['javascript', 'typescript']).default('javascript'),
54
55
  }),
55
56
  execute: async ({ context }) => {
56
- try {
57
- const fn = new Function('return (async () => {' + context.code + '})()');
58
- const result = await fn();
59
- return { success: true, output: String(result) };
60
- } catch (error: any) {
61
- return { success: false, error: error.message };
57
+ // Code is executed server-side via the AgentForge sandbox API,
58
+ // not via eval or Function constructor, to prevent arbitrary code execution risks.
59
+ const response = await fetch('/api/sandbox/execute', {
60
+ method: 'POST',
61
+ headers: { 'Content-Type': 'application/json' },
62
+ body: JSON.stringify({ code: context.code, language: context.language }),
63
+ });
64
+ if (!response.ok) {
65
+ const err = await response.json().catch(() => ({ error: 'Unknown error' }));
66
+ return { success: false, error: err.error ?? 'Sandbox execution failed' };
62
67
  }
68
+ const data = await response.json();
69
+ return { success: true, output: String(data.output ?? '') };
63
70
  },
64
71
  });`,
65
72
  },
@@ -74,16 +81,71 @@ export const codeExecutor = createTool({
74
81
  code: `import { createTool } from '@mastra/core';
75
82
  import { z } from 'zod';
76
83
 
84
+ // Safe arithmetic evaluator — no eval or Function constructor used.
85
+ function evaluateExpression(expr: string): number {
86
+ // Allow only digits, operators, parentheses, dots, and whitespace.
87
+ if (!/^[0-9+\\-*/().%\\s]+$/.test(expr)) {
88
+ throw new Error('Expression contains invalid characters');
89
+ }
90
+ // Recursive descent parser for: expr = term (('+' | '-') term)*
91
+ let pos = 0;
92
+ const peek = () => expr[pos] ?? '';
93
+ const consume = () => expr[pos++];
94
+ const skipWs = () => { while (peek() === ' ') consume(); };
95
+
96
+ function parseNumber(): number {
97
+ skipWs();
98
+ let num = '';
99
+ if (peek() === '(') { consume(); const v = parseExpr(); skipWs(); consume(); return v; }
100
+ while (/[0-9.]/.test(peek())) num += consume();
101
+ if (num === '') throw new Error('Expected number');
102
+ return parseFloat(num);
103
+ }
104
+
105
+ function parseFactor(): number {
106
+ skipWs();
107
+ if (peek() === '-') { consume(); return -parseFactor(); }
108
+ return parseNumber();
109
+ }
110
+
111
+ function parseTerm(): number {
112
+ let left = parseFactor();
113
+ skipWs();
114
+ while (peek() === '*' || peek() === '/') {
115
+ const op = consume(); skipWs();
116
+ const right = parseFactor();
117
+ left = op === '*' ? left * right : left / right;
118
+ skipWs();
119
+ }
120
+ return left;
121
+ }
122
+
123
+ function parseExpr(): number {
124
+ let left = parseTerm();
125
+ skipWs();
126
+ while (peek() === '+' || peek() === '-') {
127
+ const op = consume(); skipWs();
128
+ const right = parseTerm();
129
+ left = op === '+' ? left + right : left - right;
130
+ skipWs();
131
+ }
132
+ return left;
133
+ }
134
+
135
+ const result = parseExpr();
136
+ if (pos !== expr.length) throw new Error('Unexpected token at position ' + pos);
137
+ return result;
138
+ }
139
+
77
140
  export const calculator = createTool({
78
141
  id: 'calculator',
79
- description: 'Evaluate a mathematical expression',
142
+ description: 'Evaluate a mathematical expression safely',
80
143
  inputSchema: z.object({
81
144
  expression: z.string().describe('Math expression to evaluate, e.g. "2 + 2 * 3"'),
82
145
  }),
83
146
  execute: async ({ context }) => {
84
147
  try {
85
- const sanitized = context.expression.replace(/[^0-9+\\-*/().%\\s]/g, '');
86
- const result = Function('"use strict"; return (' + sanitized + ')')();
148
+ const result = evaluateExpression(context.expression.trim());
87
149
  return { result: Number(result), expression: context.expression };
88
150
  } catch (error: any) {
89
151
  return { error: 'Invalid expression: ' + error.message };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentforge-ai/cli",
3
- "version": "0.5.5",
3
+ "version": "0.7.0",
4
4
  "description": "CLI tool for creating, running, and managing AgentForge projects",
5
5
  "type": "module",
6
6
  "bin": {
@@ -19,7 +19,8 @@
19
19
  "fs-extra": "^11.2.0",
20
20
  "gray-matter": "^4.0.3",
21
21
  "ora": "^8.0.0",
22
- "prompts": "^2.4.2"
22
+ "prompts": "^2.4.2",
23
+ "@agentforge-ai/core": "0.7.0"
23
24
  },
24
25
  "devDependencies": {
25
26
  "@types/fs-extra": "^11.0.4",
@@ -3,17 +3,17 @@
3
3
  *
4
4
  * This module provides the core chat execution pipeline:
5
5
  * 1. User sends a message → stored via mutation
6
- * 2. Convex action triggers LLM generation via OpenRouter (AI SDK)
6
+ * 2. Convex action triggers LLM generation via Mastra Agent
7
7
  * 3. Assistant response stored back in Convex
8
8
  * 4. Real-time subscription updates the UI automatically
9
9
  *
10
- * Uses the Vercel AI SDK with OpenRouter as an OpenAI-compatible provider.
11
- * No dynamic imports of @mastra/core we use the AI SDK directly in Convex
12
- * Node.js actions for maximum reliability.
10
+ * Uses Mastra-native model routing via Agent.generate() with "provider/model-name" format.
11
+ * Mastra auto-reads provider API keys from environment variables.
13
12
  */
14
13
  import { action, mutation, query } from "./_generated/server";
15
14
  import { v } from "convex/values";
16
15
  import { api } from "./_generated/api";
16
+ import { Agent } from "@mastra/core/agent";
17
17
 
18
18
  // ============================================================
19
19
  // Queries
@@ -140,7 +140,7 @@ export const addAssistantMessage = mutation({
140
140
  * 1. Looks up the agent config from the database
141
141
  * 2. Stores the user message
142
142
  * 3. Builds conversation history from the thread
143
- * 4. Calls OpenRouter (or other provider) via the AI SDK
143
+ * 4. Calls the provider via Mastra Agent.generate()
144
144
  * 5. Stores the assistant response
145
145
  * 6. Records usage metrics
146
146
  *
@@ -180,54 +180,25 @@ export const sendMessage = action({
180
180
  content: msg.content,
181
181
  }));
182
182
 
183
- // 4. Call the LLM via AI SDK
183
+ // 4. Call the LLM via Mastra Agent
184
184
  let responseText: string;
185
185
  let usageData: { promptTokens: number; completionTokens: number; totalTokens: number } | null = null;
186
186
 
187
187
  try {
188
- // Dynamically import the AI SDK (available in Convex Node.js actions)
189
- const { generateText } = await import("ai");
190
- const { createOpenAI } = await import("@ai-sdk/openai");
191
-
192
188
  // Resolve the model provider and ID
193
189
  const provider = agent.provider || "openrouter";
194
190
  const modelId = agent.model || "openai/gpt-4o-mini";
191
+ const modelKey = `${provider}/${modelId}`;
195
192
 
196
- // Create the appropriate provider instance
197
- let model;
198
- if (provider === "openrouter") {
199
- const openrouter = createOpenAI({
200
- baseURL: "https://openrouter.ai/api/v1",
201
- apiKey: process.env.OPENROUTER_API_KEY,
202
- });
203
- model = openrouter(modelId);
204
- } else if (provider === "openai") {
205
- const openai = createOpenAI({
206
- apiKey: process.env.OPENAI_API_KEY,
207
- });
208
- model = openai(modelId);
209
- } else {
210
- // Default to OpenRouter for any provider — it routes to all models
211
- const openrouter = createOpenAI({
212
- baseURL: "https://openrouter.ai/api/v1",
213
- apiKey: process.env.OPENROUTER_API_KEY,
214
- });
215
- // Use provider/model format for OpenRouter routing
216
- const routerModelId = modelId.includes("/")
217
- ? modelId
218
- : `${provider}/${modelId}`;
219
- model = openrouter(routerModelId);
220
- }
221
-
222
- // Generate the response
223
- const result = await generateText({
224
- model,
225
- system: agent.instructions || "You are a helpful AI assistant built with AgentForge.",
226
- messages: conversationMessages,
227
- ...(agent.temperature != null && { temperature: agent.temperature }),
228
- ...(agent.maxTokens != null && { maxTokens: agent.maxTokens }),
193
+ // Generate via Mastra Agent
194
+ const mastraAgent = new Agent({
195
+ name: "agentforge-executor",
196
+ instructions: agent.instructions || "You are a helpful AI assistant built with AgentForge.",
197
+ model: modelKey,
229
198
  });
230
199
 
200
+ const result = await mastraAgent.generate(conversationMessages);
201
+
231
202
  responseText = result.text;
232
203
 
233
204
  // Extract usage if available
@@ -240,7 +211,7 @@ export const sendMessage = action({
240
211
  }
241
212
  } catch (error: unknown) {
242
213
  const errorMessage = error instanceof Error ? error.message : String(error);
243
- console.error("[chat.sendMessage] LLM error:", errorMessage);
214
+ console.error("[chat.sendMessage] Mastra error:", errorMessage);
244
215
 
245
216
  // Store error as assistant message so user sees feedback
246
217
  responseText = `I encountered an error while processing your request: ${errorMessage}`;
@@ -2,20 +2,17 @@
2
2
  * Mastra Integration Actions for Convex
3
3
  *
4
4
  * These actions run in the Convex Node.js runtime and execute LLM calls
5
- * using the Vercel AI SDK with OpenRouter as the default provider.
5
+ * using Mastra-native model routing with OpenRouter as the default provider.
6
6
  *
7
7
  * Architecture:
8
8
  * - For chat: use `chat.sendMessage` (preferred entry point)
9
9
  * - For programmatic agent execution: use `mastraIntegration.executeAgent`
10
- * - Model resolution: uses AI SDK providers directly (OpenRouter, OpenAI, etc.)
11
- *
12
- * This replaces the previous broken approach of dynamically importing
13
- * @mastra/core inside Convex actions. The AI SDK is the correct way to
14
- * call LLMs from Convex Node.js actions.
10
+ * - Model resolution: uses Mastra Agent with "provider/model-name" format
15
11
  */
16
12
  import { action } from "./_generated/server";
17
13
  import { v } from "convex/values";
18
14
  import { api } from "./_generated/api";
15
+ import { Agent } from "@mastra/core/agent";
19
16
 
20
17
  // Return type for executeAgent
21
18
  type ExecuteAgentResult = {
@@ -30,54 +27,6 @@ type ExecuteAgentResult = {
30
27
  };
31
28
  };
32
29
 
33
- /**
34
- * Resolve a model instance from provider + modelId using the AI SDK.
35
- *
36
- * Supports: openrouter, openai, anthropic, google, venice, custom.
37
- * Falls back to OpenRouter for unknown providers (it routes to all models).
38
- */
39
- async function resolveModel(provider: string, modelId: string) {
40
- const { createOpenAI } = await import("@ai-sdk/openai");
41
-
42
- switch (provider) {
43
- case "openai": {
44
- const openai = createOpenAI({
45
- apiKey: process.env.OPENAI_API_KEY,
46
- });
47
- return openai(modelId);
48
- }
49
-
50
- case "anthropic": {
51
- const { createAnthropic } = await import("@ai-sdk/anthropic");
52
- const anthropic = createAnthropic({
53
- apiKey: process.env.ANTHROPIC_API_KEY,
54
- });
55
- return anthropic(modelId);
56
- }
57
-
58
- case "google": {
59
- const { createGoogleGenerativeAI } = await import("@ai-sdk/google");
60
- const google = createGoogleGenerativeAI({
61
- apiKey: process.env.GEMINI_API_KEY,
62
- });
63
- return google(modelId);
64
- }
65
-
66
- case "openrouter":
67
- default: {
68
- // OpenRouter is OpenAI-compatible and routes to all providers
69
- const openrouter = createOpenAI({
70
- baseURL: "https://openrouter.ai/api/v1",
71
- apiKey: process.env.OPENROUTER_API_KEY,
72
- });
73
- const routerModelId = modelId.includes("/")
74
- ? modelId
75
- : `${provider}/${modelId}`;
76
- return openrouter(routerModelId);
77
- }
78
- }
79
- }
80
-
81
30
  /**
82
31
  * Execute an agent with a prompt and return the response.
83
32
  *
@@ -127,12 +76,10 @@ export const executeAgent = action({
127
76
  });
128
77
 
129
78
  try {
130
- const { generateText } = await import("ai");
131
-
132
79
  // Resolve the model
133
80
  const provider = agent.provider || "openrouter";
134
81
  const modelId = agent.model || "openai/gpt-4o-mini";
135
- const model = await resolveModel(provider, modelId);
82
+ const modelKey = `${provider}/${modelId}`;
136
83
 
137
84
  // Get conversation history for context
138
85
  const messages = await ctx.runQuery(api.messages.list, { threadId });
@@ -143,15 +90,15 @@ export const executeAgent = action({
143
90
  content: m.content,
144
91
  }));
145
92
 
146
- // Execute the LLM call
147
- const result = await generateText({
148
- model,
149
- system: agent.instructions || "You are a helpful AI assistant.",
150
- messages: conversationMessages,
151
- ...(agent.temperature != null && { temperature: agent.temperature }),
152
- ...(agent.maxTokens != null && { maxTokens: agent.maxTokens }),
93
+ // Execute via Mastra Agent
94
+ const mastraAgent = new Agent({
95
+ name: "agentforge-executor",
96
+ instructions: agent.instructions || "You are a helpful AI assistant.",
97
+ model: modelKey,
153
98
  });
154
99
 
100
+ const result = await mastraAgent.generate(conversationMessages);
101
+
155
102
  const responseContent = result.text;
156
103
 
157
104
  // Add assistant message to thread
@@ -38,7 +38,7 @@ export const webSearch = createTool({
38
38
  {
39
39
  name: 'code-executor',
40
40
  displayName: 'Code Executor',
41
- description: 'Safely execute JavaScript/TypeScript code snippets in a sandboxed environment and return the output.',
41
+ description: 'Execute JavaScript/TypeScript code snippets via the AgentForge sandbox API and return the output.',
42
42
  category: 'Tools',
43
43
  version: '1.0.0',
44
44
  author: 'AgentForge',
@@ -48,18 +48,25 @@ import { z } from 'zod';
48
48
 
49
49
  export const codeExecutor = createTool({
50
50
  id: 'code-executor',
51
- description: 'Execute JavaScript code and return the result',
51
+ description: 'Execute JavaScript code via the sandbox API and return the result',
52
52
  inputSchema: z.object({
53
53
  code: z.string().describe('JavaScript code to execute'),
54
+ language: z.enum(['javascript', 'typescript']).default('javascript'),
54
55
  }),
55
56
  execute: async ({ context }) => {
56
- try {
57
- const fn = new Function('return (async () => {' + context.code + '})()');
58
- const result = await fn();
59
- return { success: true, output: String(result) };
60
- } catch (error: any) {
61
- return { success: false, error: error.message };
57
+ // Code is executed server-side via the AgentForge sandbox API,
58
+ // not via eval or Function constructor, to prevent arbitrary code execution risks.
59
+ const response = await fetch('/api/sandbox/execute', {
60
+ method: 'POST',
61
+ headers: { 'Content-Type': 'application/json' },
62
+ body: JSON.stringify({ code: context.code, language: context.language }),
63
+ });
64
+ if (!response.ok) {
65
+ const err = await response.json().catch(() => ({ error: 'Unknown error' }));
66
+ return { success: false, error: err.error ?? 'Sandbox execution failed' };
62
67
  }
68
+ const data = await response.json();
69
+ return { success: true, output: String(data.output ?? '') };
63
70
  },
64
71
  });`,
65
72
  },
@@ -74,16 +81,71 @@ export const codeExecutor = createTool({
74
81
  code: `import { createTool } from '@mastra/core';
75
82
  import { z } from 'zod';
76
83
 
84
+ // Safe arithmetic evaluator — no eval or Function constructor used.
85
+ function evaluateExpression(expr: string): number {
86
+ // Allow only digits, operators, parentheses, dots, and whitespace.
87
+ if (!/^[0-9+\\-*/().%\\s]+$/.test(expr)) {
88
+ throw new Error('Expression contains invalid characters');
89
+ }
90
+ // Recursive descent parser for: expr = term (('+' | '-') term)*
91
+ let pos = 0;
92
+ const peek = () => expr[pos] ?? '';
93
+ const consume = () => expr[pos++];
94
+ const skipWs = () => { while (peek() === ' ') consume(); };
95
+
96
+ function parseNumber(): number {
97
+ skipWs();
98
+ let num = '';
99
+ if (peek() === '(') { consume(); const v = parseExpr(); skipWs(); consume(); return v; }
100
+ while (/[0-9.]/.test(peek())) num += consume();
101
+ if (num === '') throw new Error('Expected number');
102
+ return parseFloat(num);
103
+ }
104
+
105
+ function parseFactor(): number {
106
+ skipWs();
107
+ if (peek() === '-') { consume(); return -parseFactor(); }
108
+ return parseNumber();
109
+ }
110
+
111
+ function parseTerm(): number {
112
+ let left = parseFactor();
113
+ skipWs();
114
+ while (peek() === '*' || peek() === '/') {
115
+ const op = consume(); skipWs();
116
+ const right = parseFactor();
117
+ left = op === '*' ? left * right : left / right;
118
+ skipWs();
119
+ }
120
+ return left;
121
+ }
122
+
123
+ function parseExpr(): number {
124
+ let left = parseTerm();
125
+ skipWs();
126
+ while (peek() === '+' || peek() === '-') {
127
+ const op = consume(); skipWs();
128
+ const right = parseTerm();
129
+ left = op === '+' ? left + right : left - right;
130
+ skipWs();
131
+ }
132
+ return left;
133
+ }
134
+
135
+ const result = parseExpr();
136
+ if (pos !== expr.length) throw new Error('Unexpected token at position ' + pos);
137
+ return result;
138
+ }
139
+
77
140
  export const calculator = createTool({
78
141
  id: 'calculator',
79
- description: 'Evaluate a mathematical expression',
142
+ description: 'Evaluate a mathematical expression safely',
80
143
  inputSchema: z.object({
81
144
  expression: z.string().describe('Math expression to evaluate, e.g. "2 + 2 * 3"'),
82
145
  }),
83
146
  execute: async ({ context }) => {
84
147
  try {
85
- const sanitized = context.expression.replace(/[^0-9+\\-*/().%\\s]/g, '');
86
- const result = Function('"use strict"; return (' + sanitized + ')')();
148
+ const result = evaluateExpression(context.expression.trim());
87
149
  return { result: Number(result), expression: context.expression };
88
150
  } catch (error: any) {
89
151
  return { error: 'Invalid expression: ' + error.message };