zilmate 1.7.6 → 1.7.8

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/setup.js CHANGED
@@ -1,17 +1,14 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import { readFile, writeFile } from 'node:fs/promises';
3
3
  import { randomBytes, randomUUID } from 'node:crypto';
4
- import { execFile, spawn } from 'node:child_process';
4
+ import { execFile } from 'node:child_process';
5
5
  import { promisify } from 'node:util';
6
6
  import readline from 'node:readline/promises';
7
7
  import { stdin as input, stdout as output } from 'node:process';
8
8
  import chalk from 'chalk';
9
9
  import { printPanel, printZilMateBanner } from './format.js';
10
10
  import { runCameraDoctor } from '../tools/desktop.tool.js';
11
- import { initWorkspace } from '../workspace/init.js';
12
11
  import { workspaceLayout } from '../workspace/paths.js';
13
- import { startCloudflareQuickTunnel } from './tunnel.js';
14
- import { startJobWebhookServer } from '../jobs/webhook-server.js';
15
12
  const execFileAsync = promisify(execFile);
16
13
  const defaults = {
17
14
  ZILO_MANAGER_MODEL: 'minimax/minimax-m3',
@@ -47,7 +44,7 @@ function parseEnvFile(content) {
47
44
  const [, key, rawValue] = match;
48
45
  if (!key || rawValue === undefined)
49
46
  continue;
50
- values.set(key, rawValue.replace(/^"(.*)"$/, '$1'));
47
+ values.set(key, rawValue.replace(/^"(.*)"$/, ''));
51
48
  }
52
49
  return values;
53
50
  }
@@ -70,487 +67,280 @@ async function askOptionalSecret(rl, prompt) {
70
67
  const value = await rl.question(prompt);
71
68
  return value.trim();
72
69
  }
73
- async function askYesNo(rl, prompt, defaultValue = false) {
74
- const suffix = defaultValue ? 'Y/n' : 'y/N';
75
- const answer = (await rl.question(`${prompt} (${suffix}) `)).trim().toLowerCase();
70
+ async function askYesNo(rl, question, fallback) {
71
+ const answer = (await rl.question(`${question} (${fallback ? 'Y/n' : 'y/N'}): `)).trim().toLowerCase();
76
72
  if (!answer)
77
- return defaultValue;
73
+ return fallback;
78
74
  return answer === 'y' || answer === 'yes';
79
75
  }
80
- async function askSection(rl, title, description, prompt, defaultValue = false) {
76
+ async function askSection(rl, title, description, question, fallback) {
81
77
  console.log(chalk.cyan(`\n${title}`));
82
78
  console.log(chalk.gray(description));
83
- return askYesNo(rl, prompt, defaultValue);
79
+ return askYesNo(rl, question, fallback);
84
80
  }
85
- async function commandExists(command) {
86
- const probe = process.platform === 'win32' ? 'where.exe' : 'which';
81
+ async function readEnvValues(path) {
82
+ if (!existsSync(path))
83
+ return new Map();
87
84
  try {
88
- await execFileAsync(probe, [command], { windowsHide: true, timeout: 5000 });
89
- return true;
85
+ const content = await readFile(path, 'utf8');
86
+ return parseEnvFile(content);
90
87
  }
91
88
  catch {
92
- return false;
93
- }
94
- }
95
- function cameraInstallCommand() {
96
- if (process.platform === 'win32') {
97
- return {
98
- command: 'winget',
99
- args: ['install', '--id', 'Gyan.FFmpeg', '-e', '--accept-package-agreements', '--accept-source-agreements'],
100
- label: 'winget install --id Gyan.FFmpeg -e',
101
- };
102
- }
103
- if (process.platform === 'darwin')
104
- return { command: 'brew', args: ['install', 'ffmpeg'], label: 'brew install ffmpeg' };
105
- if (process.platform === 'linux')
106
- return { command: 'sudo', args: ['apt-get', 'install', '-y', 'ffmpeg'], label: 'sudo apt-get install -y ffmpeg' };
107
- return undefined;
108
- }
109
- async function installCameraDependency() {
110
- const install = cameraInstallCommand();
111
- if (!install) {
112
- console.log(chalk.yellow('No automatic ffmpeg installer is known for this OS. Install ffmpeg manually, then run `zilmate camera doctor`.'));
113
- return false;
114
- }
115
- if (process.platform === 'win32' && !(await commandExists('winget'))) {
116
- console.log(chalk.yellow('winget is not available. Install ffmpeg manually or with another package manager, then run `zilmate camera doctor`.'));
117
- return false;
118
- }
119
- if (process.platform === 'darwin' && !(await commandExists('brew'))) {
120
- console.log(chalk.yellow('Homebrew is not available. Install Homebrew or install ffmpeg manually, then run `zilmate camera doctor`.'));
121
- return false;
122
- }
123
- if (process.platform === 'linux' && !(await commandExists('sudo'))) {
124
- console.log(chalk.yellow(`Automatic install needs sudo. Run manually: ${install.label}`));
125
- return false;
89
+ return new Map();
126
90
  }
127
- console.log(chalk.gray(`Running: ${install.label}`));
128
- await new Promise((resolve, reject) => {
129
- const child = spawn(install.command, install.args, { stdio: 'inherit', windowsHide: false });
130
- child.on('error', reject);
131
- child.on('close', (code) => {
132
- if (code === 0)
133
- resolve();
134
- else
135
- reject(new Error(`${install.label} exited with code ${code}`));
136
- });
137
- });
138
- return commandExists('ffmpeg');
139
- }
140
- function cloudflareInstallCommand() {
141
- if (process.platform === 'win32') {
142
- return {
143
- command: 'winget',
144
- args: ['install', '--id', 'Cloudflare.cloudflared', '-e', '--accept-package-agreements', '--accept-source-agreements'],
145
- label: 'winget install --id Cloudflare.cloudflared -e',
146
- };
147
- }
148
- if (process.platform === 'darwin')
149
- return { command: 'brew', args: ['install', 'cloudflared'], label: 'brew install cloudflared' };
150
- if (process.platform === 'linux')
151
- return { command: 'sudo', args: ['apt-get', 'install', '-y', 'cloudflared'], label: 'sudo apt-get install -y cloudflared' };
152
- return undefined;
153
- }
154
- async function installCloudflareDependency() {
155
- const install = cloudflareInstallCommand();
156
- if (!install) {
157
- console.log(chalk.yellow('No automatic cloudflared installer is known for this OS. Install cloudflared manually, then run `zilmate jobs listen --tunnel`.'));
158
- return false;
159
- }
160
- if (process.platform === 'win32' && !(await commandExists('winget'))) {
161
- console.log(chalk.yellow('winget is not available. Install cloudflared manually or with another package manager.'));
162
- return false;
163
- }
164
- if (process.platform === 'darwin' && !(await commandExists('brew'))) {
165
- console.log(chalk.yellow('Homebrew is not available. Install Homebrew or install cloudflared manually.'));
166
- return false;
167
- }
168
- if (process.platform === 'linux' && !(await commandExists('sudo'))) {
169
- console.log(chalk.yellow(`Automatic install needs sudo. Run manually: ${install.label}`));
170
- return false;
171
- }
172
- console.log(chalk.gray(`Running: ${install.label}`));
173
- await new Promise((resolve, reject) => {
174
- const child = spawn(install.command, install.args, { stdio: 'inherit', windowsHide: false });
175
- child.on('error', reject);
176
- child.on('close', (code) => {
177
- if (code === 0)
178
- resolve();
179
- else
180
- reject(new Error(`${install.label} exited with code ${code}`));
181
- });
182
- });
183
- return commandExists('cloudflared');
184
- }
185
- function buildEnv(values) {
186
- const lines = [
187
- ['AI_GATEWAY_API_KEY', values.get('AI_GATEWAY_API_KEY') || ''],
188
- ['COMPOSIO_API_KEY', values.get('COMPOSIO_API_KEY') || ''],
189
- ['ZILMATE_USER_ID', values.get('ZILMATE_USER_ID') || ''],
190
- ['TAVILY_API_KEY', values.get('TAVILY_API_KEY') || ''],
191
- ['UPSTASH_REDIS_REST_URL', values.get('UPSTASH_REDIS_REST_URL') || ''],
192
- ['UPSTASH_REDIS_REST_TOKEN', values.get('UPSTASH_REDIS_REST_TOKEN') || ''],
193
- ['ZILMATE_JOBS_ENABLED', values.get('ZILMATE_JOBS_ENABLED') || 'false'],
194
- ['UPSTASH_QSTASH_TOKEN', values.get('UPSTASH_QSTASH_TOKEN') || ''],
195
- ['ZILMATE_PUBLIC_JOB_WEBHOOK_URL', values.get('ZILMATE_PUBLIC_JOB_WEBHOOK_URL') || ''],
196
- ['ZILMATE_JOB_WEBHOOK_SECRET', values.get('ZILMATE_JOB_WEBHOOK_SECRET') || ''],
197
- ['ZILMATE_TRIGGER_WORKFLOWS_ENABLED', values.get('ZILMATE_TRIGGER_WORKFLOWS_ENABLED') || 'false'],
198
- ['DEEPGRAM_API_KEY', values.get('DEEPGRAM_API_KEY') || ''],
199
- ['ZILMATE_VOICE_ENABLED', values.get('ZILMATE_VOICE_ENABLED') || 'false'],
200
- ['ZILMATE_VOICE_MODE', values.get('ZILMATE_VOICE_MODE') || 'agent'],
201
- ['ZILMATE_VOICE_LISTEN_MODEL', values.get('ZILMATE_VOICE_LISTEN_MODEL') || 'flux-general-en'],
202
- ['ZILMATE_VOICE_LISTEN_VERSION', values.get('ZILMATE_VOICE_LISTEN_VERSION') || 'v2'],
203
- ['ZILMATE_VOICE_TTS_MODEL', values.get('ZILMATE_VOICE_TTS_MODEL') || 'aura-2-thalia-en'],
204
- ['ZILMATE_VOICE_LANGUAGE', values.get('ZILMATE_VOICE_LANGUAGE') || 'en'],
205
- ['ZILMATE_VOICE_LANGUAGE_HINTS', values.get('ZILMATE_VOICE_LANGUAGE_HINTS') || ''],
206
- ['ZILMATE_VOICE_BARGE_IN', values.get('ZILMATE_VOICE_BARGE_IN') || 'true'],
207
- ['ZILMATE_VOICE_INPUT_DEVICE', values.get('ZILMATE_VOICE_INPUT_DEVICE') || defaults.ZILMATE_VOICE_INPUT_DEVICE],
208
- ['ZILMATE_SCREENSHOT_MODEL', values.get('ZILMATE_SCREENSHOT_MODEL') || defaults.ZILMATE_SCREENSHOT_MODEL],
209
- ['ZILMATE_CAMERA_DEVICE', values.get('ZILMATE_CAMERA_DEVICE') || defaults.ZILMATE_CAMERA_DEVICE],
210
- ['ZILMATE_FILE_ROOTS', values.get('ZILMATE_FILE_ROOTS') || defaults.ZILMATE_FILE_ROOTS],
211
- ['ZILMATE_WORKSPACE', values.get('ZILMATE_WORKSPACE') || ''],
212
- ['ZILMATE_WEBHOOK_PORT', values.get('ZILMATE_WEBHOOK_PORT') || '8787'],
213
- ['ZILO_MANAGER_MODEL', values.get('ZILO_MANAGER_MODEL') || defaults.ZILO_MANAGER_MODEL],
214
- ['ZILO_HELP_MODEL', values.get('ZILO_HELP_MODEL') || defaults.ZILO_HELP_MODEL],
215
- ['ZILO_POST_MODEL', values.get('ZILO_POST_MODEL') || defaults.ZILO_POST_MODEL],
216
- ['ZILO_IMAGE_DEFAULT_PROVIDER', values.get('ZILO_IMAGE_DEFAULT_PROVIDER') || defaults.ZILO_IMAGE_DEFAULT_PROVIDER],
217
- ['ZILO_IMAGE_OPENAI_MODEL', values.get('ZILO_IMAGE_OPENAI_MODEL') || defaults.ZILO_IMAGE_OPENAI_MODEL],
218
- ['ZILO_IMAGE_GEMINI_MODEL', values.get('ZILO_IMAGE_GEMINI_MODEL') || defaults.ZILO_IMAGE_GEMINI_MODEL],
219
- ['ZILO_IMAGE_MODEL', values.get('ZILO_IMAGE_MODEL') || defaults.ZILO_IMAGE_MODEL],
220
- ];
221
- return `${lines.map(([key, value]) => `${key}=${formatEnvValue(value)}`).join('\n')}\n`;
222
- }
223
- function printSetupPrep() {
224
- printPanel('Before setup starts', [
225
- ['Required', 'AI Gateway key'],
226
- ['Apps/tools', 'Composio key if you want Gmail, Slack, GitHub, Notion, etc.'],
227
- ['Web research', 'Tavily key if you want live web research'],
228
- ['Memory/jobs', 'Upstash Redis keys if you want cloud-backed storage'],
229
- ['Hosted schedules', 'QStash token; optional Cloudflare tunnel for public webhook'],
230
- ['Voice', 'Deepgram key if you want realtime voice'],
231
- ['Camera', 'ffmpeg if you want laptop camera capture'],
232
- ]);
233
- console.log(chalk.gray('You can skip any optional section and run setup again later.'));
234
91
  }
235
- async function readEnvValues(envPath) {
236
- return existsSync(envPath) ? parseEnvFile(await readFile(envPath, 'utf8')) : new Map();
237
- }
238
- async function writeEnvValues(envPath, values, options = {}) {
239
- const disk = await readEnvValues(envPath);
240
- const merged = new Map(disk);
241
- if (options.merge) {
242
- for (const [key, value] of values) {
243
- const onDisk = merged.get(key);
244
- const touched = options.touchedKeys?.has(key);
245
- if (touched) {
246
- if (value !== undefined)
247
- merged.set(key, value);
92
+ async function writeEnvValues(path, values, options = {}) {
93
+ let content = '';
94
+ if (options.merge && existsSync(path)) {
95
+ const existing = await readFile(path, 'utf8');
96
+ const existingLines = existing.split(/\r?\n/);
97
+ const usedKeys = new Set();
98
+ for (const line of existingLines) {
99
+ const trimmed = line.trim();
100
+ const match = /^([A-Z0-9_]+)=/.exec(trimmed);
101
+ if (match && values.has(match[1]) && (!options.touchedKeys || options.touchedKeys.has(match[1]))) {
102
+ content += `${match[1]}=${formatEnvValue(values.get(match[1]))}\n`;
103
+ usedKeys.add(match[1]);
248
104
  }
249
- else if (!onDisk || onDisk.trim() === '') {
250
- merged.set(key, value);
105
+ else {
106
+ content += `${line}\n`;
251
107
  }
252
108
  }
253
- }
254
- else {
255
- for (const [key, value] of values)
256
- merged.set(key, value);
257
- }
258
- for (const [key, value] of Object.entries(defaults)) {
259
- if (!merged.has(key) || merged.get(key)?.trim() === '')
260
- merged.set(key, value);
261
- }
262
- await writeFile(envPath, buildEnv(merged), 'utf8');
263
- }
264
- function envHasValue(values, key) {
265
- const value = values.get(key);
266
- return Boolean(value && value.trim() !== '');
267
- }
268
- async function configureCloudflareTunnelSection(rl, values) {
269
- console.log(chalk.cyan('\nCloudflare quick tunnel'));
270
- console.log(chalk.gray('QStash hosted schedules need a public HTTPS webhook. cloudflared creates a free quick tunnel to your laptop.'));
271
- console.log(chalk.gray('Install: https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/'));
272
- console.log(chalk.gray('While your laptop is on, keep the tunnel alive with: zilmate jobs listen --tunnel'));
273
- let hasCloudflared = await commandExists('cloudflared');
274
- if (!hasCloudflared) {
275
- if (await askYesNo(rl, 'cloudflared is missing. Install Cloudflare tunnel dependency now?', false)) {
276
- try {
277
- hasCloudflared = await installCloudflareDependency();
278
- console.log(hasCloudflared ? chalk.green('cloudflared is ready.') : chalk.yellow('cloudflared was not detected after install.'));
279
- }
280
- catch (error) {
281
- console.log(chalk.yellow(error instanceof Error ? error.message : String(error)));
109
+ for (const [key, value] of values) {
110
+ if (!usedKeys.has(key)) {
111
+ content += `${key}=${formatEnvValue(value)}\n`;
282
112
  }
283
113
  }
284
114
  }
285
- if (!hasCloudflared) {
286
- console.log(chalk.yellow('cloudflared not found on PATH. Install it, or paste ZILMATE_PUBLIC_JOB_WEBHOOK_URL manually.'));
287
- return;
288
- }
289
- if (envHasValue(values, 'ZILMATE_PUBLIC_JOB_WEBHOOK_URL')) {
290
- console.log(chalk.green(`Existing webhook: ${values.get('ZILMATE_PUBLIC_JOB_WEBHOOK_URL')}`));
291
- if (!await askYesNo(rl, 'Create a new Cloudflare quick tunnel URL?', false))
292
- return;
115
+ else {
116
+ for (const [key, value] of values) {
117
+ content += `${key}=${formatEnvValue(value)}\n`;
118
+ }
293
119
  }
294
- await maybeCreateCloudflareWebhook(rl, values);
120
+ await writeFile(path, content.trim() + '\n', 'utf8');
295
121
  }
296
- async function maybeCreateCloudflareWebhook(rl, values) {
297
- let hasCloudflared = await commandExists('cloudflared');
298
- if (!hasCloudflared) {
299
- if (await askYesNo(rl, 'cloudflared is missing. Install Cloudflare tunnel dependency now?', false)) {
300
- try {
301
- hasCloudflared = await installCloudflareDependency();
302
- console.log(hasCloudflared ? chalk.green('cloudflared is ready.') : chalk.yellow('cloudflared was not detected after install.'));
303
- }
304
- catch (error) {
305
- console.log(chalk.yellow(error instanceof Error ? error.message : String(error)));
306
- }
307
- }
122
+ async function commandExists(command) {
123
+ try {
124
+ await execFileAsync(command, ['--version']);
125
+ return true;
308
126
  }
309
- if (!hasCloudflared) {
310
- console.log(chalk.yellow('cloudflared not found. Install it to auto-create a public webhook URL, or enter ZILMATE_PUBLIC_JOB_WEBHOOK_URL manually.'));
311
- return;
127
+ catch {
128
+ return false;
312
129
  }
313
- const createTunnel = await askYesNo(rl, 'Create a Cloudflare quick tunnel for the job webhook now? (requires cloudflared)', false);
314
- if (!createTunnel)
315
- return;
316
- const port = Number.parseInt(values.get('ZILMATE_WEBHOOK_PORT') || '8787', 10) || 8787;
317
- console.log(chalk.gray(`Starting local webhook on port ${port} and Cloudflare tunnel...`));
318
- const server = await startJobWebhookServer(port);
319
- try {
320
- const tunnel = await startCloudflareQuickTunnel(server.url);
321
- const webhookUrl = `${tunnel.url.replace(/\/$/, '')}/jobs/webhook`;
322
- values.set('ZILMATE_PUBLIC_JOB_WEBHOOK_URL', webhookUrl);
323
- values.set('ZILMATE_WEBHOOK_PORT', String(port));
324
- console.log(chalk.green(`Public webhook URL: ${webhookUrl}`));
325
- console.log(chalk.yellow('Quick tunnels change when restarted. Run `zilmate jobs listen --tunnel` while your laptop is on to keep QStash reachable.'));
130
+ }
131
+ async function installCameraDependency() {
132
+ if (process.platform === 'darwin') {
133
+ await execFileAsync('brew', ['install', 'ffmpeg']);
134
+ return true;
326
135
  }
327
- finally {
328
- await server.close();
136
+ if (process.platform === 'linux') {
137
+ await execFileAsync('sudo', ['apt-get', 'update']);
138
+ await execFileAsync('sudo', ['apt-get', 'install', '-y', 'ffmpeg']);
139
+ return true;
329
140
  }
141
+ return false;
330
142
  }
331
143
  export async function runSetup(options = {}) {
332
144
  const envPath = options.path || '.env';
333
145
  const existing = await readEnvValues(envPath);
334
146
  const rl = readline.createInterface({ input, output });
147
+ const mergeMode = options.force ? false : existing.size > 0;
335
148
  const touchedKeys = new Set();
336
- const mergeMode = existing.size > 0 && !options.force;
337
149
  try {
338
- printZilMateBanner('Setup');
339
- console.log(chalk.gray(`Config file: ${envPath}`));
150
+ printZilMateBanner('ZilMate Setup');
340
151
  if (mergeMode) {
341
- console.log(chalk.green('Merge mode: existing .env values are preserved. Only missing keys will be added.'));
342
- console.log(chalk.gray('Use --force to reconfigure all sections, or answer yes when prompted to replace a specific key.'));
152
+ console.log(chalk.gray(`Merging settings into existing ${envPath}.`));
343
153
  }
344
154
  else {
345
- console.log(chalk.gray('Only AI Gateway is required. Everything else can be skipped, enabled, disabled, or changed later.'));
155
+ console.log(chalk.gray(`Creating new ${envPath}.`));
346
156
  }
347
- if (!options.yes)
348
- printSetupPrep();
349
157
  const values = new Map(existing);
350
- const layout = await initWorkspace();
351
- if (!values.get('ZILMATE_WORKSPACE')) {
352
- values.set('ZILMATE_WORKSPACE', layout.root);
353
- touchedKeys.add('ZILMATE_WORKSPACE');
354
- }
355
- console.log(chalk.gray(`ZilMate workspace: ${layout.root}`));
356
- if (options.aiGatewayKey)
357
- values.set('AI_GATEWAY_API_KEY', options.aiGatewayKey);
358
- if (options.composioKey !== undefined)
359
- values.set('COMPOSIO_API_KEY', options.composioKey);
360
- if (options.zilmateUserId !== undefined)
361
- values.set('ZILMATE_USER_ID', options.zilmateUserId);
362
- if (options.tavilyKey !== undefined)
363
- values.set('TAVILY_API_KEY', options.tavilyKey);
364
- if (options.redisUrl !== undefined)
365
- values.set('UPSTASH_REDIS_REST_URL', options.redisUrl);
366
- if (options.redisToken !== undefined)
367
- values.set('UPSTASH_REDIS_REST_TOKEN', options.redisToken);
368
- if (options.jobsEnabled !== undefined)
369
- values.set('ZILMATE_JOBS_ENABLED', normalizeBooleanOption(options.jobsEnabled) ? 'true' : 'false');
370
- if (options.qstashToken !== undefined)
371
- values.set('UPSTASH_QSTASH_TOKEN', options.qstashToken);
372
- if (options.publicJobWebhookUrl !== undefined)
373
- values.set('ZILMATE_PUBLIC_JOB_WEBHOOK_URL', options.publicJobWebhookUrl);
374
- if (options.jobWebhookSecret !== undefined)
375
- values.set('ZILMATE_JOB_WEBHOOK_SECRET', options.jobWebhookSecret);
376
- if (options.triggerWorkflowsEnabled !== undefined)
377
- values.set('ZILMATE_TRIGGER_WORKFLOWS_ENABLED', normalizeBooleanOption(options.triggerWorkflowsEnabled) ? 'true' : 'false');
378
- if (options.deepgramApiKey !== undefined)
379
- values.set('DEEPGRAM_API_KEY', options.deepgramApiKey);
380
- if (options.voiceEnabled !== undefined)
381
- values.set('ZILMATE_VOICE_ENABLED', normalizeBooleanOption(options.voiceEnabled) ? 'true' : 'false');
382
- if (options.voiceListenModel !== undefined) {
383
- values.set('ZILMATE_VOICE_LISTEN_MODEL', options.voiceListenModel);
384
- values.set('ZILMATE_VOICE_LISTEN_VERSION', options.voiceListenModel.startsWith('flux-') ? 'v2' : 'v1');
385
- }
386
- if (options.voiceTtsModel !== undefined)
387
- values.set('ZILMATE_VOICE_TTS_MODEL', options.voiceTtsModel);
388
- if (options.voiceLanguage !== undefined)
389
- values.set('ZILMATE_VOICE_LANGUAGE', options.voiceLanguage);
390
- if (options.voiceInputDevice !== undefined)
391
- values.set('ZILMATE_VOICE_INPUT_DEVICE', options.voiceInputDevice);
392
- if (options.cameraDevice !== undefined)
393
- values.set('ZILMATE_CAMERA_DEVICE', options.cameraDevice);
394
- if (options.screenshotModel !== undefined)
395
- values.set('ZILMATE_SCREENSHOT_MODEL', options.screenshotModel);
396
- if (options.fileRoots !== undefined)
397
- values.set('ZILMATE_FILE_ROOTS', options.fileRoots);
398
- const installCloudflareDeps = options.installCloudflareDeps === undefined ? undefined : normalizeBooleanOption(options.installCloudflareDeps);
399
- if (installCloudflareDeps && !(await commandExists('cloudflared'))) {
400
- await installCloudflareDependency();
401
- }
402
- const currentGatewayKey = values.get('AI_GATEWAY_API_KEY');
403
158
  if (options.aiGatewayKey) {
404
159
  values.set('AI_GATEWAY_API_KEY', options.aiGatewayKey);
160
+ touchedKeys.add('AI_GATEWAY_API_KEY');
405
161
  }
406
- else if (!currentGatewayKey && options.yes) {
407
- throw new Error('AI_GATEWAY_API_KEY is required. Pass --ai-gateway-key or run setup interactively.');
408
- }
409
- else if (currentGatewayKey && !options.yes) {
410
- if (!mergeMode || await askYesNo(rl, 'AI_GATEWAY_API_KEY already exists. Replace it?', false)) {
411
- values.set('AI_GATEWAY_API_KEY', await askRequiredSecret(rl, 'AI_GATEWAY_API_KEY: '));
412
- touchedKeys.add('AI_GATEWAY_API_KEY');
413
- }
414
- }
415
- else if (!currentGatewayKey) {
162
+ else if (!values.has('AI_GATEWAY_API_KEY') || options.force) {
163
+ console.log(chalk.cyan('\nAI Gateway'));
164
+ console.log(chalk.gray('ZilMate uses the Vercel AI Gateway for LLM access. You need an API key from the gateway.'));
416
165
  values.set('AI_GATEWAY_API_KEY', await askRequiredSecret(rl, 'AI_GATEWAY_API_KEY: '));
417
166
  touchedKeys.add('AI_GATEWAY_API_KEY');
418
167
  }
419
- if (!values.get('ZILMATE_USER_ID')) {
420
- values.set('ZILMATE_USER_ID', `zilmate-${randomUUID()}`);
421
- }
422
- if (!options.yes && options.composioKey === undefined && await askSection(rl, 'Composio app tools', 'Connectors let ZilMate use apps like Gmail, Slack, GitHub, Notion, Supabase, Stripe, Linear, Discord, and more through Composio.', 'Enable Composio app tools?', Boolean(values.get('COMPOSIO_API_KEY')))) {
423
- const composioKey = await askOptionalSecret(rl, 'COMPOSIO_API_KEY (blank to skip): ');
424
- values.set('COMPOSIO_API_KEY', composioKey);
425
- }
426
- else if (!options.yes && options.composioKey === undefined) {
427
- values.set('COMPOSIO_API_KEY', '');
428
- }
429
- if (!options.yes && options.tavilyKey === undefined && await askSection(rl, 'Web research', 'Tavily enables live web search, extraction, crawling, mapping, and deeper research when local knowledge is not enough.', 'Enable Tavily web research?', Boolean(values.get('TAVILY_API_KEY')))) {
430
- const tavilyKey = await askOptionalSecret(rl, 'TAVILY_API_KEY (blank to skip): ');
431
- values.set('TAVILY_API_KEY', tavilyKey);
168
+ if (options.composioKey) {
169
+ values.set('COMPOSIO_API_KEY', options.composioKey);
170
+ touchedKeys.add('COMPOSIO_API_KEY');
432
171
  }
433
- else if (!options.yes && options.tavilyKey === undefined) {
434
- values.set('TAVILY_API_KEY', '');
172
+ else if (!options.yes && (!values.has('COMPOSIO_API_KEY') || options.force)) {
173
+ if (await askSection(rl, 'External App Intelligence (Composio)', 'Enable ZilMate to use Stripe, HubSpot, GitHub, Gmail, and 100+ other apps via Composio.', 'Configure Composio now?', true)) {
174
+ values.set('COMPOSIO_API_KEY', await askRequiredSecret(rl, 'COMPOSIO_API_KEY: '));
175
+ touchedKeys.add('COMPOSIO_API_KEY');
176
+ }
435
177
  }
436
- if (!options.yes && options.redisUrl === undefined && options.redisToken === undefined && await askSection(rl, 'Cloud memory and job storage', 'Upstash Redis makes memory, chat history, Composio sessions, and job records portable across hosted/server environments. Without it, ZilMate uses local files.', 'Enable Upstash Redis memory/job storage?', Boolean(values.get('UPSTASH_REDIS_REST_URL') && values.get('UPSTASH_REDIS_REST_TOKEN')))) {
437
- values.set('UPSTASH_REDIS_REST_URL', (await rl.question('UPSTASH_REDIS_REST_URL: ')).trim());
438
- values.set('UPSTASH_REDIS_REST_TOKEN', await askOptionalSecret(rl, 'UPSTASH_REDIS_REST_TOKEN: '));
178
+ if (options.zilmateUserId) {
179
+ values.set('ZILMATE_USER_ID', options.zilmateUserId);
180
+ touchedKeys.add('ZILMATE_USER_ID');
439
181
  }
440
- else if (!options.yes && options.redisUrl === undefined && options.redisToken === undefined) {
441
- values.set('UPSTASH_REDIS_REST_URL', '');
442
- values.set('UPSTASH_REDIS_REST_TOKEN', '');
182
+ else if (values.get('COMPOSIO_API_KEY') && (!values.get('ZILMATE_USER_ID') || options.force)) {
183
+ const suggestedId = `zilmate-${randomUUID().slice(0, 8)}`;
184
+ const userId = (await rl.question(`ZilMate User ID (default: ${suggestedId}): `)).trim();
185
+ values.set('ZILMATE_USER_ID', userId || suggestedId);
186
+ touchedKeys.add('ZILMATE_USER_ID');
443
187
  }
444
- if (!options.yes && options.jobsEnabled === undefined) {
445
- const enableJobs = await askSection(rl, 'Background jobs', 'Local jobs keep running after chat closes while `zilmate jobs worker` is open. They stop if the laptop sleeps or shuts down.', 'Enable local background jobs and schedules?', values.get('ZILMATE_JOBS_ENABLED') === 'true');
446
- values.set('ZILMATE_JOBS_ENABLED', enableJobs ? 'true' : 'false');
188
+ if (options.tavilyKey) {
189
+ values.set('TAVILY_API_KEY', options.tavilyKey);
190
+ touchedKeys.add('TAVILY_API_KEY');
447
191
  }
448
- else if (!values.has('ZILMATE_JOBS_ENABLED')) {
449
- values.set('ZILMATE_JOBS_ENABLED', 'false');
192
+ else if (!options.yes && (!values.has('TAVILY_API_KEY') || options.force)) {
193
+ if (await askSection(rl, 'Web Research (Tavily)', 'Tavily allows ZilMate to perform real-time web searches for accurate up-to-date information.', 'Configure Tavily now?', true)) {
194
+ values.set('TAVILY_API_KEY', await askRequiredSecret(rl, 'TAVILY_API_KEY: '));
195
+ touchedKeys.add('TAVILY_API_KEY');
196
+ }
450
197
  }
451
- if (!options.yes && values.get('ZILMATE_JOBS_ENABLED') === 'true' && !envHasValue(values, 'ZILMATE_PUBLIC_JOB_WEBHOOK_URL')) {
452
- if (await askYesNo(rl, 'Configure Cloudflare quick tunnel for job webhooks now?', false)) {
453
- await configureCloudflareTunnelSection(rl, values);
198
+ if (options.redisUrl && options.redisToken) {
199
+ values.set('UPSTASH_REDIS_REST_URL', options.redisUrl);
200
+ values.set('UPSTASH_REDIS_REST_TOKEN', options.redisToken);
201
+ touchedKeys.add('UPSTASH_REDIS_REST_URL');
202
+ touchedKeys.add('UPSTASH_REDIS_REST_TOKEN');
203
+ }
204
+ else if (!options.yes && (!values.has('UPSTASH_REDIS_REST_URL') || options.force)) {
205
+ if (await askSection(rl, 'Persistent Multi-Instance Memory (Redis)', 'By default, ZilMate uses local files for memory. Use Upstash Redis for distributed memory and reliable background jobs.', 'Configure Upstash Redis now?', false)) {
206
+ values.set('UPSTASH_REDIS_REST_URL', await askRequiredSecret(rl, 'UPSTASH_REDIS_REST_URL: '));
207
+ values.set('UPSTASH_REDIS_REST_TOKEN', await askRequiredSecret(rl, 'UPSTASH_REDIS_REST_TOKEN: '));
208
+ touchedKeys.add('UPSTASH_REDIS_REST_URL');
209
+ touchedKeys.add('UPSTASH_REDIS_REST_TOKEN');
454
210
  }
455
211
  }
456
- if (!options.yes && options.qstashToken === undefined) {
457
- if (await askSection(rl, 'Hosted schedules', 'Use QStash only when you have a hosted public webhook. This is what allows schedules to fire while your laptop is closed.', 'Enable Upstash QStash hosted schedules?', Boolean(values.get('UPSTASH_QSTASH_TOKEN')))) {
458
- values.set('UPSTASH_QSTASH_TOKEN', await askOptionalSecret(rl, 'UPSTASH_QSTASH_TOKEN (blank to skip): '));
459
- const existingWebhook = values.get('ZILMATE_PUBLIC_JOB_WEBHOOK_URL');
460
- if (!existingWebhook) {
461
- await configureCloudflareTunnelSection(rl, values);
462
- }
463
- if (!values.get('ZILMATE_PUBLIC_JOB_WEBHOOK_URL')) {
464
- values.set('ZILMATE_PUBLIC_JOB_WEBHOOK_URL', (await rl.question('ZILMATE_PUBLIC_JOB_WEBHOOK_URL (blank for local only): ')).trim());
465
- }
466
- const existingSecret = values.get('ZILMATE_JOB_WEBHOOK_SECRET');
467
- const secret = await askOptionalSecret(rl, `ZILMATE_JOB_WEBHOOK_SECRET (blank to ${existingSecret ? 'keep existing' : 'auto-generate'}): `);
468
- values.set('ZILMATE_JOB_WEBHOOK_SECRET', secret || existingSecret || newWebhookSecret());
212
+ const jobsEnabled = options.jobsEnabled !== undefined ? normalizeBooleanOption(options.jobsEnabled) : undefined;
213
+ if (jobsEnabled !== undefined) {
214
+ values.set('ZILMATE_JOBS_ENABLED', jobsEnabled ? 'true' : 'false');
215
+ touchedKeys.add('ZILMATE_JOBS_ENABLED');
216
+ }
217
+ else if (!options.yes && (!values.has('ZILMATE_JOBS_ENABLED') || options.force)) {
218
+ if (await askSection(rl, 'Background Jobs', 'ZilMate can run background tasks, schedules, and trigger-based workflows.', 'Enable background jobs?', true)) {
219
+ values.set('ZILMATE_JOBS_ENABLED', 'true');
220
+ touchedKeys.add('ZILMATE_JOBS_ENABLED');
469
221
  }
470
222
  else {
471
- values.set('UPSTASH_QSTASH_TOKEN', '');
472
- values.set('ZILMATE_PUBLIC_JOB_WEBHOOK_URL', '');
223
+ values.set('ZILMATE_JOBS_ENABLED', 'false');
224
+ touchedKeys.add('ZILMATE_JOBS_ENABLED');
473
225
  }
474
226
  }
475
- if (!options.yes && options.triggerWorkflowsEnabled === undefined) {
476
- console.log(chalk.cyan('\nComposio trigger workflows'));
477
- console.log(chalk.gray('When enabled, Composio trigger events can queue ZilMate jobs for Gmail, GitHub, Slack, calendar-style events, and more.'));
478
- const canEnableTriggers = Boolean(values.get('COMPOSIO_API_KEY'));
479
- if (!canEnableTriggers) {
480
- console.log(chalk.yellow('Skipping trigger workflows because Composio is not configured.'));
481
- values.set('ZILMATE_TRIGGER_WORKFLOWS_ENABLED', 'false');
227
+ if (values.get('ZILMATE_JOBS_ENABLED') === 'true') {
228
+ if (options.qstashToken && options.publicJobWebhookUrl) {
229
+ values.set('UPSTASH_QSTASH_TOKEN', options.qstashToken);
230
+ values.set('ZILMATE_PUBLIC_JOB_WEBHOOK_URL', options.publicJobWebhookUrl);
231
+ touchedKeys.add('UPSTASH_QSTASH_TOKEN');
232
+ touchedKeys.add('ZILMATE_PUBLIC_JOB_WEBHOOK_URL');
482
233
  }
483
- else if (await askYesNo(rl, 'Enable Composio trigger-to-job workflows?', values.get('ZILMATE_TRIGGER_WORKFLOWS_ENABLED') === 'true')) {
484
- values.set('ZILMATE_TRIGGER_WORKFLOWS_ENABLED', 'true');
485
- if (values.get('ZILMATE_JOBS_ENABLED') !== 'true') {
486
- console.log(chalk.yellow('Trigger workflows need jobs, so background jobs were enabled.'));
487
- values.set('ZILMATE_JOBS_ENABLED', 'true');
234
+ else if (!options.yes && (!values.has('UPSTASH_QSTASH_TOKEN') || options.force)) {
235
+ if (await askSection(rl, 'Hosted Job Scheduling (QStash)', 'Local schedules stop when the laptop closes. Use Upstash QStash for durable, hosted scheduling and retries.', 'Configure Upstash QStash now?', false)) {
236
+ values.set('UPSTASH_QSTASH_TOKEN', await askRequiredSecret(rl, 'UPSTASH_QSTASH_TOKEN: '));
237
+ values.set('ZILMATE_PUBLIC_JOB_WEBHOOK_URL', await askRequiredSecret(rl, 'ZILMATE_PUBLIC_JOB_WEBHOOK_URL: '));
238
+ touchedKeys.add('UPSTASH_QSTASH_TOKEN');
239
+ touchedKeys.add('ZILMATE_PUBLIC_JOB_WEBHOOK_URL');
488
240
  }
489
241
  }
490
- else {
491
- values.set('ZILMATE_TRIGGER_WORKFLOWS_ENABLED', 'false');
242
+ if (values.get('UPSTASH_QSTASH_TOKEN') && (!values.get('ZILMATE_JOB_WEBHOOK_SECRET') || options.force)) {
243
+ const secret = options.jobWebhookSecret || newWebhookSecret();
244
+ values.set('ZILMATE_JOB_WEBHOOK_SECRET', secret);
245
+ touchedKeys.add('ZILMATE_JOB_WEBHOOK_SECRET');
492
246
  }
493
- }
494
- else if (!values.has('ZILMATE_TRIGGER_WORKFLOWS_ENABLED')) {
495
- values.set('ZILMATE_TRIGGER_WORKFLOWS_ENABLED', 'false');
496
- }
497
- if (!options.yes && options.voiceEnabled === undefined) {
498
- const enableVoice = await askSection(rl, 'Realtime voice', 'Voice mode uses Deepgram Flux V2 for listening/end-of-turn, ZilMate manager/tools for thinking, and Aura-2 for spoken replies. It can be skipped.', 'Enable realtime voice mode?', values.get('ZILMATE_VOICE_ENABLED') === 'true');
499
- values.set('ZILMATE_VOICE_ENABLED', enableVoice ? 'true' : 'false');
500
- if (enableVoice) {
501
- const deepgramKey = options.deepgramApiKey ?? await askOptionalSecret(rl, 'DEEPGRAM_API_KEY (blank to configure later): ');
502
- if (deepgramKey)
503
- values.set('DEEPGRAM_API_KEY', deepgramKey);
504
- const listenModel = (await rl.question(`Listen model (${values.get('ZILMATE_VOICE_LISTEN_MODEL') || 'flux-general-en'}): `)).trim();
505
- if (listenModel) {
506
- values.set('ZILMATE_VOICE_LISTEN_MODEL', listenModel);
507
- values.set('ZILMATE_VOICE_LISTEN_VERSION', listenModel.startsWith('flux-') ? 'v2' : 'v1');
508
- }
509
- else if (!values.get('ZILMATE_VOICE_LISTEN_MODEL')) {
510
- values.set('ZILMATE_VOICE_LISTEN_MODEL', 'flux-general-en');
511
- values.set('ZILMATE_VOICE_LISTEN_VERSION', 'v2');
247
+ const triggersEnabled = options.triggerWorkflowsEnabled !== undefined ? normalizeBooleanOption(options.triggerWorkflowsEnabled) : undefined;
248
+ if (triggersEnabled !== undefined) {
249
+ values.set('ZILMATE_TRIGGER_WORKFLOWS_ENABLED', triggersEnabled ? 'true' : 'false');
250
+ touchedKeys.add('ZILMATE_TRIGGER_WORKFLOWS_ENABLED');
251
+ }
252
+ else if (!options.yes && (!values.has('ZILMATE_TRIGGER_WORKFLOWS_ENABLED') || options.force)) {
253
+ if (await askSection(rl, 'Composio Triggers', 'Automatically start ZilMate jobs when events occur in Slack, Stripe, Gmail, etc.', 'Enable trigger workflows?', true)) {
254
+ values.set('ZILMATE_TRIGGER_WORKFLOWS_ENABLED', 'true');
255
+ touchedKeys.add('ZILMATE_TRIGGER_WORKFLOWS_ENABLED');
512
256
  }
513
- const ttsModel = (await rl.question(`TTS voice (${values.get('ZILMATE_VOICE_TTS_MODEL') || 'aura-2-thalia-en'}): `)).trim();
514
- if (ttsModel)
515
- values.set('ZILMATE_VOICE_TTS_MODEL', ttsModel);
516
- const language = (await rl.question(`Language (${values.get('ZILMATE_VOICE_LANGUAGE') || 'en'}): `)).trim();
517
- if (language)
518
- values.set('ZILMATE_VOICE_LANGUAGE', language);
519
- values.set('ZILMATE_VOICE_MODE', 'agent');
520
- values.set('ZILMATE_VOICE_BARGE_IN', 'true');
521
- const currentInputDevice = values.get('ZILMATE_VOICE_INPUT_DEVICE') || '';
522
- if (await askYesNo(rl, 'Set a terminal microphone input device?', Boolean(currentInputDevice))) {
523
- const voiceInputDevice = (await rl.question(`ZILMATE_VOICE_INPUT_DEVICE${currentInputDevice ? ` (${currentInputDevice})` : ' (blank for default mic)'}: `)).trim();
524
- values.set('ZILMATE_VOICE_INPUT_DEVICE', voiceInputDevice || currentInputDevice);
257
+ else {
258
+ values.set('ZILMATE_TRIGGER_WORKFLOWS_ENABLED', 'false');
259
+ touchedKeys.add('ZILMATE_TRIGGER_WORKFLOWS_ENABLED');
525
260
  }
526
261
  }
527
- else {
528
- console.log(chalk.gray('Voice disabled. Run `zilmate voice setup` or `zilmate voice enable` when you want it.'));
262
+ }
263
+ if (options.deepgramApiKey) {
264
+ values.set('DEEPGRAM_API_KEY', options.deepgramApiKey);
265
+ touchedKeys.add('DEEPGRAM_API_KEY');
266
+ }
267
+ else if (!options.yes && (!values.has('DEEPGRAM_API_KEY') || options.force)) {
268
+ if (await askSection(rl, 'Realtime Voice (Deepgram)', 'Enable ZilMate to listen and speak in realtime with ultra-low latency.', 'Configure Deepgram voice now?', false)) {
269
+ values.set('DEEPGRAM_API_KEY', await askRequiredSecret(rl, 'DEEPGRAM_API_KEY: '));
270
+ touchedKeys.add('DEEPGRAM_API_KEY');
529
271
  }
530
272
  }
531
- else if (!values.has('ZILMATE_VOICE_ENABLED')) {
532
- values.set('ZILMATE_VOICE_ENABLED', 'false');
273
+ if (values.get('DEEPGRAM_API_KEY')) {
274
+ const voiceEnabled = options.voiceEnabled !== undefined ? normalizeBooleanOption(options.voiceEnabled) : undefined;
275
+ if (voiceEnabled !== undefined) {
276
+ values.set('ZILMATE_VOICE_ENABLED', voiceEnabled ? 'true' : 'false');
277
+ touchedKeys.add('ZILMATE_VOICE_ENABLED');
278
+ }
279
+ else if (!options.yes && (!values.has('ZILMATE_VOICE_ENABLED') || options.force)) {
280
+ values.set('ZILMATE_VOICE_ENABLED', (await askYesNo(rl, 'Enable realtime voice by default?', true)) ? 'true' : 'false');
281
+ touchedKeys.add('ZILMATE_VOICE_ENABLED');
282
+ }
533
283
  }
534
- if (!values.get('ZILMATE_SCREENSHOT_MODEL'))
535
- values.set('ZILMATE_SCREENSHOT_MODEL', defaults.ZILMATE_SCREENSHOT_MODEL);
536
- if (!values.has('ZILMATE_CAMERA_DEVICE'))
537
- values.set('ZILMATE_CAMERA_DEVICE', '');
538
- if (!values.has('ZILMATE_FILE_ROOTS'))
539
- values.set('ZILMATE_FILE_ROOTS', '');
540
284
  if (!options.yes && options.fileRoots === undefined) {
541
- if (await askSection(rl, 'Local file tools', 'File tools let ZilMate search, read, summarize, create, move, copy, and rename files inside approved roots. Writes still ask for approval.', 'Configure extra file access roots?', Boolean(values.get('ZILMATE_FILE_ROOTS')))) {
285
+ if (await askSection(rl, 'Local File Access', 'Let ZilMate search, read, summarize, create, move, copy, and rename files inside approved roots. Writes still ask for approval.', 'Configure extra file access roots?', Boolean(values.get('ZILMATE_FILE_ROOTS')))) {
542
286
  const currentRoots = values.get('ZILMATE_FILE_ROOTS') || '';
543
287
  const roots = (await rl.question(`ZILMATE_FILE_ROOTS comma-separated${currentRoots ? ` (${currentRoots})` : ' (blank for current folder only)'}: `)).trim();
544
288
  values.set('ZILMATE_FILE_ROOTS', roots || currentRoots);
289
+ touchedKeys.add('ZILMATE_FILE_ROOTS');
545
290
  }
546
291
  }
547
292
  if (!options.yes && options.screenshotModel === undefined) {
548
293
  if (await askSection(rl, 'Screen and photo understanding', 'ZilMate uses a vision model to describe screenshots and camera photos. The default is Gemini 3.1 Flash Lite through the AI Gateway.', 'Use the default screenshot/photo vision model?', true)) {
549
294
  values.set('ZILMATE_SCREENSHOT_MODEL', values.get('ZILMATE_SCREENSHOT_MODEL') || defaults.ZILMATE_SCREENSHOT_MODEL);
295
+ touchedKeys.add('ZILMATE_SCREENSHOT_MODEL');
550
296
  }
551
297
  else {
552
298
  const model = (await rl.question(`ZILMATE_SCREENSHOT_MODEL (${values.get('ZILMATE_SCREENSHOT_MODEL') || defaults.ZILMATE_SCREENSHOT_MODEL}): `)).trim();
553
299
  values.set('ZILMATE_SCREENSHOT_MODEL', model || values.get('ZILMATE_SCREENSHOT_MODEL') || defaults.ZILMATE_SCREENSHOT_MODEL);
300
+ touchedKeys.add('ZILMATE_SCREENSHOT_MODEL');
301
+ }
302
+ }
303
+ // Chat Integration Section
304
+ if (!options.yes && (!values.has('CHAT_INTEGRATION_ENABLED') || options.force)) {
305
+ if (await askSection(rl, 'Chat Channels (Slack, Telegram, iMessage)', 'Enable ZilMate to respond to you on Slack, Telegram, or iMessage, and proactively report business events.', 'Configure Chat Channels now?', false)) {
306
+ values.set('CHAT_INTEGRATION_ENABLED', 'true');
307
+ touchedKeys.add('CHAT_INTEGRATION_ENABLED');
308
+ if (await askYesNo(rl, 'Configure Slack?', Boolean(values.get('SLACK_BOT_TOKEN')))) {
309
+ const slackToken = options.slackBotToken || await askRequiredSecret(rl, 'SLACK_BOT_TOKEN: ');
310
+ values.set('SLACK_BOT_TOKEN', slackToken);
311
+ touchedKeys.add('SLACK_BOT_TOKEN');
312
+ const slackSecret = options.slackSigningSecret || await askOptionalSecret(rl, 'SLACK_SIGNING_SECRET (optional): ');
313
+ if (slackSecret) {
314
+ values.set('SLACK_SIGNING_SECRET', slackSecret);
315
+ touchedKeys.add('SLACK_SIGNING_SECRET');
316
+ }
317
+ }
318
+ if (await askYesNo(rl, 'Configure Telegram?', Boolean(values.get('TELEGRAM_BOT_TOKEN')))) {
319
+ const tgToken = options.telegramBotToken || await askRequiredSecret(rl, 'TELEGRAM_BOT_TOKEN: ');
320
+ values.set('TELEGRAM_BOT_TOKEN', tgToken);
321
+ touchedKeys.add('TELEGRAM_BOT_TOKEN');
322
+ }
323
+ if (await askYesNo(rl, 'Configure iMessage?', values.get('CHAT_IMESSAGE_ENABLED') === 'true')) {
324
+ values.set('CHAT_IMESSAGE_ENABLED', 'true');
325
+ touchedKeys.add('CHAT_IMESSAGE_ENABLED');
326
+ if (process.platform === 'darwin') {
327
+ values.set('IMESSAGE_LOCAL', (await askYesNo(rl, 'Use local iMessage database (macOS only)?', true)) ? 'true' : 'false');
328
+ touchedKeys.add('IMESSAGE_LOCAL');
329
+ }
330
+ else {
331
+ values.set('IMESSAGE_LOCAL', 'false');
332
+ touchedKeys.add('IMESSAGE_LOCAL');
333
+ console.log(chalk.yellow('Non-macOS platform detected. iMessage will run in Remote Mode via Photon bridge.'));
334
+ }
335
+ }
336
+ else {
337
+ values.set('CHAT_IMESSAGE_ENABLED', 'false');
338
+ touchedKeys.add('CHAT_IMESSAGE_ENABLED');
339
+ }
340
+ }
341
+ else {
342
+ values.set('CHAT_INTEGRATION_ENABLED', 'false');
343
+ touchedKeys.add('CHAT_INTEGRATION_ENABLED');
554
344
  }
555
345
  }
556
346
  const installCameraDeps = options.installCameraDeps === undefined ? undefined : normalizeBooleanOption(options.installCameraDeps);
@@ -575,6 +365,7 @@ export async function runSetup(options = {}) {
575
365
  if (await askYesNo(rl, 'Set a specific camera device now?', Boolean(currentDevice))) {
576
366
  const cameraDevice = (await rl.question(`Camera device${currentDevice ? ` (${currentDevice})` : ' (blank for auto-detect)'}: `)).trim();
577
367
  values.set('ZILMATE_CAMERA_DEVICE', cameraDevice || currentDevice);
368
+ touchedKeys.add('ZILMATE_CAMERA_DEVICE');
578
369
  }
579
370
  const checks = await runCameraDoctor();
580
371
  console.log(chalk.gray(`Camera check: ${checks.map((check) => `${check.name} ${check.status}`).join(', ')}`));
@@ -595,6 +386,7 @@ export async function runSetup(options = {}) {
595
386
  ['Workspace', values.get('ZILMATE_WORKSPACE') || workspaceLayout().root],
596
387
  ['Trigger workflows', values.get('ZILMATE_TRIGGER_WORKFLOWS_ENABLED') === 'true' ? 'enabled' : 'disabled'],
597
388
  ['Voice', values.get('ZILMATE_VOICE_ENABLED') === 'true' ? values.get('DEEPGRAM_API_KEY') ? 'enabled' : 'enabled, missing Deepgram key' : 'disabled'],
389
+ ['Chat', values.get('CHAT_INTEGRATION_ENABLED') === 'true' ? 'enabled' : 'disabled'],
598
390
  ['Camera', await commandExists('ffmpeg') ? 'ready' : 'needs ffmpeg'],
599
391
  ['Tunnel', await commandExists('cloudflared') ? 'ready' : 'needs cloudflared'],
600
392
  ]);
@@ -697,4 +489,59 @@ export async function setVoiceEnabled(enabled, options = {}) {
697
489
  ['Next', enabled ? values.get('DEEPGRAM_API_KEY') ? 'zilmate voice doctor' : 'zilmate voice setup' : 'zilmate voice enable'],
698
490
  ]);
699
491
  }
492
+ export async function runChatSetup(options = {}) {
493
+ const envPath = options.path || '.env';
494
+ const existing = await readEnvValues(envPath);
495
+ const rl = readline.createInterface({ input, output });
496
+ try {
497
+ printZilMateBanner('Chat setup');
498
+ console.log(chalk.gray('Configure Slack, Telegram, and iMessage channels for ZilMate.'));
499
+ const values = new Map(existing);
500
+ values.set('CHAT_INTEGRATION_ENABLED', 'true');
501
+ if (options.slackBotToken) {
502
+ values.set('SLACK_BOT_TOKEN', options.slackBotToken);
503
+ if (options.slackSigningSecret)
504
+ values.set('SLACK_SIGNING_SECRET', options.slackSigningSecret);
505
+ }
506
+ else if (await askYesNo(rl, 'Configure Slack?', Boolean(values.get('SLACK_BOT_TOKEN')))) {
507
+ values.set('SLACK_BOT_TOKEN', await askRequiredSecret(rl, 'SLACK_BOT_TOKEN: '));
508
+ values.set('SLACK_SIGNING_SECRET', await askOptionalSecret(rl, 'SLACK_SIGNING_SECRET: '));
509
+ }
510
+ if (options.telegramBotToken) {
511
+ values.set('TELEGRAM_BOT_TOKEN', options.telegramBotToken);
512
+ }
513
+ else if (await askYesNo(rl, 'Configure Telegram?', Boolean(values.get('TELEGRAM_BOT_TOKEN')))) {
514
+ values.set('TELEGRAM_BOT_TOKEN', await askRequiredSecret(rl, 'TELEGRAM_BOT_TOKEN: '));
515
+ }
516
+ const imessageEnabled = options.imessageEnabled !== undefined ? normalizeBooleanOption(options.imessageEnabled) : undefined;
517
+ if (imessageEnabled !== undefined) {
518
+ values.set('CHAT_IMESSAGE_ENABLED', imessageEnabled ? 'true' : 'false');
519
+ if (options.imessageLocal !== undefined)
520
+ values.set('IMESSAGE_LOCAL', normalizeBooleanOption(options.imessageLocal) ? 'true' : 'false');
521
+ }
522
+ else if (await askYesNo(rl, 'Configure iMessage?', values.get('CHAT_IMESSAGE_ENABLED') === 'true')) {
523
+ values.set('CHAT_IMESSAGE_ENABLED', 'true');
524
+ if (process.platform === 'darwin') {
525
+ values.set('IMESSAGE_LOCAL', (await askYesNo(rl, 'Use local iMessage database (macOS only)?', true)) ? 'true' : 'false');
526
+ }
527
+ else {
528
+ values.set('IMESSAGE_LOCAL', 'false');
529
+ }
530
+ }
531
+ await writeEnvValues(envPath, values, {
532
+ merge: existsSync(envPath),
533
+ touchedKeys: new Set(['CHAT_INTEGRATION_ENABLED', 'SLACK_BOT_TOKEN', 'SLACK_SIGNING_SECRET', 'TELEGRAM_BOT_TOKEN', 'CHAT_IMESSAGE_ENABLED', 'IMESSAGE_LOCAL']),
534
+ });
535
+ console.log(chalk.green(`Saved chat settings to ${envPath}.`));
536
+ printPanel('Chat summary', [
537
+ ['Status', 'enabled'],
538
+ ['Slack', values.get('SLACK_BOT_TOKEN') ? 'configured' : 'missing'],
539
+ ['Telegram', values.get('TELEGRAM_BOT_TOKEN') ? 'configured' : 'missing'],
540
+ ['iMessage', values.get('CHAT_IMESSAGE_ENABLED') === 'true' ? values.get('IMESSAGE_LOCAL') === 'true' ? 'enabled (Local)' : 'enabled (Remote)' : 'disabled'],
541
+ ]);
542
+ }
543
+ finally {
544
+ rl.close();
545
+ }
546
+ }
700
547
  //# sourceMappingURL=setup.js.map