diff --git a/README.md b/README.md
index a66e8c0..3bf687d 100644
--- a/README.md
+++ b/README.md
@@ -14,6 +14,8 @@ This template demonstrates how to build an AI coding assistant that can work wit
- **Live Development Monitoring**: Watch directory changes and monitor development workflows
- **Command Execution**: Run shell commands, install packages, and manage dependencies
- **Memory System**: Persistent conversation memory with semantic recall and working memory
+- **Personalization Layer**: Configure agent identity, preferences, and a catalog of repeatable workflows
+- **Desktop Launcher**: Electron-based desktop application for starting/stopping the agent without the terminal
- **Development Workflows**: Professional development patterns with build automation
## Prerequisites
@@ -29,7 +31,8 @@ This template demonstrates how to build an AI coding assistant that can work wit
```bash
git clone https://github.com/mastra-ai/template-coding-agent.git
cd template-coding-agent
- pnpm install
+ npm install
+ # or: pnpm install
```
2. **Set up environment variables:**
@@ -44,12 +47,27 @@ This template demonstrates how to build an AI coding assistant that can work wit
OPENAI_API_KEY="your-openai-api-key-here"
```
-3. **Start the development server:**
+3. **Check your environment:**
```bash
- pnpm run dev
+ npm run doctor
```
+ The doctor script highlights missing dependencies or API keys before you launch the agent.
+
+4. **Start the agent (pick one):**
+
+ - **CLI:** `npm run dev` for hot-reload development or `npm run start` for production mode.
+ - **Desktop App:** `npm run desktop` to open the Electron launcher. Provide your API keys once and start/stop the agent with buttons. Output streaming and status updates are surfaced in the UI.
+
+5. **Package a desktop build (optional):**
+
+ ```bash
+ npm run desktop:package
+ ```
+
+ This produces a platform-specific build in the `dist/` directory using `electron-builder`.
+
## Architecture
### Core Components
@@ -63,6 +81,7 @@ The main agent with comprehensive development capabilities:
- **File Operations**: Complete CRUD operations for files and directories
- **Development Monitoring**: Watches for changes and monitors workflows
- **Memory Integration**: Maintains conversation context and project history
+- **Personal Profile**: Dynamically incorporates preferences defined in `personal-config.ts`
#### **E2B Tools** (`src/mastra/tools/e2b.ts`)
@@ -98,6 +117,7 @@ Complete toolkit for sandbox interaction:
**Development Workflow:**
- `runCommand` - Execute shell commands, build scripts, package management
+- `runCustomCommand` - Trigger user-defined workflows stored in `personal-config.ts`
### Memory System
@@ -117,22 +137,56 @@ E2B_API_KEY=your_e2b_api_key_here
OPENAI_API_KEY=your_openai_api_key_here
```
-### Customization
+### Personalization & Custom Commands
-You can customize the agent behavior by modifying the instructions in `src/mastra/agents/coding-agent.ts`:
+Fine-tune the assistant without editing core agent code by updating `src/mastra/agents/personal-config.ts`.
```typescript
-export const codingAgent = new Agent({
- name: 'Coding Agent',
- instructions: `
- // Customize agent instructions here
- // Focus on specific languages, frameworks, or development patterns
- `,
- model: openai('gpt-4.1'),
- // ... other configuration
-});
+export const personalAgentConfig = {
+ name: 'My Personal Coding Companion',
+ tagline: 'A persistent AI partner tailored to your workflows.',
+ mission: 'Describe the long-term goals for the agent.',
+ preferences: {
+ languages: ['TypeScript', 'Python'],
+ technologies: ['Node.js', 'React'],
+ developmentStyle: ['Prefer TDD', 'Document architectural decisions'],
+ },
+ memoryStrategy: {
+ longTermSummary: 'What should be remembered across sessions.',
+ workingMemoryGuidelines: ['Snapshot progress at the end of each session.'],
+ },
+ customCommands: [
+ {
+ id: 'start-dev',
+ label: 'Start Dev Server',
+ description: 'Launch the development server inside the sandbox.',
+ command: 'pnpm run dev',
+ },
+ ],
+};
```
+Key capabilities provided by the configuration file:
+
+- **Agent identity** — control tone, mission, and project focus.
+- **Preference sets** — inform the agent about favored languages, tools, and delivery style.
+- **Memory strategy** — guide how long-term and short-term context should be stored.
+- **Custom commands** — register reusable shell commands that can be triggered with the `runCustomCommand` tool using a `commandId`.
+
+Feel free to extend the configuration with additional custom commands or memory options; the agent automatically reads the updated details on startup.
+
+### Desktop Launcher
+
+The Electron desktop app wraps the same Mastra agent so you can run it without the terminal:
+
+1. Install dependencies and run `npm run doctor` to verify prerequisites.
+2. Launch the desktop shell with `npm run desktop`.
+3. Enter your API keys once—the app stores them locally using the operating system's application storage.
+4. Start or stop the agent with a single click. Live logs and status updates stream into the UI so you can monitor the session in real time.
+5. Package a distributable build with `npm run desktop:package` when you are ready to share it across machines.
+
+The launcher proxies your chosen mode (`mastra dev` for development or `mastra start` for production) and forwards environment variables so the agent runs exactly as it does via the CLI.
+
## Common Issues
### "E2B_API_KEY is not set"
@@ -171,8 +225,10 @@ export const codingAgent = new Agent({
src/mastra/
agents/
coding-agent.ts # Main coding agent with development capabilities
+ personal-config.ts # User-editable persona, memory, and command catalog
tools/
e2b.ts # Complete E2B sandbox interaction toolkit
+ custom-commands.ts # Tool for invoking reusable workflows
index.ts # Mastra configuration with storage and logging
```
diff --git a/desktop/electron-main.mjs b/desktop/electron-main.mjs
new file mode 100644
index 0000000..1624584
--- /dev/null
+++ b/desktop/electron-main.mjs
@@ -0,0 +1,180 @@
+import { app, BrowserWindow, ipcMain } from 'electron';
+import { spawn } from 'node:child_process';
+import { fileURLToPath } from 'node:url';
+import path from 'node:path';
+import process from 'node:process';
+import { existsSync } from 'node:fs';
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+const projectRoot = path.resolve(__dirname, '..');
+
+let mainWindow;
+let agentProcess = null;
+let currentMode = null;
+
+const sendStatus = status => {
+ if (mainWindow && !mainWindow.isDestroyed()) {
+ mainWindow.webContents.send('agent:status', status);
+ }
+};
+
+const sendOutput = payload => {
+ if (mainWindow && !mainWindow.isDestroyed()) {
+ mainWindow.webContents.send('agent:output', payload);
+ }
+};
+
+const resolveRendererPath = () => {
+ const rendererUrl = new URL('./renderer/index.html', import.meta.url);
+ return fileURLToPath(rendererUrl);
+};
+
+const resolvePreloadPath = () => {
+ const preloadUrl = new URL('./preload.mjs', import.meta.url);
+ return fileURLToPath(preloadUrl);
+};
+
+const createWindow = () => {
+ mainWindow = new BrowserWindow({
+ width: 1100,
+ height: 720,
+ title: 'Coding Agent Desktop',
+ webPreferences: {
+ preload: resolvePreloadPath(),
+ },
+ });
+
+ mainWindow.loadFile(resolveRendererPath());
+
+ mainWindow.on('closed', () => {
+ mainWindow = null;
+ });
+};
+
+const startAgentProcess = (mode, extraEnv) => {
+ if (agentProcess) {
+ return { ok: false, message: 'The agent is already running.' };
+ }
+
+ const mastraBinary = path.join(
+ projectRoot,
+ 'node_modules',
+ '.bin',
+ process.platform === 'win32' ? 'mastra.cmd' : 'mastra',
+ );
+
+ if (!existsSync(mastraBinary)) {
+ return {
+ ok: false,
+ message:
+ 'Could not locate the Mastra CLI. Install dependencies first with "npm install" or "pnpm install".',
+ };
+ }
+
+ const commandArgs = mode === 'dev' ? ['dev'] : ['start'];
+
+ const spawnOptions = {
+ cwd: projectRoot,
+ env: { ...process.env, ...extraEnv },
+ shell: process.platform === 'win32',
+ };
+
+ try {
+ agentProcess = spawn(mastraBinary, commandArgs, spawnOptions);
+ } catch (error) {
+ return { ok: false, message: `Failed to launch Mastra: ${error.message}` };
+ }
+
+ currentMode = mode;
+ sendStatus({ state: 'running', mode });
+
+ const forward = (type, data) => {
+ sendOutput({ type, data: data.toString(), timestamp: Date.now() });
+ };
+
+ agentProcess.stdout?.on('data', chunk => forward('stdout', chunk));
+ agentProcess.stderr?.on('data', chunk => forward('stderr', chunk));
+
+ agentProcess.on('exit', code => {
+ sendOutput({ type: 'event', data: `Agent exited with code ${code ?? 'unknown'}`, timestamp: Date.now() });
+ currentMode = null;
+ sendStatus({ state: 'stopped' });
+ agentProcess = null;
+ });
+
+ agentProcess.on('error', error => {
+ sendOutput({ type: 'error', data: error.message, timestamp: Date.now() });
+ currentMode = null;
+ sendStatus({ state: 'stopped' });
+ agentProcess = null;
+ });
+
+ return { ok: true };
+};
+
+const stopAgentProcess = () => {
+ if (!agentProcess) {
+ return { ok: false, message: 'The agent is not running.' };
+ }
+
+ sendStatus({ state: 'stopping', mode: currentMode });
+
+ if (process.platform === 'win32') {
+ agentProcess.kill();
+ } else {
+ agentProcess.kill('SIGINT');
+ setTimeout(() => agentProcess?.kill('SIGKILL'), 4000);
+ }
+
+ return { ok: true };
+};
+
+app.whenReady().then(() => {
+ createWindow();
+
+ app.on('activate', () => {
+ if (BrowserWindow.getAllWindows().length === 0) {
+ createWindow();
+ }
+ });
+});
+
+app.on('window-all-closed', () => {
+ if (process.platform !== 'darwin') {
+ app.quit();
+ }
+});
+
+app.on('before-quit', () => {
+ if (agentProcess) {
+ stopAgentProcess();
+ }
+});
+
+ipcMain.handle('agent:start', async (_event, payload) => {
+ const { mode, env } = payload ?? {};
+ if (!mode || (mode !== 'dev' && mode !== 'start')) {
+ return { ok: false, message: 'Invalid mode. Choose either "dev" or "start".' };
+ }
+
+ const result = startAgentProcess(mode, env ?? {});
+ if (!result.ok) {
+ sendStatus({ state: 'stopped' });
+ }
+ return result;
+});
+
+ipcMain.handle('agent:stop', async () => {
+ const result = stopAgentProcess();
+ if (!result.ok) {
+ return result;
+ }
+ return { ok: true };
+});
+
+ipcMain.handle('agent:status', async () => {
+ if (!agentProcess) {
+ return { state: 'stopped' };
+ }
+ return { state: 'running', mode: currentMode };
+});
diff --git a/desktop/preload.mjs b/desktop/preload.mjs
new file mode 100644
index 0000000..8dd274b
--- /dev/null
+++ b/desktop/preload.mjs
@@ -0,0 +1,17 @@
+import { contextBridge, ipcRenderer } from 'electron';
+
+contextBridge.exposeInMainWorld('codingAgentDesktop', {
+ startAgent: options => ipcRenderer.invoke('agent:start', options),
+ stopAgent: () => ipcRenderer.invoke('agent:stop'),
+ getStatus: () => ipcRenderer.invoke('agent:status'),
+ onOutput: callback => {
+ const listener = (_event, payload) => callback(payload);
+ ipcRenderer.on('agent:output', listener);
+ return () => ipcRenderer.removeListener('agent:output', listener);
+ },
+ onStatus: callback => {
+ const listener = (_event, payload) => callback(payload);
+ ipcRenderer.on('agent:status', listener);
+ return () => ipcRenderer.removeListener('agent:status', listener);
+ },
+});
diff --git a/desktop/renderer/index.html b/desktop/renderer/index.html
new file mode 100644
index 0000000..c0277a0
--- /dev/null
+++ b/desktop/renderer/index.html
@@ -0,0 +1,68 @@
+
+
+
+
+
+ Coding Agent Desktop
+
+
+
+
+
+
+
+ Environment
+
+ Status: stopped
+
+
+
+
+
+
+
+
+
+
diff --git a/desktop/renderer/renderer.js b/desktop/renderer/renderer.js
new file mode 100644
index 0000000..42110aa
--- /dev/null
+++ b/desktop/renderer/renderer.js
@@ -0,0 +1,122 @@
+const outputLog = document.getElementById('output-log');
+const statusText = document.getElementById('status-text');
+const startButton = document.getElementById('start-button');
+const stopButton = document.getElementById('stop-button');
+const form = document.getElementById('config-form');
+const modeSelect = document.getElementById('mode');
+const e2bInput = document.getElementById('e2b-key');
+const openaiInput = document.getElementById('openai-key');
+
+const STORAGE_KEY = 'coding-agent-desktop-config';
+
+const persistConfig = config => {
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(config));
+};
+
+const loadConfig = () => {
+ try {
+ const raw = localStorage.getItem(STORAGE_KEY);
+ if (!raw) return null;
+ return JSON.parse(raw);
+ } catch (error) {
+ console.warn('Failed to load stored config', error);
+ return null;
+ }
+};
+
+const appendLogEntry = ({ type, data, timestamp }) => {
+ const entry = document.createElement('pre');
+ entry.className = `log-entry ${type}`;
+ const time = new Date(timestamp);
+ const prefix = `[${time.toLocaleTimeString()}]`;
+ entry.textContent = `${prefix} ${data}`;
+ outputLog.append(entry);
+ outputLog.scrollTop = outputLog.scrollHeight;
+};
+
+const setStatus = status => {
+ statusText.textContent = `Status: ${status}`;
+};
+
+const disableFormWhileRunning = isRunning => {
+ startButton.disabled = isRunning;
+ modeSelect.disabled = isRunning;
+ e2bInput.disabled = isRunning;
+ openaiInput.disabled = isRunning;
+ stopButton.disabled = !isRunning;
+};
+
+const hydrateForm = () => {
+ const config = loadConfig();
+ if (!config) return;
+ if (config.mode) {
+ modeSelect.value = config.mode;
+ }
+ if (config.e2bKey) {
+ e2bInput.value = config.e2bKey;
+ }
+ if (config.openaiKey) {
+ openaiInput.value = config.openaiKey;
+ }
+};
+
+hydrateForm();
+
+disableFormWhileRunning(false);
+
+window.codingAgentDesktop.onOutput(payload => {
+ appendLogEntry(payload);
+});
+
+window.codingAgentDesktop.onStatus(payload => {
+ const { state, mode } = payload ?? {};
+ if (state === 'running') {
+ setStatus(`running (${mode ?? 'unknown'})`);
+ disableFormWhileRunning(true);
+ } else if (state === 'stopping') {
+ setStatus('stopping…');
+ } else {
+ setStatus('stopped');
+ disableFormWhileRunning(false);
+ }
+});
+
+form.addEventListener('submit', async event => {
+ event.preventDefault();
+
+ const mode = modeSelect.value;
+ const e2bKey = e2bInput.value.trim();
+ const openaiKey = openaiInput.value.trim();
+
+ persistConfig({ mode, e2bKey, openaiKey });
+
+ disableFormWhileRunning(true);
+ setStatus('starting…');
+
+ const env = {};
+ if (e2bKey) env.E2B_API_KEY = e2bKey;
+ if (openaiKey) env.OPENAI_API_KEY = openaiKey;
+
+ const result = await window.codingAgentDesktop.startAgent({ mode, env });
+ if (!result?.ok) {
+ disableFormWhileRunning(false);
+ setStatus('stopped');
+ appendLogEntry({ type: 'error', data: result?.message ?? 'Unable to start agent', timestamp: Date.now() });
+ }
+});
+
+stopButton.addEventListener('click', async () => {
+ stopButton.disabled = true;
+ const result = await window.codingAgentDesktop.stopAgent();
+ if (!result?.ok) {
+ appendLogEntry({ type: 'error', data: result?.message ?? 'Agent was not running', timestamp: Date.now() });
+ }
+});
+
+window.codingAgentDesktop.getStatus().then(status => {
+ const { state, mode } = status ?? {};
+ if (state === 'running') {
+ setStatus(`running (${mode ?? 'unknown'})`);
+ disableFormWhileRunning(true);
+ }
+});
diff --git a/desktop/renderer/styles.css b/desktop/renderer/styles.css
new file mode 100644
index 0000000..5efa5f7
--- /dev/null
+++ b/desktop/renderer/styles.css
@@ -0,0 +1,195 @@
+:root {
+ color-scheme: dark;
+ font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
+ background-color: #111827;
+ color: #f9fafb;
+}
+
+body {
+ margin: 0;
+ display: grid;
+ grid-template-columns: 2fr 1fr;
+ grid-template-areas:
+ 'header header'
+ 'main aside';
+ min-height: 100vh;
+}
+
+header {
+ grid-area: header;
+ padding: 24px 32px 16px;
+ background: linear-gradient(135deg, #1f2937 0%, #111827 100%);
+ border-bottom: 1px solid #1f2937;
+}
+
+header h1 {
+ margin: 0 0 8px;
+ font-size: 28px;
+}
+
+header .tagline {
+ margin: 0;
+ color: #9ca3af;
+}
+
+main {
+ grid-area: main;
+ padding: 24px 32px;
+ display: grid;
+ gap: 24px;
+ grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
+}
+
+aside {
+ grid-area: aside;
+ padding: 24px 32px;
+ background: #0f172a;
+ border-left: 1px solid #1e293b;
+}
+
+h2 {
+ margin-top: 0;
+ font-size: 20px;
+}
+
+.configuration,
+.output {
+ background: #1f2937;
+ border: 1px solid #374151;
+ border-radius: 16px;
+ padding: 24px;
+ box-shadow: 0 20px 60px rgba(15, 23, 42, 0.45);
+}
+
+.field {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ margin-bottom: 16px;
+}
+
+label {
+ font-weight: 600;
+ color: #d1d5db;
+}
+
+input,
+select {
+ background: #111827;
+ border: 1px solid #374151;
+ border-radius: 10px;
+ padding: 10px 12px;
+ color: #f9fafb;
+}
+
+input:focus,
+select:focus {
+ outline: 2px solid #3b82f6;
+ outline-offset: 2px;
+}
+
+.actions {
+ display: flex;
+ gap: 12px;
+ margin-top: 8px;
+}
+
+button {
+ appearance: none;
+ border: none;
+ border-radius: 10px;
+ padding: 10px 18px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: transform 0.15s ease, box-shadow 0.15s ease;
+}
+
+button:disabled {
+ opacity: 0.4;
+ cursor: not-allowed;
+}
+
+button#start-button {
+ background: linear-gradient(135deg, #2563eb, #3b82f6);
+ color: #f9fafb;
+ box-shadow: 0 10px 30px rgba(59, 130, 246, 0.35);
+}
+
+button#start-button:hover:not(:disabled) {
+ transform: translateY(-1px);
+ box-shadow: 0 14px 40px rgba(59, 130, 246, 0.45);
+}
+
+button.secondary {
+ background: #374151;
+ color: #e5e7eb;
+}
+
+.status {
+ margin-top: 12px;
+ color: #9ca3af;
+}
+
+.log {
+ background: #0f172a;
+ border-radius: 12px;
+ padding: 16px;
+ height: 360px;
+ overflow-y: auto;
+ font-family: 'Fira Code', 'SFMono-Regular', ui-monospace, monospace;
+ font-size: 13px;
+ border: 1px solid #1e293b;
+}
+
+.log-entry {
+ margin: 0 0 8px;
+ white-space: pre-wrap;
+ word-break: break-word;
+}
+
+.log-entry.stdout {
+ color: #a5b4fc;
+}
+
+.log-entry.stderr {
+ color: #fca5a5;
+}
+
+.log-entry.event {
+ color: #34d399;
+}
+
+.log-entry.error {
+ color: #f87171;
+}
+
+.tips {
+ color: #e5e7eb;
+}
+
+.tips ul {
+ padding-left: 18px;
+ margin: 8px 0 0;
+ color: #cbd5f5;
+}
+
+code {
+ background: rgba(148, 163, 184, 0.1);
+ padding: 2px 6px;
+ border-radius: 6px;
+}
+
+@media (max-width: 1080px) {
+ body {
+ grid-template-columns: 1fr;
+ grid-template-areas:
+ 'header'
+ 'main'
+ 'aside';
+ }
+
+ aside {
+ border-left: none;
+ border-top: 1px solid #1e293b;
+ }
+}
diff --git a/package.json b/package.json
index 542f309..c1309e4 100644
--- a/package.json
+++ b/package.json
@@ -2,12 +2,16 @@
"name": "coding-agent",
"version": "0.2.0",
"description": "Advanced Mastra AI coding agent with secure E2B sandbox execution, comprehensive file management, and multi-language support for Python, JavaScript, and TypeScript development workflows",
- "main": "index.js",
+ "main": "desktop/electron-main.mjs",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "mastra dev",
"build": "mastra build",
- "start": "mastra start"
+ "start": "mastra start",
+ "lint": "tsc --noEmit",
+ "doctor": "node scripts/doctor.mjs",
+ "desktop": "electron .",
+ "desktop:package": "electron-builder --dir"
},
"keywords": [],
"author": "",
@@ -28,8 +32,25 @@
"zod": "^3.25.76"
},
"devDependencies": {
+ "@types/electron": "^1.6.0",
"@types/node": "^24.1.0",
+ "electron": "^31.3.1",
+ "electron-builder": "^24.13.3",
"mastra": "latest",
"typescript": "^5.8.3"
+ },
+ "build": {
+ "appId": "com.mastra.codingagent.desktop",
+ "productName": "Coding Agent Desktop",
+ "files": [
+ "dist/**/*",
+ "desktop/**/*",
+ "src/**/*",
+ "package.json",
+ "tsconfig.json"
+ ],
+ "extraMetadata": {
+ "main": "desktop/electron-main.mjs"
+ }
}
}
diff --git a/scripts/doctor.mjs b/scripts/doctor.mjs
new file mode 100644
index 0000000..0711c3f
--- /dev/null
+++ b/scripts/doctor.mjs
@@ -0,0 +1,74 @@
+#!/usr/bin/env node
+import { execSync } from 'node:child_process';
+import { existsSync, readFileSync } from 'node:fs';
+import path from 'node:path';
+import process from 'node:process';
+
+const projectRoot = path.resolve(process.cwd());
+const issues = [];
+const warnings = [];
+
+const requiredNodeMajor = 20;
+const currentNodeMajor = Number.parseInt(process.versions.node.split('.')[0] ?? '0', 10);
+if (Number.isFinite(currentNodeMajor) && currentNodeMajor < requiredNodeMajor) {
+ issues.push(
+ `Node.js ${requiredNodeMajor}.x or newer is required. Detected ${process.versions.node}. Please upgrade Node.js.`,
+ );
+}
+
+const envPath = path.join(projectRoot, '.env');
+if (!existsSync(envPath)) {
+ warnings.push('No .env file found. Copy .env.example to .env and supply your API keys.');
+} else {
+ const envContents = readFileSync(envPath, 'utf8');
+ if (!envContents.includes('E2B_API_KEY')) {
+ warnings.push('E2B_API_KEY is missing from .env. The sandbox integration will not work without it.');
+ }
+ if (!envContents.includes('OPENAI_API_KEY')) {
+ warnings.push('OPENAI_API_KEY is missing from .env. The agent will be unable to call the OpenAI API.');
+ }
+}
+
+const nodeModulesPath = path.join(projectRoot, 'node_modules');
+if (!existsSync(nodeModulesPath)) {
+ issues.push('Dependencies are not installed. Run "npm install" or "pnpm install" first.');
+}
+
+const checkBinary = binary => {
+ try {
+ execSync(`${binary} --version`, { stdio: 'ignore' });
+ return true;
+ } catch {
+ return false;
+ }
+};
+
+if (!checkBinary('mastra')) {
+ const mastraBin = path.join(projectRoot, 'node_modules', '.bin', process.platform === 'win32' ? 'mastra.cmd' : 'mastra');
+ if (!existsSync(mastraBin)) {
+ warnings.push('The Mastra CLI is unavailable on PATH. Use npm scripts (e.g., "npm run dev") to start the agent.');
+ }
+}
+
+const summary = [];
+if (issues.length === 0) {
+ summary.push('Environment check passed with no blocking issues.');
+} else {
+ summary.push('Environment check detected blocking issues:');
+ for (const issue of issues) {
+ summary.push(` • ${issue}`);
+ }
+}
+
+if (warnings.length) {
+ summary.push('Warnings:');
+ for (const warning of warnings) {
+ summary.push(` • ${warning}`);
+ }
+}
+
+console.log(summary.join('\n'));
+
+if (issues.length > 0) {
+ process.exitCode = 1;
+}
diff --git a/src/mastra/agents/coding-agent.ts b/src/mastra/agents/coding-agent.ts
index e5a32bf..bda2064 100644
--- a/src/mastra/agents/coding-agent.ts
+++ b/src/mastra/agents/coding-agent.ts
@@ -18,10 +18,10 @@ import {
writeFiles,
} from '../tools/e2b';
import { fastembed } from '@mastra/fastembed';
+import { runCustomCommand } from '../tools/custom-commands';
+import { personalAgentConfig } from './personal-config';
-export const codingAgent = new Agent({
- name: 'Coding Agent',
- instructions: `
+const baseInstructions = `
# Mastra Coding Agent for E2B Sandboxes
You are an advanced coding agent that plans, writes, executes, and iterates on code in secure, isolated E2B sandboxes with comprehensive file management, live monitoring, and development workflow capabilities.
@@ -186,7 +186,130 @@ For sophisticated projects, leverage:
- **Deployment preparation** and distribution packaging
Remember: You are not just a code executor, but a complete development environment that can handle sophisticated, multi-file projects with professional development workflows and comprehensive monitoring capabilities.
-`,
+`;
+
+const buildPersonalizedInstructions = (): string => {
+ const lines: string[] = [];
+
+ lines.push('## Personalized Configuration');
+ if (personalAgentConfig.name || personalAgentConfig.tagline) {
+ const identityParts = [personalAgentConfig.name, personalAgentConfig.tagline].filter(Boolean).join(' — ');
+ if (identityParts) {
+ lines.push(`- **Identity**: ${identityParts}`);
+ }
+ }
+ if (personalAgentConfig.mission) {
+ lines.push(`- **Mission**: ${personalAgentConfig.mission}`);
+ }
+
+ const { preferences } = personalAgentConfig;
+ if (
+ preferences?.languages?.length ||
+ preferences?.technologies?.length ||
+ preferences?.developmentStyle?.length ||
+ preferences?.goals?.length
+ ) {
+ lines.push('');
+ lines.push('### Preference Highlights');
+ if (preferences.languages?.length) {
+ lines.push(`- Preferred languages: ${preferences.languages.join(', ')}`);
+ }
+ if (preferences.technologies?.length) {
+ lines.push(`- Preferred technologies: ${preferences.technologies.join(', ')}`);
+ }
+ if (preferences.developmentStyle?.length) {
+ lines.push('- Development style guidelines:');
+ lines.push(...preferences.developmentStyle.map(style => ` - ${style}`));
+ }
+ if (preferences.goals?.length) {
+ lines.push('- Long-term goals:');
+ lines.push(...preferences.goals.map(goal => ` - ${goal}`));
+ }
+ }
+
+ const { memoryStrategy } = personalAgentConfig;
+ if (
+ memoryStrategy?.longTermSummary ||
+ memoryStrategy?.longTermFocusAreas?.length ||
+ memoryStrategy?.shortTermSummary ||
+ memoryStrategy?.workingMemoryGuidelines?.length
+ ) {
+ lines.push('');
+ lines.push('### Memory Strategy');
+ if (memoryStrategy.longTermSummary) {
+ lines.push(`- Long-term memory: ${memoryStrategy.longTermSummary}`);
+ }
+ if (memoryStrategy.longTermFocusAreas?.length) {
+ lines.push('- Focus areas for long-term storage:');
+ lines.push(...memoryStrategy.longTermFocusAreas.map(area => ` - ${area}`));
+ }
+ if (memoryStrategy.shortTermSummary) {
+ lines.push(`- Short-term memory: ${memoryStrategy.shortTermSummary}`);
+ }
+ if (memoryStrategy.workingMemoryGuidelines?.length) {
+ lines.push('- Working memory best practices:');
+ lines.push(...memoryStrategy.workingMemoryGuidelines.map(guideline => ` - ${guideline}`));
+ }
+ }
+
+ if (personalAgentConfig.additionalInstructions?.length) {
+ lines.push('');
+ lines.push('### Additional Personal Instructions');
+ lines.push(...personalAgentConfig.additionalInstructions.map(instruction => `- ${instruction}`));
+ }
+
+ if (personalAgentConfig.customCommands?.length) {
+ lines.push('');
+ lines.push('### Custom Command Catalog');
+ lines.push('- Use `runCustomCommand` with the commandId to invoke these workflows.');
+ for (const command of personalAgentConfig.customCommands) {
+ const labelSuffix = command.label ? ` (${command.label})` : '';
+ lines.push(`- **${command.id}**${labelSuffix}: ${command.description}`);
+ }
+ }
+
+ return lines.join('\n');
+};
+
+const personalizedInstructions = buildPersonalizedInstructions();
+const finalInstructions = `${baseInstructions}${personalizedInstructions ? `\n\n${personalizedInstructions}` : ''}`;
+
+const mergeMemoryOptions = (
+ defaults: Record,
+ overrides?: Record,
+): Record => {
+ if (!overrides) {
+ return { ...defaults };
+ }
+
+ const merged: Record = { ...defaults, ...overrides };
+
+ const defaultThreads = (defaults.threads ?? {}) as Record;
+ const overrideThreads = (overrides.threads ?? {}) as Record;
+ if (Object.keys(defaultThreads).length || Object.keys(overrideThreads).length) {
+ merged.threads = { ...defaultThreads, ...overrideThreads };
+ }
+
+ const defaultWorkingMemory = (defaults.workingMemory ?? {}) as Record;
+ const overrideWorkingMemory = (overrides.workingMemory ?? {}) as Record;
+ if (Object.keys(defaultWorkingMemory).length || Object.keys(overrideWorkingMemory).length) {
+ merged.workingMemory = { ...defaultWorkingMemory, ...overrideWorkingMemory };
+ }
+
+ return merged;
+};
+
+const defaultMemoryOptions: Record = {
+ threads: { generateTitle: true },
+ semanticRecall: true,
+ workingMemory: { enabled: true },
+};
+
+const memoryOptions = mergeMemoryOptions(defaultMemoryOptions, personalAgentConfig.memoryOptions);
+
+export const codingAgent = new Agent({
+ name: personalAgentConfig.name || 'Coding Agent',
+ instructions: finalInstructions,
model: openai('gpt-4.1'),
tools: {
createSandbox,
@@ -202,14 +325,11 @@ Remember: You are not just a code executor, but a complete development environme
getFileSize,
watchDirectory,
runCommand,
+ runCustomCommand,
},
memory: new Memory({
storage: new LibSQLStore({ url: 'file:../../mastra.db' }),
- options: {
- threads: { generateTitle: true },
- semanticRecall: true,
- workingMemory: { enabled: true },
- },
+ options: memoryOptions,
embedder: fastembed,
vector: new LibSQLVector({ connectionUrl: 'file:../../mastra.db' }),
}),
diff --git a/src/mastra/agents/personal-config.ts b/src/mastra/agents/personal-config.ts
new file mode 100644
index 0000000..fcaf785
--- /dev/null
+++ b/src/mastra/agents/personal-config.ts
@@ -0,0 +1,144 @@
+export interface CustomCommandConfig {
+ /** Unique identifier used when calling the command via the runCustomCommand tool. */
+ id: string;
+ /**
+ * Human-readable label for the command.
+ * This is used for documentation and may be surfaced in the agent instructions.
+ */
+ label: string;
+ /** Description that explains what the command does. */
+ description: string;
+ /** The shell command that will be executed in the sandbox. */
+ command: string;
+ /** Optional working directory used when executing the command. */
+ workingDirectory?: string;
+ /**
+ * Timeout in milliseconds. When omitted the tool defaults to the sandbox
+ * standard timeout (30 seconds).
+ */
+ timeoutMs?: number;
+}
+
+export interface PersonalAgentPreferences {
+ /** Preferred programming languages. */
+ languages?: string[];
+ /** Frameworks, libraries, or tools the agent should prioritize. */
+ technologies?: string[];
+ /** General development style tips. */
+ developmentStyle?: string[];
+ /** Additional high level instructions. */
+ goals?: string[];
+}
+
+export interface MemoryStrategyConfig {
+ /** Description of how long-term memory should be utilized. */
+ longTermSummary?: string;
+ /** Topics or categories that should be prioritized when storing long-term memory. */
+ longTermFocusAreas?: string[];
+ /** Description for how the agent should handle short-term working memory. */
+ shortTermSummary?: string;
+ /** Guidance for when to snapshot or summarize context. */
+ workingMemoryGuidelines?: string[];
+}
+
+export interface PersonalAgentConfig {
+ /** Friendly name for the agent. */
+ name: string;
+ /** Short tagline or mission statement. */
+ tagline: string;
+ /** High-level description of the agent's purpose for the user. */
+ mission: string;
+ /** Collection of preference values to customize the agent behaviour. */
+ preferences?: PersonalAgentPreferences;
+ /** Memory instructions used to guide how state should be handled. */
+ memoryStrategy?: MemoryStrategyConfig;
+ /**
+ * Extra instructions appended to the agent prompt.
+ * Useful for keeping a personal tone of voice or project-specific rules.
+ */
+ additionalInstructions?: string[];
+ /**
+ * Optional overrides for the Mastra Memory options.
+ * This object is shallowly merged with the defaults in coding-agent.ts so that
+ * advanced users can tweak the behaviour without modifying agent code.
+ */
+ memoryOptions?: Record;
+ /** A catalogue of custom commands available via the runCustomCommand tool. */
+ customCommands?: CustomCommandConfig[];
+}
+
+export const personalAgentConfig: PersonalAgentConfig = {
+ name: 'My Personal Coding Companion',
+ tagline: 'A persistent AI partner tailored to your workflows.',
+ mission:
+ 'Deliver thoughtful coding assistance, maintain long-term knowledge about ongoing projects, and execute repeatable workflows efficiently.',
+ preferences: {
+ languages: ['TypeScript', 'Python'],
+ technologies: ['Node.js', 'React', 'Docker'],
+ developmentStyle: [
+ 'Prioritize clean, well-documented code with strong typing.',
+ 'Value incremental delivery with frequent validation.',
+ 'Prefer automating repetitive tasks when possible.',
+ ],
+ goals: [
+ 'Continuously surface context from prior sessions when relevant.',
+ 'Document significant project decisions for future reference.',
+ 'Seek clarification when requirements feel ambiguous.',
+ ],
+ },
+ memoryStrategy: {
+ longTermSummary:
+ 'Leverage the LibSQL-backed semantic memory to retain durable knowledge: project architecture, key decisions, recurring preferences, and workflows.',
+ longTermFocusAreas: [
+ 'Project-specific coding conventions and style guides.',
+ 'Preferred tooling, scripts, and infrastructure setups.',
+ 'Historical troubleshooting steps and successful resolutions.',
+ ],
+ shortTermSummary:
+ 'Maintain a detailed working memory to handle active tasks, summarizing new findings before they exit the short-term context window.',
+ workingMemoryGuidelines: [
+ 'Summarize or snapshot progress after completing major milestones.',
+ 'Capture rationale for architectural or tooling choices.',
+ 'Highlight outstanding questions or follow-up actions.',
+ ],
+ },
+ additionalInstructions: [
+ 'Always provide concise reasoning before running destructive commands.',
+ 'When appropriate, create lightweight documentation updates alongside code changes.',
+ 'Encourage a teaching tone when explaining complex concepts.',
+ ],
+ memoryOptions: {
+ workingMemory: {
+ enabled: true,
+ maxTokens: 2048,
+ },
+ semanticRecall: true,
+ threads: {
+ generateTitle: true,
+ tagWithDate: true,
+ },
+ },
+ customCommands: [
+ {
+ id: 'start-dev',
+ label: 'Start Development Server',
+ description: 'Launch the dev server using pnpm.',
+ command: 'pnpm run dev',
+ timeoutMs: 120000,
+ },
+ {
+ id: 'lint-project',
+ label: 'Run Project Linting',
+ description: 'Execute the linting workflow for the current repository.',
+ command: 'pnpm run lint',
+ },
+ {
+ id: 'project-status',
+ label: 'Git Project Status',
+ description: 'Check repository status and show a short summary of recent activity.',
+ command: 'git status --short && git log -3 --oneline',
+ workingDirectory: '.',
+ timeoutMs: 30000,
+ },
+ ],
+};
diff --git a/src/mastra/tools/custom-commands.ts b/src/mastra/tools/custom-commands.ts
new file mode 100644
index 0000000..603ff37
--- /dev/null
+++ b/src/mastra/tools/custom-commands.ts
@@ -0,0 +1,69 @@
+import { createTool } from '@mastra/core/tools';
+import z from 'zod';
+import { Sandbox } from '@e2b/code-interpreter';
+import { personalAgentConfig } from '../agents/personal-config';
+
+const customCommands = personalAgentConfig.customCommands ?? [];
+const commandsById = new Map(customCommands.map(command => [command.id, command]));
+
+export const runCustomCommand = createTool({
+ id: 'runCustomCommand',
+ description:
+ 'Execute a user-defined custom command in the connected E2B sandbox. Commands are configured in src/mastra/agents/personal-config.ts.',
+ inputSchema: z.object({
+ sandboxId: z.string().describe('The sandboxId for the sandbox to run the custom command in'),
+ commandId: z.string().describe('Unique identifier for the command defined in personal-config.ts'),
+ args: z.array(z.string()).optional().describe('Additional arguments appended to the command string'),
+ workingDirectory: z.string().optional().describe('Override the configured working directory'),
+ timeoutMs: z.number().optional().describe('Override the configured timeout in milliseconds'),
+ }),
+ outputSchema: z
+ .object({
+ success: z.boolean().describe('Whether the command executed successfully'),
+ exitCode: z.number().describe('The exit code of the command'),
+ stdout: z.string().describe('The standard output from the command'),
+ stderr: z.string().describe('The standard error from the command'),
+ command: z.string().describe('The command string that was executed'),
+ executionTime: z.number().describe('How long the command took to execute in milliseconds'),
+ })
+ .or(
+ z.object({
+ error: z.string().describe('The error from a failed command execution'),
+ }),
+ ),
+ execute: async ({ context }) => {
+ const commandConfig = commandsById.get(context.commandId);
+
+ if (!commandConfig) {
+ return {
+ error: `Custom command "${context.commandId}" is not defined. Update personal-config.ts to add it.`,
+ };
+ }
+
+ try {
+ const sandbox = await Sandbox.connect(context.sandboxId);
+ const commandArgs = context.args?.join(' ') ?? '';
+ const commandToRun = [commandConfig.command, commandArgs].filter(Boolean).join(' ').trim();
+
+ const startTime = Date.now();
+ const result = await sandbox.commands.run(commandToRun, {
+ cwd: context.workingDirectory ?? commandConfig.workingDirectory,
+ timeoutMs: context.timeoutMs ?? commandConfig.timeoutMs ?? 30000,
+ });
+ const executionTime = Date.now() - startTime;
+
+ return {
+ success: result.exitCode === 0,
+ exitCode: result.exitCode,
+ stdout: result.stdout,
+ stderr: result.stderr,
+ command: commandToRun,
+ executionTime,
+ };
+ } catch (error) {
+ return {
+ error: JSON.stringify(error, Object.getOwnPropertyNames(error)),
+ };
+ }
+ },
+});