neoagent 2.3.1-beta.66 → 2.3.1-beta.68

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/.env.example CHANGED
@@ -4,7 +4,8 @@
4
4
 
5
5
  PORT=3333
6
6
  NODE_ENV=production
7
- SESSION_SECRET=change-this-to-a-random-secret-in-production
7
+ # Auto-generated on first start or setup if left blank.
8
+ SESSION_SECRET=
8
9
 
9
10
  # Optional: dedicated key for encrypting local at-rest secrets (for example API_KEYS.json).
10
11
  # If unset, encryption falls back to SESSION_SECRET.
@@ -23,7 +24,7 @@ NEOAGENT_DEPLOYMENT_MODE=self_hosted
23
24
  # Deployment profile:
24
25
  # private -> single-user / trusted host runtime
25
26
  # prod -> multi-user / isolated per-user VM runtime
26
- NEOAGENT_PROFILE=private
27
+ NEOAGENT_PROFILE=prod
27
28
 
28
29
  # VM runtime settings used by `prod`.
29
30
  # Set these before switching NEOAGENT_PROFILE=prod.
@@ -38,9 +39,8 @@ NEOAGENT_VM_BASE_IMAGE=
38
39
 
39
40
  # The guest image must boot a Linux guest that runs `npm run start:guest-agent`
40
41
  # and exposes the guest agent on port 8421.
41
- # Replace this placeholder before using NEOAGENT_PROFILE=prod.
42
- # Example: openssl rand -hex 32
43
- NEOAGENT_VM_GUEST_TOKEN=change-this-guest-token-before-prod
42
+ # Auto-generated on first start or setup if left blank.
43
+ NEOAGENT_VM_GUEST_TOKEN=
44
44
  NEOAGENT_VM_MEMORY_MB=4096
45
45
  NEOAGENT_VM_CPUS=2
46
46
 
package/lib/manager.js CHANGED
@@ -18,6 +18,7 @@ const {
18
18
  LOG_DIR,
19
19
  ENV_FILE,
20
20
  PID_FILE,
21
+ getDefaultVmBaseImageUrl,
21
22
  ensureRuntimeDirs,
22
23
  migrateLegacyRuntime
23
24
  } = require('../runtime/paths');
@@ -425,6 +426,7 @@ async function cmdSetup() {
425
426
  ensureRuntimeDirs();
426
427
 
427
428
  const current = Object.fromEntries(parseEnv(readEnvFileRaw()).entries());
429
+ const defaultVmBaseImageUrl = getDefaultVmBaseImageUrl();
428
430
 
429
431
  logInfo('Press Enter to keep the current value shown in brackets.');
430
432
 
@@ -436,7 +438,7 @@ async function cmdSetup() {
436
438
  const secureCookies = await ask('Secure cookies (true/false)', secureCookiesDefault);
437
439
  const trustProxyDefault = current.TRUST_PROXY || secureCookiesDefault;
438
440
  const trustProxy = await ask('Trust reverse proxy headers (true/false)', trustProxyDefault);
439
- const sessionSecret = await askSecret('Session secret', current.SESSION_SECRET || randomSecret());
441
+ const sessionSecret = current.SESSION_SECRET || randomSecret();
440
442
  const deploymentMode = await ask(
441
443
  'Deployment mode (self_hosted/managed)',
442
444
  current.NEOAGENT_DEPLOYMENT_MODE || 'self_hosted'
@@ -446,6 +448,11 @@ async function cmdSetup() {
446
448
  current.NEOAGENT_RELEASE_CHANNEL || 'stable'
447
449
  );
448
450
  const origins = await ask('Allowed CORS origins', current.ALLOWED_ORIGINS || '');
451
+ const deploymentProfile = current.NEOAGENT_PROFILE || 'prod';
452
+ const vmBaseImageUrl = current.NEOAGENT_VM_BASE_IMAGE_URL || defaultVmBaseImageUrl;
453
+ const vmMemoryMb = current.NEOAGENT_VM_MEMORY_MB || '4096';
454
+ const vmCpus = current.NEOAGENT_VM_CPUS || '2';
455
+ const vmGuestToken = current.NEOAGENT_VM_GUEST_TOKEN || randomSecret();
449
456
 
450
457
  heading('AI Providers');
451
458
  const anthropic = await askSecret('Anthropic API key', current.ANTHROPIC_API_KEY || '');
@@ -562,8 +569,13 @@ async function cmdSetup() {
562
569
  `SECURE_COOKIES=${normalizedSecureCookies}`,
563
570
  `TRUST_PROXY=${normalizedTrustProxy}`,
564
571
  `SESSION_SECRET=${sessionSecret}`,
572
+ `NEOAGENT_PROFILE=${deploymentProfile}`,
565
573
  `NEOAGENT_DEPLOYMENT_MODE=${normalizedDeploymentMode}`,
566
574
  `NEOAGENT_RELEASE_CHANNEL=${normalizedReleaseChannel}`,
575
+ `NEOAGENT_VM_BASE_IMAGE_URL=${vmBaseImageUrl}`,
576
+ `NEOAGENT_VM_MEMORY_MB=${vmMemoryMb}`,
577
+ `NEOAGENT_VM_CPUS=${vmCpus}`,
578
+ `NEOAGENT_VM_GUEST_TOKEN=${vmGuestToken}`,
567
579
  anthropic ? `ANTHROPIC_API_KEY=${anthropic}` : '',
568
580
  anthropicBaseUrl ? `ANTHROPIC_BASE_URL=${anthropicBaseUrl}` : '',
569
581
  openai ? `OPENAI_API_KEY=${openai}` : '',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neoagent",
3
- "version": "2.3.1-beta.66",
3
+ "version": "2.3.1-beta.68",
4
4
  "description": "Proactive personal AI agent with no limits",
5
5
  "license": "MIT",
6
6
  "main": "server/index.js",
@@ -54,6 +54,9 @@
54
54
  "@anthropic-ai/sdk": "^0.39.0",
55
55
  "@google/generative-ai": "^0.24.0",
56
56
  "@modelcontextprotocol/sdk": "^1.12.1",
57
+ "@remotion/cli": "^4.0.459",
58
+ "@slidev/cli": "^52.15.2",
59
+ "@slidev/theme-default": "^0.25.0",
57
60
  "baileys": "^6.7.21",
58
61
  "bcrypt": "^6.0.0",
59
62
  "better-sqlite3": "^11.8.1",
@@ -73,11 +76,13 @@
73
76
  "nodemailer": "^8.0.5",
74
77
  "openai": "^4.85.4",
75
78
  "otplib": "^13.4.0",
79
+ "playwright-chromium": "^1.59.1",
76
80
  "proper-lockfile": "^4.1.2",
77
81
  "puppeteer-core": "^24.40.0",
78
82
  "puppeteer-extra": "^3.3.6",
79
83
  "puppeteer-extra-plugin-stealth": "^2.11.2",
80
84
  "qrcode": "^1.5.4",
85
+ "remotion": "^4.0.459",
81
86
  "sharp": "^0.34.5",
82
87
  "socket.io": "^4.8.1",
83
88
  "telegraf": "^4.16.3",
package/runtime/paths.js CHANGED
@@ -1,6 +1,8 @@
1
1
  const fs = require('fs');
2
+ const crypto = require('crypto');
2
3
  const os = require('os');
3
4
  const path = require('path');
5
+ const { parseEnv } = require('./env');
4
6
 
5
7
  const APP_DIR = path.resolve(__dirname, '..');
6
8
  const HOME_DIR = os.homedir();
@@ -15,6 +17,10 @@ const PID_FILE = path.join(DATA_DIR, 'neoagent.pid');
15
17
  const LEGACY_ENV_FILE = path.join(APP_DIR, '.env');
16
18
  const LEGACY_DATA_DIR = path.join(APP_DIR, 'data');
17
19
  const LEGACY_AGENT_DATA_DIR = path.join(APP_DIR, 'agent-data');
20
+ const DEFAULT_VM_BASE_IMAGE_URLS = Object.freeze({
21
+ arm64: 'https://cloud-images.ubuntu.com/releases/24.04/release/ubuntu-24.04-server-cloudimg-arm64.img',
22
+ x64: 'https://cloud-images.ubuntu.com/releases/24.04/release/ubuntu-24.04-server-cloudimg-amd64.img',
23
+ });
18
24
 
19
25
  function ensureRuntimeDirs() {
20
26
  for (const dir of [RUNTIME_HOME, DATA_DIR, LOG_DIR, AGENT_DATA_DIR]) {
@@ -95,6 +101,133 @@ function migrateLegacyRuntime(logger = () => {}) {
95
101
  return changed;
96
102
  }
97
103
 
104
+ function readEnvFileRaw(envFile = ENV_FILE) {
105
+ try {
106
+ return fs.readFileSync(envFile, 'utf8');
107
+ } catch {
108
+ return '';
109
+ }
110
+ }
111
+
112
+ function upsertEnvValue(envFile, key, value) {
113
+ const raw = readEnvFileRaw(envFile);
114
+ const lines = raw ? raw.split('\n') : [];
115
+ let replaced = false;
116
+
117
+ for (let i = 0; i < lines.length; i++) {
118
+ if (lines[i].startsWith(`${key}=`)) {
119
+ lines[i] = `${key}=${value}`;
120
+ replaced = true;
121
+ break;
122
+ }
123
+ }
124
+
125
+ if (!replaced) {
126
+ lines.push(`${key}=${value}`);
127
+ }
128
+
129
+ const output = lines.filter((line, idx, arr) => idx !== arr.length - 1 || line !== '').join('\n') + '\n';
130
+ fs.mkdirSync(path.dirname(envFile), { recursive: true });
131
+ fs.writeFileSync(envFile, output, { mode: 0o600 });
132
+ }
133
+
134
+ function generateSecret(bytes = 32) {
135
+ return crypto.randomBytes(bytes).toString('hex');
136
+ }
137
+
138
+ function getDefaultVmBaseImageUrl(arch = process.arch) {
139
+ return arch === 'arm64' ? DEFAULT_VM_BASE_IMAGE_URLS.arm64 : DEFAULT_VM_BASE_IMAGE_URLS.x64;
140
+ }
141
+
142
+ function isPlaceholderValue(value, placeholders) {
143
+ const secret = String(value || '').trim();
144
+ return !secret || placeholders.has(secret);
145
+ }
146
+
147
+ function isValidVmGuestToken(value) {
148
+ const secret = String(value || '').trim();
149
+ if (!secret || secret.length < 32) return false;
150
+ if (/^(change|replace|set|your|example|sample|placeholder|token|secret)[-_a-z0-9]*$/i.test(secret)) return false;
151
+ if (/change-this-guest-token-before-prod/i.test(secret)) return false;
152
+ if (/^(.)\1+$/.test(secret)) return false;
153
+ return true;
154
+ }
155
+
156
+ function ensureSecureRuntimeEnv({ envFile = ENV_FILE, env = process.env, logger = console } = {}) {
157
+ const raw = readEnvFileRaw(envFile);
158
+ const parsed = parseEnv(raw);
159
+ const changes = [];
160
+ const defaultProfile = 'prod';
161
+ const sessionPlaceholders = new Set([
162
+ 'neoagent-dev-secret-change-me',
163
+ 'change-this-to-a-random-secret-in-production',
164
+ 'change-me-to-something-random',
165
+ ]);
166
+
167
+ let deploymentProfile = String(env.NEOAGENT_PROFILE || parsed.get('NEOAGENT_PROFILE') || '').trim();
168
+ if (!deploymentProfile) {
169
+ deploymentProfile = defaultProfile;
170
+ env.NEOAGENT_PROFILE = deploymentProfile;
171
+ upsertEnvValue(envFile, 'NEOAGENT_PROFILE', deploymentProfile);
172
+ changes.push('NEOAGENT_PROFILE');
173
+ }
174
+
175
+ let vmBaseImageUrl = String(env.NEOAGENT_VM_BASE_IMAGE_URL || parsed.get('NEOAGENT_VM_BASE_IMAGE_URL') || '').trim();
176
+ if (!vmBaseImageUrl) {
177
+ vmBaseImageUrl = getDefaultVmBaseImageUrl();
178
+ env.NEOAGENT_VM_BASE_IMAGE_URL = vmBaseImageUrl;
179
+ upsertEnvValue(envFile, 'NEOAGENT_VM_BASE_IMAGE_URL', vmBaseImageUrl);
180
+ changes.push('NEOAGENT_VM_BASE_IMAGE_URL');
181
+ }
182
+
183
+ let vmMemoryMb = String(env.NEOAGENT_VM_MEMORY_MB || parsed.get('NEOAGENT_VM_MEMORY_MB') || '').trim();
184
+ if (!vmMemoryMb) {
185
+ vmMemoryMb = '4096';
186
+ env.NEOAGENT_VM_MEMORY_MB = vmMemoryMb;
187
+ upsertEnvValue(envFile, 'NEOAGENT_VM_MEMORY_MB', vmMemoryMb);
188
+ changes.push('NEOAGENT_VM_MEMORY_MB');
189
+ }
190
+
191
+ let vmCpus = String(env.NEOAGENT_VM_CPUS || parsed.get('NEOAGENT_VM_CPUS') || '').trim();
192
+ if (!vmCpus) {
193
+ vmCpus = '2';
194
+ env.NEOAGENT_VM_CPUS = vmCpus;
195
+ upsertEnvValue(envFile, 'NEOAGENT_VM_CPUS', vmCpus);
196
+ changes.push('NEOAGENT_VM_CPUS');
197
+ }
198
+
199
+ let sessionSecret = String(env.SESSION_SECRET || parsed.get('SESSION_SECRET') || '').trim();
200
+ if (isPlaceholderValue(sessionSecret, sessionPlaceholders)) {
201
+ sessionSecret = generateSecret(32);
202
+ env.SESSION_SECRET = sessionSecret;
203
+ upsertEnvValue(envFile, 'SESSION_SECRET', sessionSecret);
204
+ changes.push('SESSION_SECRET');
205
+ }
206
+
207
+ let guestToken = String(env.NEOAGENT_VM_GUEST_TOKEN || parsed.get('NEOAGENT_VM_GUEST_TOKEN') || '').trim();
208
+ if (!isValidVmGuestToken(guestToken)) {
209
+ guestToken = generateSecret(32);
210
+ env.NEOAGENT_VM_GUEST_TOKEN = guestToken;
211
+ upsertEnvValue(envFile, 'NEOAGENT_VM_GUEST_TOKEN', guestToken);
212
+ changes.push('NEOAGENT_VM_GUEST_TOKEN');
213
+ }
214
+
215
+ if (changes.length > 0 && logger) {
216
+ const message = `Initialized runtime defaults: ${changes.join(', ')}`;
217
+ if (typeof logger.info === 'function') {
218
+ logger.info(message);
219
+ } else if (typeof logger.log === 'function') {
220
+ logger.log(message);
221
+ }
222
+ }
223
+
224
+ return {
225
+ changes,
226
+ sessionSecret: env.SESSION_SECRET || null,
227
+ guestToken: env.NEOAGENT_VM_GUEST_TOKEN || null,
228
+ };
229
+ }
230
+
98
231
  module.exports = {
99
232
  APP_DIR,
100
233
  HOME_DIR,
@@ -108,6 +241,9 @@ module.exports = {
108
241
  LEGACY_ENV_FILE,
109
242
  LEGACY_DATA_DIR,
110
243
  LEGACY_AGENT_DATA_DIR,
244
+ DEFAULT_VM_BASE_IMAGE_URLS,
111
245
  ensureRuntimeDirs,
246
+ ensureSecureRuntimeEnv,
247
+ getDefaultVmBaseImageUrl,
112
248
  migrateLegacyRuntime
113
249
  };
@@ -0,0 +1,29 @@
1
+ const test = require('node:test');
2
+ const assert = require('node:assert/strict');
3
+ const fs = require('fs');
4
+ const os = require('os');
5
+ const path = require('path');
6
+
7
+ const {
8
+ ensureSecureRuntimeEnv,
9
+ } = require('./paths');
10
+
11
+ test('ensureSecureRuntimeEnv generates runtime secrets when missing', () => {
12
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'neoagent-runtime-'));
13
+ const envFile = path.join(tempDir, '.env');
14
+ const env = {};
15
+
16
+ const result = ensureSecureRuntimeEnv({
17
+ envFile,
18
+ env,
19
+ logger: null,
20
+ });
21
+
22
+ const content = fs.readFileSync(envFile, 'utf8');
23
+ assert.match(content, /^SESSION_SECRET=/m);
24
+ assert.match(content, /^NEOAGENT_VM_GUEST_TOKEN=/m);
25
+ assert.equal(result.changes.includes('SESSION_SECRET'), true);
26
+ assert.equal(result.changes.includes('NEOAGENT_VM_GUEST_TOKEN'), true);
27
+ assert.ok(String(env.SESSION_SECRET || '').length >= 64);
28
+ assert.ok(String(env.NEOAGENT_VM_GUEST_TOKEN || '').length >= 64);
29
+ });
package/server/index.js CHANGED
@@ -3,6 +3,7 @@
3
3
  const {
4
4
  ENV_FILE,
5
5
  LEGACY_ENV_FILE,
6
+ ensureSecureRuntimeEnv,
6
7
  migrateLegacyRuntime,
7
8
  ensureRuntimeDirs
8
9
  } = require('../runtime/paths');
@@ -11,6 +12,7 @@ const dotenv = require('dotenv');
11
12
 
12
13
  migrateLegacyRuntime();
13
14
  ensureRuntimeDirs();
15
+ ensureSecureRuntimeEnv({ logger: console });
14
16
  dotenv.config({ path: LEGACY_ENV_FILE });
15
17
  dotenv.config({ path: ENV_FILE, override: true });
16
18
 
@@ -19,10 +21,6 @@ const { createServer } = require('http');
19
21
 
20
22
  const db = require('./db/database');
21
23
  const { setupConsoleInterceptor } = require('./utils/logger');
22
- const {
23
- configuredSessionSecret,
24
- isInsecureSessionSecret,
25
- } = require('./services/account/session_secret');
26
24
  const { validateOrigin } = require('./config/origins');
27
25
  const {
28
26
  applyHttpMiddleware,
@@ -87,16 +85,6 @@ function logStartupConfig() {
87
85
 
88
86
  logStartupConfig();
89
87
 
90
- if (!configuredSessionSecret()) {
91
- console.warn(
92
- 'WARNING: SESSION_SECRET not set — using a process-local random fallback. Set it in .env before exposing this server.'
93
- );
94
- } else if (isInsecureSessionSecret()) {
95
- console.warn(
96
- 'WARNING: SESSION_SECRET uses a known placeholder value. Replace it with a random secret before exposing this server.'
97
- );
98
- }
99
-
100
88
  const app = express();
101
89
  app.disable('x-powered-by');
102
90
  const httpServer = createServer(app);
@@ -1 +1 @@
1
- 03e50077364c472b1caef378bca9ddc5
1
+ 2cdc5aa958700a8fae243c563d18825d
@@ -37,6 +37,6 @@ _flutter.buildConfig = {"engineRevision":"42d3d75a56efe1a2e9902f52dc8006099c45d9
37
37
 
38
38
  _flutter.loader.load({
39
39
  serviceWorkerSettings: {
40
- serviceWorkerVersion: "167614730" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
40
+ serviceWorkerVersion: "1009655341" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
41
41
  }
42
42
  });
@@ -127230,7 +127230,7 @@ r===$&&A.b()
127230
127230
  o.push(A.id(p,A.iS(!1,new A.a3(B.tE,A.dZ(new A.cU(B.h8,new A.a5p(r,p),p),p,p),p),!1,B.I,!0),p,p,0,0,0,p))}r=!1
127231
127231
  if(!s.ay)if(!s.ch){r=s.e
127232
127232
  r===$&&A.b()
127233
- r=B.b.A("mp05guq4-d8088a9").length!==0&&r.b}if(r){r=s.d
127233
+ r=B.b.A("mp0wqm0n-9104fe4").length!==0&&r.b}if(r){r=s.d
127234
127234
  r===$&&A.b()
127235
127235
  r=r.V&&!r.a0?84:0
127236
127236
  q=s.e
@@ -131894,7 +131894,7 @@ $S:324}
131894
131894
  A.Y0.prototype={}
131895
131895
  A.R0.prototype={
131896
131896
  mJ(a){var s=this
131897
- if(B.b.A("mp05guq4-d8088a9").length===0||s.a!=null)return
131897
+ if(B.b.A("mp0wqm0n-9104fe4").length===0||s.a!=null)return
131898
131898
  s.zY()
131899
131899
  s.a=A.pN(B.Pr,new A.b3g(s))},
131900
131900
  zY(){var s=0,r=A.l(t.H),q,p=2,o=[],n=this,m,l,k,j,i,h,g,f
@@ -131912,7 +131912,7 @@ if(!t.f.b(k)){s=1
131912
131912
  break}i=J.Z(k,"buildId")
131913
131913
  h=i==null?null:B.b.A(J.r(i))
131914
131914
  j=h==null?"":h
131915
- if(J.bi(j)===0||J.c(j,"mp05guq4-d8088a9")){s=1
131915
+ if(J.bi(j)===0||J.c(j,"mp0wqm0n-9104fe4")){s=1
131916
131916
  break}n.b=!0
131917
131917
  n.J()
131918
131918
  p=2
@@ -131929,7 +131929,7 @@ case 2:return A.i(o.at(-1),r)}})
131929
131929
  return A.k($async$zY,r)},
131930
131930
  v_(){var s=0,r=A.l(t.H),q,p=2,o=[],n=this,m,l,k,j,i,h,g,f,e,d,c,b,a,a0,a1
131931
131931
  var $async$v_=A.h(function(a2,a3){if(a2===1){o.push(a3)
131932
- s=p}for(;;)switch(s){case 0:if(B.b.A("mp05guq4-d8088a9").length===0||n.c){s=1
131932
+ s=p}for(;;)switch(s){case 0:if(B.b.A("mp0wqm0n-9104fe4").length===0||n.c){s=1
131933
131933
  break}n.c=!0
131934
131934
  n.J()
131935
131935
  p=4
@@ -43,6 +43,7 @@ function createWorkflow(config) {
43
43
  supportingCapabilities: request.supportingCapabilities,
44
44
  preferredTools: [...this.preferredTools],
45
45
  expectedOutputs: [...this.expectedOutputs],
46
+ summaryHints: [...this.summaryHints],
46
47
  validationRules: [
47
48
  `Produce deliverable artifacts of type "${config.type}" before finishing.`,
48
49
  'Mention output files or artifact links explicitly in the final response when available.',
@@ -110,10 +111,15 @@ const WORKFLOWS = [
110
111
  createWorkflow({
111
112
  type: 'slides',
112
113
  displayName: 'Slides',
113
- preferredTools: ['write_file', 'edit_file', 'execute_command', 'browser_screenshot', 'generate_image'],
114
+ preferredTools: ['generate_slide_deck', 'write_file', 'edit_file', 'execute_command', 'browser_screenshot', 'generate_image'],
114
115
  expectedOutputs: ['presentation deck', 'exported slide file', 'visual proof'],
115
116
  expectedArtifactKinds: ['slides', 'document', 'image'],
116
117
  expectedExtensions: ['.ppt', '.pptx', '.pdf', '.html', '.png', '.jpg', '.jpeg'],
118
+ summaryHints: [
119
+ 'Prefer generate_slide_deck for final deck creation and export.',
120
+ 'Use 5-12 slides with clear titles and concise body copy.',
121
+ 'Request pdf or pdf+pptx exports when the user wants a finished shareable deck.',
122
+ ],
117
123
  }),
118
124
  createWorkflow({
119
125
  type: 'document',
@@ -161,10 +167,15 @@ const WORKFLOWS = [
161
167
  createWorkflow({
162
168
  type: 'video',
163
169
  displayName: 'Video',
164
- preferredTools: ['execute_command', 'write_file', 'edit_file'],
170
+ preferredTools: ['generate_video_with_remotion', 'execute_command', 'write_file', 'edit_file'],
165
171
  expectedOutputs: ['rendered video asset'],
166
172
  expectedArtifactKinds: ['video'],
167
173
  expectedExtensions: ['.mp4', '.mov', '.m4v', '.webm'],
174
+ summaryHints: [
175
+ 'Prefer generate_video_with_remotion for the final rendered asset.',
176
+ 'Use 3-10 scenes with explicit duration_seconds and concise on-screen text.',
177
+ 'Attach local assets by absolute image_path or audio_path when available.',
178
+ ],
168
179
  }),
169
180
  ];
170
181
 
@@ -184,6 +195,7 @@ function buildDeliverableWorkflowGuidance(plan) {
184
195
  plan.requestedOutputs?.length ? `Requested outputs: ${plan.requestedOutputs.join(', ')}` : '',
185
196
  plan.preferredTools?.length ? `Preferred tools/capabilities: ${plan.preferredTools.join(', ')}` : '',
186
197
  plan.expectedOutputs?.length ? `Expected artifacts: ${plan.expectedOutputs.join(', ')}` : '',
198
+ plan.summaryHints?.length ? `Execution guidance: ${plan.summaryHints.join(' ')}` : '',
187
199
  'Before finishing, ensure the final deliverable exists, validate it, and summarize the produced artifacts clearly.',
188
200
  ].filter(Boolean).join('\n');
189
201
  }
@@ -0,0 +1,98 @@
1
+ 'use strict';
2
+
3
+ const { generateSlideDeck } = require('./slidev');
4
+ const { generateVideoWithRemotion } = require('./remotion');
5
+
6
+ function getIntegratedToolDefinitions() {
7
+ return [
8
+ {
9
+ name: 'generate_slide_deck',
10
+ description: 'Generate a polished presentation using Slidev and export finished artifacts. Prefer this for decks instead of raw file-writing. Best practice: pass a clear title and a complete slides array, and request export_formats ["pdf"] or ["pdf","pptx"] for a shareable final result.',
11
+ parameters: {
12
+ type: 'object',
13
+ properties: {
14
+ title: { type: 'string', description: 'Deck title.' },
15
+ subtitle: { type: 'string', description: 'Optional deck subtitle or framing line.' },
16
+ theme: { type: 'string', description: 'Slidev theme name. Defaults to "default".' },
17
+ deck_markdown: { type: 'string', description: 'Optional full Slidev markdown deck. Use this when you want exact Slidev syntax control.' },
18
+ slides: {
19
+ type: 'array',
20
+ description: 'Structured slide definitions. Use this for most decks if you do not need custom Slidev markdown.',
21
+ items: {
22
+ type: 'object',
23
+ properties: {
24
+ title: { type: 'string', description: 'Slide title.' },
25
+ body: { type: 'string', description: 'Short narrative paragraph or statement.' },
26
+ bullets: { type: 'array', items: { type: 'string' }, description: 'Bullet list for the slide.' },
27
+ notes: { type: 'string', description: 'Presenter notes.' },
28
+ image_url: { type: 'string', description: 'Optional remote image URL to embed.' },
29
+ image_path: { type: 'string', description: 'Optional absolute local image path to embed.' },
30
+ layout: { type: 'string', description: 'Optional Slidev layout, for example cover, section, statement, quote, or two-cols.' },
31
+ className: { type: 'string', description: 'Optional Slidev class value.' },
32
+ },
33
+ },
34
+ },
35
+ export_formats: {
36
+ type: 'array',
37
+ items: { type: 'string', enum: ['pdf', 'pptx', 'png'] },
38
+ description: 'Finished output formats. Defaults to ["pdf"].',
39
+ },
40
+ filename_base: { type: 'string', description: 'Optional output filename base.' },
41
+ },
42
+ required: ['title'],
43
+ },
44
+ },
45
+ {
46
+ name: 'generate_video_with_remotion',
47
+ description: 'Generate a finished MP4 video using Remotion. Prefer this for explainers, launch videos, reels, and narrated visual summaries. Best practice: pass 3-10 scenes with explicit duration_seconds, concise on-screen text, and optional image_path or image_url assets.',
48
+ parameters: {
49
+ type: 'object',
50
+ properties: {
51
+ title: { type: 'string', description: 'Video title or opening headline.' },
52
+ subtitle: { type: 'string', description: 'Optional supporting line.' },
53
+ style: { type: 'string', description: 'High-level visual style direction.' },
54
+ aspect_ratio: { type: 'string', enum: ['16:9', '9:16', '1:1', '4:5'], description: 'Video canvas aspect ratio. Defaults to 16:9.' },
55
+ fps: { type: 'number', description: 'Frames per second. Defaults to 30.' },
56
+ audio_path: { type: 'string', description: 'Optional absolute local path to a soundtrack or voiceover file.' },
57
+ scenes: {
58
+ type: 'array',
59
+ description: 'Scene list in playback order.',
60
+ items: {
61
+ type: 'object',
62
+ properties: {
63
+ title: { type: 'string', description: 'Scene headline.' },
64
+ body: { type: 'string', description: 'Supporting sentence or paragraph.' },
65
+ bullets: { type: 'array', items: { type: 'string' }, description: 'Optional bullets to show in the scene.' },
66
+ duration_seconds: { type: 'number', description: 'Scene duration in seconds.' },
67
+ image_url: { type: 'string', description: 'Optional remote image URL.' },
68
+ image_path: { type: 'string', description: 'Optional absolute local image path.' },
69
+ accent_color: { type: 'string', description: 'Optional accent color, for example #7dd3fc.' },
70
+ background_color: { type: 'string', description: 'Optional scene background color.' },
71
+ align: { type: 'string', enum: ['left', 'center', 'right'], description: 'Text alignment.' },
72
+ },
73
+ required: ['title'],
74
+ },
75
+ },
76
+ filename_base: { type: 'string', description: 'Optional output filename base.' },
77
+ },
78
+ required: ['title', 'scenes'],
79
+ },
80
+ },
81
+ ];
82
+ }
83
+
84
+ async function executeIntegratedTool(toolName, args, context = {}) {
85
+ switch (String(toolName || '').trim()) {
86
+ case 'generate_slide_deck':
87
+ return generateSlideDeck(args, context);
88
+ case 'generate_video_with_remotion':
89
+ return generateVideoWithRemotion(args, context);
90
+ default:
91
+ return null;
92
+ }
93
+ }
94
+
95
+ module.exports = {
96
+ executeIntegratedTool,
97
+ getIntegratedToolDefinitions,
98
+ };