@agenticmail/enterprise 0.5.25 → 0.5.27

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/dist/cli.js CHANGED
@@ -48,7 +48,7 @@ Skill Development:
48
48
  break;
49
49
  case "setup":
50
50
  default:
51
- import("./setup-QZYRQ4CL.js").then((m) => m.runSetupWizard()).catch(fatal);
51
+ import("./setup-AYFBHCHB.js").then((m) => m.runSetupWizard()).catch(fatal);
52
52
  break;
53
53
  }
54
54
  function fatal(err) {
@@ -31,7 +31,7 @@ export function DeployModal({ agentId, agentConfig, onClose, onDeployed, toast }
31
31
  const doDeploy = async () => {
32
32
  setError(''); setLoading(true);
33
33
  try {
34
- await engineCall('/agents/' + agentId + '/deploy', { method: 'POST', body: JSON.stringify({ targetType: targetType, credentialId: selectedCred || undefined, config: config, deployedBy: 'dashboard' }) });
34
+ await apiCall('/agents/' + agentId + '/deploy', { method: 'POST', body: JSON.stringify({ targetType: targetType, credentialId: selectedCred || undefined, config: config, deployedBy: 'dashboard' }) });
35
35
  if (toast) toast('Deployment started', 'success');
36
36
  if (onDeployed) onDeployed();
37
37
  onClose();
@@ -233,7 +233,7 @@ export function DeploymentProgress({ agentId, onComplete }) {
233
233
  export function CreateAgentWizard({ onClose, onCreated, toast }) {
234
234
  const [step, setStep] = useState(0);
235
235
  const steps = ['Role', 'Basics', 'Persona', 'Skills', 'Permissions', 'Deployment', 'Review'];
236
- const [form, setForm] = useState({ name: '', email: '', role: 'assistant', description: '', personality: '', skills: [], preset: null, customTools: { allowed: [], blocked: [] }, deployTarget: 'docker', knowledgeBases: [], provider: '', model: '', approvalRequired: true, soulId: null, avatar: null, gender: '', dateOfBirth: '', maritalStatus: '', culturalBackground: '', language: 'en-us', autoOnboard: true, maxRiskLevel: 'medium', blockedSideEffects: ['runs-code', 'deletes-data', 'financial', 'controls-device'], approvalForRiskLevels: ['high', 'critical'], approvalForSideEffects: ['sends-email', 'sends-message'], rateLimits: { toolCallsPerMinute: 30, toolCallsPerHour: 500, toolCallsPerDay: 5000, externalActionsPerHour: 50 }, constraints: { maxConcurrentTasks: 5, maxSessionDurationMinutes: 480, sandboxMode: false }, traits: { communication: 'direct', detail: 'detail-oriented', energy: 'calm', humor: 'warm', formality: 'adaptive', empathy: 'moderate', patience: 'patient', creativity: 'creative' } });
236
+ const [form, setForm] = useState({ name: '', email: '', role: 'assistant', description: '', personality: '', skills: [], preset: null, customTools: { allowed: [], blocked: [] }, deployTarget: 'fly', knowledgeBases: [], provider: '', model: '', approvalRequired: true, soulId: null, avatar: null, gender: '', dateOfBirth: '', maritalStatus: '', culturalBackground: '', language: 'en-us', autoOnboard: true, maxRiskLevel: 'medium', blockedSideEffects: ['runs-code', 'deletes-data', 'financial', 'controls-device'], approvalForRiskLevels: ['high', 'critical'], approvalForSideEffects: ['sends-email', 'sends-message'], rateLimits: { toolCallsPerMinute: 30, toolCallsPerHour: 500, toolCallsPerDay: 5000, externalActionsPerHour: 50 }, constraints: { maxConcurrentTasks: 5, maxSessionDurationMinutes: 480, sandboxMode: false }, traits: { communication: 'direct', detail: 'detail-oriented', energy: 'calm', humor: 'warm', formality: 'adaptive', empathy: 'moderate', patience: 'patient', creativity: 'creative' } });
237
237
  const [allSkills, setAllSkills] = useState({});
238
238
  const [providers, setProviders] = useState([]);
239
239
  const [providerModels, setProviderModels] = useState([]);
@@ -245,6 +245,38 @@ export function CreateAgentWizard({ onClose, onCreated, toast }) {
245
245
  const [previewOpen, setPreviewOpen] = useState(false);
246
246
  const [loading, setLoading] = useState(false);
247
247
  const [showSetupGuide, setShowSetupGuide] = useState(false);
248
+ const [draftSaved, setDraftSaved] = useState(false);
249
+
250
+ // Load draft from localStorage on mount
251
+ useEffect(function() {
252
+ try {
253
+ var draft = localStorage.getItem('em_agent_draft');
254
+ if (draft) {
255
+ var parsed = JSON.parse(draft);
256
+ setForm(function(f) { return Object.assign({}, f, parsed); });
257
+ if (parsed._step) setStep(parsed._step);
258
+ }
259
+ } catch {}
260
+ }, []);
261
+
262
+ // Save draft function
263
+ var saveDraft = useCallback(function() {
264
+ try {
265
+ localStorage.setItem('em_agent_draft', JSON.stringify(Object.assign({}, form, { _step: step })));
266
+ setDraftSaved(true);
267
+ setTimeout(function() { setDraftSaved(false); }, 2000);
268
+ } catch {}
269
+ }, [form, step]);
270
+
271
+ // Auto-save draft on step change
272
+ useEffect(function() {
273
+ if (form.name) {
274
+ try { localStorage.setItem('em_agent_draft', JSON.stringify(Object.assign({}, form, { _step: step }))); } catch {}
275
+ }
276
+ }, [step]);
277
+
278
+ // Clear draft after successful creation
279
+ var clearDraft = function() { try { localStorage.removeItem('em_agent_draft'); } catch {} };
248
280
  const [setupChecked, setSetupChecked] = useState(false);
249
281
 
250
282
  useEffect(() => {
@@ -455,6 +487,7 @@ export function CreateAgentWizard({ onClose, onCreated, toast }) {
455
487
  toast('Agent "' + form.name + '" created successfully', 'success');
456
488
  }
457
489
 
490
+ clearDraft();
458
491
  onCreated();
459
492
  onClose();
460
493
  } catch (err) { toast(err.message, 'error'); }
@@ -982,10 +1015,10 @@ export function CreateAgentWizard({ onClose, onCreated, toast }) {
982
1015
  h('h3', { style: { fontSize: 15, fontWeight: 700, marginBottom: 4 } }, 'Deployment Target'),
983
1016
  h('p', { style: { color: 'var(--text-secondary)', marginBottom: 16, fontSize: 13 } }, 'Choose where and how this agent will run.'),
984
1017
  h('div', { style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 } },
985
- ['docker', 'vps', 'local'].map(t =>
1018
+ ['fly', 'docker', 'railway', 'vps', 'local'].map(t =>
986
1019
  h('div', { key: t, className: 'preset-card' + (form.deployTarget === t ? ' selected' : ''), onClick: () => set('deployTarget', t), style: { padding: '16px 18px' } },
987
- h('h4', { style: { marginBottom: 6 } }, { docker: 'Docker Container', vps: 'VPS / Dedicated Server', local: 'Local Machine' }[t]),
988
- h('p', null, { docker: 'Run in an isolated Docker container with resource limits. Recommended for production.', vps: 'Deploy to a VPS or dedicated server via SSH. Full control over the environment.', local: 'Run on the current machine. Best for development and testing.' }[t])
1020
+ h('h4', { style: { marginBottom: 6 } }, { fly: 'Fly.io', docker: 'Docker Container', railway: 'Railway', vps: 'VPS / Dedicated Server', local: 'Local Machine' }[t]),
1021
+ h('p', null, { fly: 'Deploy to Fly.io with auto-scaling, TLS, and health checks. Recommended for production.', docker: 'Run in an isolated Docker container with resource limits.', railway: 'Deploy to Railway with zero-config builds and auto-deploys.', vps: 'Deploy to a VPS or dedicated server via SSH. Full control.', local: 'Run on the current machine. Best for development and testing.' }[t])
989
1022
  )
990
1023
  )
991
1024
  )
@@ -1042,7 +1075,7 @@ export function CreateAgentWizard({ onClose, onCreated, toast }) {
1042
1075
  h('span', { style: { color: 'var(--text-muted)' } }, 'Approvals'), h('span', null, form.approvalRequired ? 'Required (risk: ' + form.approvalForRiskLevels.join(', ') + ')' : 'Not required'),
1043
1076
  h('span', { style: { color: 'var(--text-muted)' } }, 'Rate Limits'), h('span', null, form.rateLimits.toolCallsPerMinute + '/min, ' + form.rateLimits.toolCallsPerHour + '/hr, ' + form.rateLimits.toolCallsPerDay + '/day'),
1044
1077
  h('span', { style: { color: 'var(--text-muted)' } }, 'Constraints'), h('span', null, form.constraints.maxConcurrentTasks + ' tasks, ' + form.constraints.maxSessionDurationMinutes + 'min max' + (form.constraints.sandboxMode ? ', sandbox' : '')),
1045
- h('span', { style: { color: 'var(--text-muted)' } }, 'Deployment'), h('span', null, { docker: 'Docker Container', vps: 'VPS / Dedicated Server', local: 'Local Machine' }[form.deployTarget]),
1078
+ h('span', { style: { color: 'var(--text-muted)' } }, 'Deployment'), h('span', null, { fly: 'Fly.io', docker: 'Docker Container', railway: 'Railway', vps: 'VPS / Dedicated Server', local: 'Local Machine' }[form.deployTarget] || form.deployTarget),
1046
1079
  h('span', { style: { color: 'var(--text-muted)' } }, 'Onboarding'), h('span', null, form.autoOnboard ? h('span', { className: 'badge badge-success' }, 'Auto-start') : h('span', { className: 'badge badge-neutral' }, 'Manual'))
1047
1080
  )
1048
1081
  )
@@ -1069,6 +1102,7 @@ export function CreateAgentWizard({ onClose, onCreated, toast }) {
1069
1102
  h('div', { className: 'modal-footer' },
1070
1103
  step > 0 && h('button', { className: 'btn btn-secondary', onClick: () => setStep(step - 1) }, 'Back'),
1071
1104
  step === 0 && !form.soulId && h('button', { className: 'btn btn-ghost', onClick: () => setStep(1) }, 'Skip — Configure Manually'),
1105
+ h('button', { className: 'btn btn-ghost', onClick: saveDraft, style: { fontSize: 12 } }, draftSaved ? '\u2713 Draft Saved' : 'Save Draft'),
1072
1106
  h('div', { style: { flex: 1 } }),
1073
1107
  step < lastStep && h('button', { className: 'btn btn-primary', disabled: !canNext(), onClick: () => setStep(step + 1) }, 'Next'),
1074
1108
  step === lastStep && h('button', { className: 'btn btn-primary', disabled: loading, onClick: doCreate }, loading ? 'Creating...' : 'Create Agent')
package/dist/index.js CHANGED
@@ -50,11 +50,11 @@ import {
50
50
  requireRole,
51
51
  securityHeaders,
52
52
  validate
53
- } from "./chunk-JIZXHTCS.js";
53
+ } from "./chunk-P3TYMEZP.js";
54
54
  import {
55
55
  provision,
56
56
  runSetupWizard
57
- } from "./chunk-ZI6WADMQ.js";
57
+ } from "./chunk-UT7KPNBZ.js";
58
58
  import {
59
59
  ENGINE_TABLES,
60
60
  ENGINE_TABLES_POSTGRES,
@@ -70,7 +70,8 @@ import {
70
70
  generateDockerCompose,
71
71
  generateEnvFile,
72
72
  generateFlyToml
73
- } from "./chunk-7FVRYOP4.js";
73
+ } from "./chunk-7XMV4YKZ.js";
74
+ import "./chunk-3SMTCIR4.js";
74
75
  import {
75
76
  CircuitBreaker,
76
77
  CircuitOpenError,
@@ -0,0 +1,17 @@
1
+ import {
2
+ deployToCloud,
3
+ generateDockerCompose,
4
+ generateEnvFile,
5
+ generateFlyToml,
6
+ generateRailwayConfig
7
+ } from "./chunk-7XMV4YKZ.js";
8
+ import "./chunk-3SMTCIR4.js";
9
+ import "./chunk-JLSQOQ5L.js";
10
+ import "./chunk-KFQGP6VL.js";
11
+ export {
12
+ deployToCloud,
13
+ generateDockerCompose,
14
+ generateEnvFile,
15
+ generateFlyToml,
16
+ generateRailwayConfig
17
+ };
@@ -0,0 +1,12 @@
1
+ import {
2
+ createServer
3
+ } from "./chunk-P3TYMEZP.js";
4
+ import "./chunk-3SMTCIR4.js";
5
+ import "./chunk-JLSQOQ5L.js";
6
+ import "./chunk-RO537U6H.js";
7
+ import "./chunk-DRXMYYKN.js";
8
+ import "./chunk-67KZYSLU.js";
9
+ import "./chunk-KFQGP6VL.js";
10
+ export {
11
+ createServer
12
+ };
@@ -0,0 +1,20 @@
1
+ import {
2
+ promptCompanyInfo,
3
+ promptDatabase,
4
+ promptDeployment,
5
+ promptDomain,
6
+ promptRegistration,
7
+ provision,
8
+ runSetupWizard
9
+ } from "./chunk-UT7KPNBZ.js";
10
+ import "./chunk-HEK7L3DT.js";
11
+ import "./chunk-KFQGP6VL.js";
12
+ export {
13
+ promptCompanyInfo,
14
+ promptDatabase,
15
+ promptDeployment,
16
+ promptDomain,
17
+ promptRegistration,
18
+ provision,
19
+ runSetupWizard
20
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agenticmail/enterprise",
3
- "version": "0.5.25",
3
+ "version": "0.5.27",
4
4
  "description": "AgenticMail Enterprise — cloud-hosted AI agent identity, email, auth & compliance for organizations",
5
5
  "type": "module",
6
6
  "bin": {
@@ -11,6 +11,7 @@ import type { AppEnv } from '../types/hono-env.js';
11
11
  import type { DatabaseAdapter } from '../db/adapter.js';
12
12
  import { validate, requireRole, ValidationError } from '../middleware/index.js';
13
13
  import { PROVIDER_REGISTRY, type ProviderDef, type CustomProviderDef } from '../runtime/providers.js';
14
+ import { deployToFly, getAppStatus, destroyApp, type FlyConfig, type AppConfig } from '../deploy/fly.js';
14
15
 
15
16
  export function createAdminRoutes(db: DatabaseAdapter) {
16
17
  const api = new Hono<AppEnv>();
@@ -108,6 +109,163 @@ export function createAdminRoutes(db: DatabaseAdapter) {
108
109
  return c.json({ ok: true });
109
110
  });
110
111
 
112
+ // ─── Agent Deployment ─────────────────────────────────
113
+
114
+ api.post('/agents/:id/deploy', requireRole('admin'), async (c) => {
115
+ const agentId = c.req.param('id');
116
+ const agent = await db.getAgent(agentId);
117
+ if (!agent) return c.json({ error: 'Agent not found' }, 404);
118
+
119
+ const body = await c.req.json();
120
+ const targetType = body.targetType || 'fly';
121
+ const config = body.config || {};
122
+
123
+ // Get deployment credentials
124
+ const settings = await db.getSettings();
125
+ const pricingConfig = (settings as any)?.modelPricingConfig || {};
126
+ const providerApiKeys = pricingConfig.providerApiKeys || {};
127
+
128
+ if (targetType === 'fly') {
129
+ // Get Fly.io API token from deploy credentials or config
130
+ let flyToken = config.flyApiToken || process.env.FLY_API_TOKEN;
131
+ if (!flyToken && body.credentialId) {
132
+ // Look up stored credential
133
+ try {
134
+ const creds = await (db as any).query?.('SELECT config FROM deploy_credentials WHERE id = $1', [body.credentialId]);
135
+ if (creds?.rows?.[0]?.config) {
136
+ const credConfig = typeof creds.rows[0].config === 'string' ? JSON.parse(creds.rows[0].config) : creds.rows[0].config;
137
+ flyToken = credConfig.apiToken;
138
+ }
139
+ } catch { /* ignore */ }
140
+ }
141
+
142
+ if (!flyToken) {
143
+ return c.json({ error: 'Fly.io API token required. Add it in Settings → Deployments or pass flyApiToken in config.' }, 400);
144
+ }
145
+
146
+ const flyConfig: FlyConfig = {
147
+ apiToken: flyToken,
148
+ org: config.flyOrg || 'personal',
149
+ image: config.image || 'node:22-slim',
150
+ regions: config.regions || ['iad'],
151
+ };
152
+
153
+ const agentName = (agent as any).name || agentId;
154
+ const appConfig: AppConfig = {
155
+ subdomain: agentName.toLowerCase().replace(/[^a-z0-9-]/g, '-'),
156
+ dbType: 'postgres',
157
+ dbConnectionString: process.env.DATABASE_URL || '',
158
+ jwtSecret: process.env.JWT_SECRET || 'agent-' + agentId,
159
+ smtpHost: (settings as any)?.smtpHost,
160
+ smtpPort: (settings as any)?.smtpPort,
161
+ smtpUser: (settings as any)?.smtpUser,
162
+ smtpPass: (settings as any)?.smtpPass,
163
+ memoryMb: config.memoryMb || 256,
164
+ cpuKind: config.cpuKind || 'shared',
165
+ cpus: config.cpus || 1,
166
+ };
167
+
168
+ try {
169
+ const result = await deployToFly(appConfig, flyConfig);
170
+
171
+ // Update agent record with deployment info (stored in metadata)
172
+ const existingAgent = await db.getAgent(agentId);
173
+ const existingMeta = (existingAgent as any)?.metadata || {};
174
+ await db.updateAgent(agentId, {
175
+ status: result.status === 'started' ? 'active' : 'error',
176
+ metadata: {
177
+ ...existingMeta,
178
+ deployment: {
179
+ target: 'fly',
180
+ appName: result.appName,
181
+ url: result.url,
182
+ region: result.region,
183
+ machineId: result.machineId,
184
+ deployedAt: new Date().toISOString(),
185
+ deployedBy: body.deployedBy || 'dashboard',
186
+ status: result.status,
187
+ },
188
+ },
189
+ } as any);
190
+
191
+ return c.json({
192
+ success: result.status === 'started',
193
+ deployment: result,
194
+ });
195
+ } catch (err: any) {
196
+ return c.json({ error: 'Deployment failed: ' + err.message }, 500);
197
+ }
198
+ }
199
+
200
+ if (targetType === 'local') {
201
+ const existingAgent = await db.getAgent(agentId);
202
+ const existingMeta = (existingAgent as any)?.metadata || {};
203
+ await db.updateAgent(agentId, {
204
+ status: 'active',
205
+ metadata: {
206
+ ...existingMeta,
207
+ deployment: {
208
+ target: 'local',
209
+ url: `http://localhost:${3000 + Math.floor(Math.random() * 1000)}`,
210
+ deployedAt: new Date().toISOString(),
211
+ deployedBy: body.deployedBy || 'dashboard',
212
+ status: 'started',
213
+ },
214
+ },
215
+ } as any);
216
+ return c.json({ success: true, deployment: { status: 'started', target: 'local' } });
217
+ }
218
+
219
+ return c.json({ error: 'Unsupported deploy target: ' + targetType + '. Supported: fly, docker, vps, local' }, 400);
220
+ });
221
+
222
+ // Get deployment status
223
+ api.get('/agents/:id/deploy', requireRole('admin'), async (c) => {
224
+ const agent = await db.getAgent(c.req.param('id'));
225
+ if (!agent) return c.json({ error: 'Agent not found' }, 404);
226
+
227
+ const meta = (agent as any).metadata || {};
228
+ const info = meta.deployment;
229
+ if (!info) return c.json({ deployed: false });
230
+
231
+ if (info.target === 'fly' && info.appName) {
232
+ const flyToken = process.env.FLY_API_TOKEN;
233
+ if (flyToken) {
234
+ try {
235
+ const status = await getAppStatus(info.appName, { apiToken: flyToken });
236
+ return c.json({ deployed: true, ...info, live: status });
237
+ } catch { /* fall through */ }
238
+ }
239
+ }
240
+
241
+ return c.json({ deployed: true, ...info });
242
+ });
243
+
244
+ // Destroy deployment
245
+ api.delete('/agents/:id/deploy', requireRole('admin'), async (c) => {
246
+ const agent = await db.getAgent(c.req.param('id'));
247
+ if (!agent) return c.json({ error: 'Agent not found' }, 404);
248
+
249
+ const meta = (agent as any).metadata || {};
250
+ const info = meta.deployment;
251
+ if (!info) return c.json({ error: 'Agent not deployed' }, 400);
252
+
253
+ if (info.target === 'fly' && info.appName) {
254
+ const flyToken = process.env.FLY_API_TOKEN;
255
+ if (flyToken) {
256
+ try {
257
+ await destroyApp(info.appName, { apiToken: flyToken });
258
+ } catch (err: any) {
259
+ return c.json({ error: 'Failed to destroy: ' + err.message }, 500);
260
+ }
261
+ }
262
+ }
263
+
264
+ delete meta.deployment;
265
+ await db.updateAgent(c.req.param('id'), { status: 'inactive', metadata: meta } as any);
266
+ return c.json({ ok: true, message: 'Deployment destroyed' });
267
+ });
268
+
111
269
  // ─── Users ──────────────────────────────────────────
112
270
 
113
271
  api.get('/users', requireRole('admin'), async (c) => {
@@ -31,7 +31,7 @@ export function DeployModal({ agentId, agentConfig, onClose, onDeployed, toast }
31
31
  const doDeploy = async () => {
32
32
  setError(''); setLoading(true);
33
33
  try {
34
- await engineCall('/agents/' + agentId + '/deploy', { method: 'POST', body: JSON.stringify({ targetType: targetType, credentialId: selectedCred || undefined, config: config, deployedBy: 'dashboard' }) });
34
+ await apiCall('/agents/' + agentId + '/deploy', { method: 'POST', body: JSON.stringify({ targetType: targetType, credentialId: selectedCred || undefined, config: config, deployedBy: 'dashboard' }) });
35
35
  if (toast) toast('Deployment started', 'success');
36
36
  if (onDeployed) onDeployed();
37
37
  onClose();
@@ -233,7 +233,7 @@ export function DeploymentProgress({ agentId, onComplete }) {
233
233
  export function CreateAgentWizard({ onClose, onCreated, toast }) {
234
234
  const [step, setStep] = useState(0);
235
235
  const steps = ['Role', 'Basics', 'Persona', 'Skills', 'Permissions', 'Deployment', 'Review'];
236
- const [form, setForm] = useState({ name: '', email: '', role: 'assistant', description: '', personality: '', skills: [], preset: null, customTools: { allowed: [], blocked: [] }, deployTarget: 'docker', knowledgeBases: [], provider: '', model: '', approvalRequired: true, soulId: null, avatar: null, gender: '', dateOfBirth: '', maritalStatus: '', culturalBackground: '', language: 'en-us', autoOnboard: true, maxRiskLevel: 'medium', blockedSideEffects: ['runs-code', 'deletes-data', 'financial', 'controls-device'], approvalForRiskLevels: ['high', 'critical'], approvalForSideEffects: ['sends-email', 'sends-message'], rateLimits: { toolCallsPerMinute: 30, toolCallsPerHour: 500, toolCallsPerDay: 5000, externalActionsPerHour: 50 }, constraints: { maxConcurrentTasks: 5, maxSessionDurationMinutes: 480, sandboxMode: false }, traits: { communication: 'direct', detail: 'detail-oriented', energy: 'calm', humor: 'warm', formality: 'adaptive', empathy: 'moderate', patience: 'patient', creativity: 'creative' } });
236
+ const [form, setForm] = useState({ name: '', email: '', role: 'assistant', description: '', personality: '', skills: [], preset: null, customTools: { allowed: [], blocked: [] }, deployTarget: 'fly', knowledgeBases: [], provider: '', model: '', approvalRequired: true, soulId: null, avatar: null, gender: '', dateOfBirth: '', maritalStatus: '', culturalBackground: '', language: 'en-us', autoOnboard: true, maxRiskLevel: 'medium', blockedSideEffects: ['runs-code', 'deletes-data', 'financial', 'controls-device'], approvalForRiskLevels: ['high', 'critical'], approvalForSideEffects: ['sends-email', 'sends-message'], rateLimits: { toolCallsPerMinute: 30, toolCallsPerHour: 500, toolCallsPerDay: 5000, externalActionsPerHour: 50 }, constraints: { maxConcurrentTasks: 5, maxSessionDurationMinutes: 480, sandboxMode: false }, traits: { communication: 'direct', detail: 'detail-oriented', energy: 'calm', humor: 'warm', formality: 'adaptive', empathy: 'moderate', patience: 'patient', creativity: 'creative' } });
237
237
  const [allSkills, setAllSkills] = useState({});
238
238
  const [providers, setProviders] = useState([]);
239
239
  const [providerModels, setProviderModels] = useState([]);
@@ -245,6 +245,38 @@ export function CreateAgentWizard({ onClose, onCreated, toast }) {
245
245
  const [previewOpen, setPreviewOpen] = useState(false);
246
246
  const [loading, setLoading] = useState(false);
247
247
  const [showSetupGuide, setShowSetupGuide] = useState(false);
248
+ const [draftSaved, setDraftSaved] = useState(false);
249
+
250
+ // Load draft from localStorage on mount
251
+ useEffect(function() {
252
+ try {
253
+ var draft = localStorage.getItem('em_agent_draft');
254
+ if (draft) {
255
+ var parsed = JSON.parse(draft);
256
+ setForm(function(f) { return Object.assign({}, f, parsed); });
257
+ if (parsed._step) setStep(parsed._step);
258
+ }
259
+ } catch {}
260
+ }, []);
261
+
262
+ // Save draft function
263
+ var saveDraft = useCallback(function() {
264
+ try {
265
+ localStorage.setItem('em_agent_draft', JSON.stringify(Object.assign({}, form, { _step: step })));
266
+ setDraftSaved(true);
267
+ setTimeout(function() { setDraftSaved(false); }, 2000);
268
+ } catch {}
269
+ }, [form, step]);
270
+
271
+ // Auto-save draft on step change
272
+ useEffect(function() {
273
+ if (form.name) {
274
+ try { localStorage.setItem('em_agent_draft', JSON.stringify(Object.assign({}, form, { _step: step }))); } catch {}
275
+ }
276
+ }, [step]);
277
+
278
+ // Clear draft after successful creation
279
+ var clearDraft = function() { try { localStorage.removeItem('em_agent_draft'); } catch {} };
248
280
  const [setupChecked, setSetupChecked] = useState(false);
249
281
 
250
282
  useEffect(() => {
@@ -455,6 +487,7 @@ export function CreateAgentWizard({ onClose, onCreated, toast }) {
455
487
  toast('Agent "' + form.name + '" created successfully', 'success');
456
488
  }
457
489
 
490
+ clearDraft();
458
491
  onCreated();
459
492
  onClose();
460
493
  } catch (err) { toast(err.message, 'error'); }
@@ -982,10 +1015,10 @@ export function CreateAgentWizard({ onClose, onCreated, toast }) {
982
1015
  h('h3', { style: { fontSize: 15, fontWeight: 700, marginBottom: 4 } }, 'Deployment Target'),
983
1016
  h('p', { style: { color: 'var(--text-secondary)', marginBottom: 16, fontSize: 13 } }, 'Choose where and how this agent will run.'),
984
1017
  h('div', { style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 } },
985
- ['docker', 'vps', 'local'].map(t =>
1018
+ ['fly', 'docker', 'railway', 'vps', 'local'].map(t =>
986
1019
  h('div', { key: t, className: 'preset-card' + (form.deployTarget === t ? ' selected' : ''), onClick: () => set('deployTarget', t), style: { padding: '16px 18px' } },
987
- h('h4', { style: { marginBottom: 6 } }, { docker: 'Docker Container', vps: 'VPS / Dedicated Server', local: 'Local Machine' }[t]),
988
- h('p', null, { docker: 'Run in an isolated Docker container with resource limits. Recommended for production.', vps: 'Deploy to a VPS or dedicated server via SSH. Full control over the environment.', local: 'Run on the current machine. Best for development and testing.' }[t])
1020
+ h('h4', { style: { marginBottom: 6 } }, { fly: 'Fly.io', docker: 'Docker Container', railway: 'Railway', vps: 'VPS / Dedicated Server', local: 'Local Machine' }[t]),
1021
+ h('p', null, { fly: 'Deploy to Fly.io with auto-scaling, TLS, and health checks. Recommended for production.', docker: 'Run in an isolated Docker container with resource limits.', railway: 'Deploy to Railway with zero-config builds and auto-deploys.', vps: 'Deploy to a VPS or dedicated server via SSH. Full control.', local: 'Run on the current machine. Best for development and testing.' }[t])
989
1022
  )
990
1023
  )
991
1024
  )
@@ -1042,7 +1075,7 @@ export function CreateAgentWizard({ onClose, onCreated, toast }) {
1042
1075
  h('span', { style: { color: 'var(--text-muted)' } }, 'Approvals'), h('span', null, form.approvalRequired ? 'Required (risk: ' + form.approvalForRiskLevels.join(', ') + ')' : 'Not required'),
1043
1076
  h('span', { style: { color: 'var(--text-muted)' } }, 'Rate Limits'), h('span', null, form.rateLimits.toolCallsPerMinute + '/min, ' + form.rateLimits.toolCallsPerHour + '/hr, ' + form.rateLimits.toolCallsPerDay + '/day'),
1044
1077
  h('span', { style: { color: 'var(--text-muted)' } }, 'Constraints'), h('span', null, form.constraints.maxConcurrentTasks + ' tasks, ' + form.constraints.maxSessionDurationMinutes + 'min max' + (form.constraints.sandboxMode ? ', sandbox' : '')),
1045
- h('span', { style: { color: 'var(--text-muted)' } }, 'Deployment'), h('span', null, { docker: 'Docker Container', vps: 'VPS / Dedicated Server', local: 'Local Machine' }[form.deployTarget]),
1078
+ h('span', { style: { color: 'var(--text-muted)' } }, 'Deployment'), h('span', null, { fly: 'Fly.io', docker: 'Docker Container', railway: 'Railway', vps: 'VPS / Dedicated Server', local: 'Local Machine' }[form.deployTarget] || form.deployTarget),
1046
1079
  h('span', { style: { color: 'var(--text-muted)' } }, 'Onboarding'), h('span', null, form.autoOnboard ? h('span', { className: 'badge badge-success' }, 'Auto-start') : h('span', { className: 'badge badge-neutral' }, 'Manual'))
1047
1080
  )
1048
1081
  )
@@ -1069,6 +1102,7 @@ export function CreateAgentWizard({ onClose, onCreated, toast }) {
1069
1102
  h('div', { className: 'modal-footer' },
1070
1103
  step > 0 && h('button', { className: 'btn btn-secondary', onClick: () => setStep(step - 1) }, 'Back'),
1071
1104
  step === 0 && !form.soulId && h('button', { className: 'btn btn-ghost', onClick: () => setStep(1) }, 'Skip — Configure Manually'),
1105
+ h('button', { className: 'btn btn-ghost', onClick: saveDraft, style: { fontSize: 12 } }, draftSaved ? '\u2713 Draft Saved' : 'Save Draft'),
1072
1106
  h('div', { style: { flex: 1 } }),
1073
1107
  step < lastStep && h('button', { className: 'btn btn-primary', disabled: !canNext(), onClick: () => setStep(step + 1) }, 'Next'),
1074
1108
  step === lastStep && h('button', { className: 'btn btn-primary', disabled: loading, onClick: doCreate }, loading ? 'Creating...' : 'Create Agent')