scalix 1.5.0 → 1.5.2

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/headless.js CHANGED
@@ -4,17 +4,18 @@ import { execSync } from 'node:child_process';
4
4
  import { createAgent } from './agent.js';
5
5
  import { defaultTools } from './tools.js';
6
6
  import { extraTools } from './tools-extra.js';
7
+ import { memoryTools } from './tools-memory.js';
7
8
  import { webTools } from './tools-web.js';
8
9
  import { advancedTools } from './tools-advanced.js';
9
10
  import { visionTools } from './tools-vision.js';
10
11
  import { voiceTools } from './tools-voice.js';
11
12
  import { browserTools, closeBrowser } from './tools-browser.js';
12
- import { loadMcpServices } from './mcp.js';
13
+ import { startMcp } from './mcp.js';
13
14
  import { costTracker } from './cost-tracker.js';
14
15
  import { saveSession, loadSession, listSessions, generateSessionId } from './sessions.js';
15
16
  import { logger } from './logger.js';
16
- import { DEFAULT_MODELS, DEFAULT_INSTRUCTIONS, ENV_MODEL, HEADLESS_DEFAULTS, fetchModelSpecs } from './constants.js';
17
- import { loadConfig, resolveActiveProvider, DEFAULT_LIMITS } from './settings.js';
17
+ import { DEFAULT_MODELS, DEFAULT_INSTRUCTIONS, ENV_MODEL, HEADLESS_DEFAULTS, fetchModelSpecs, normalizeModelId } from './constants.js';
18
+ import { loadConfig, loadGlobalConfig, saveConfig, resolveActiveProvider, DEFAULT_LIMITS } from './settings.js';
18
19
  import { checkForUpdate, getUpdateBanner } from './update-checker.js';
19
20
  import { getModeDescription, PERMISSION_MODES, setSandboxRoot } from './sandbox.js';
20
21
  import { collectGitInfo, gitInfoToContext } from './git-context.js';
@@ -155,7 +156,7 @@ async function runSinglePrompt(prompt, output, format, outputSchemaPath, beforeE
155
156
  }).catch(() => { });
156
157
  }
157
158
  const envModels = process.env[ENV_MODEL]
158
- ? process.env[ENV_MODEL].split(',').map((m) => m.trim()).filter(Boolean)
159
+ ? process.env[ENV_MODEL].split(',').map((m) => m.trim()).filter(Boolean).map(normalizeModelId)
159
160
  : [];
160
161
  const selectedModel = envModels.length > 0 ? envModels
161
162
  : config.models.length > 0 ? config.models : DEFAULT_MODELS;
@@ -163,7 +164,6 @@ async function runSinglePrompt(prompt, output, format, outputSchemaPath, beforeE
163
164
  console.error('Error: No models configured.\nSet SCALIX_MODEL env var or run interactive setup: scalix');
164
165
  process.exit(1);
165
166
  }
166
- const mcpTools = loadMcpServices();
167
167
  // Only discover/load project-local plugins in a trusted workspace — plugins
168
168
  // dynamically import and execute code from the project directory.
169
169
  const plugins = isWorkspaceTrusted() ? await discoverPlugins(process.cwd()) : [];
@@ -171,7 +171,7 @@ async function runSinglePrompt(prompt, output, format, outputSchemaPath, beforeE
171
171
  process.stderr.write('Project plugins skipped (untrusted workspace).\n');
172
172
  }
173
173
  const pluginTools = getPluginTools(plugins);
174
- const allTools = [...defaultTools, ...extraTools, ...webTools, ...advancedTools, ...visionTools, ...voiceTools, ...browserTools, ...mcpTools, ...pluginTools];
174
+ const allTools = [...defaultTools, ...extraTools, ...memoryTools, ...webTools, ...advancedTools, ...visionTools, ...voiceTools, ...browserTools, ...pluginTools];
175
175
  const instructions = await buildEnhancedInstructions();
176
176
  let outputSchema;
177
177
  if (outputSchemaPath) {
@@ -194,6 +194,8 @@ async function runSinglePrompt(prompt, output, format, outputSchemaPath, beforeE
194
194
  apiKey,
195
195
  baseUrl,
196
196
  provider: active.provider,
197
+ cloudApiKey: active.cloudApiKey,
198
+ cloudBaseUrl: active.cloudBaseUrl,
197
199
  model: selectedModel,
198
200
  instructions,
199
201
  tools: allTools,
@@ -212,11 +214,18 @@ async function runSinglePrompt(prompt, output, format, outputSchemaPath, beforeE
212
214
  groundingCheck: true,
213
215
  contextRetrieval: true,
214
216
  });
217
+ if (agent.getSkippedHookCount() > 0) {
218
+ process.stderr.write(`Project hooks skipped (untrusted workspace): ${agent.getSkippedHookCount()}\n`);
219
+ }
220
+ // Connect configured MCP servers and register their tools. Warnings only:
221
+ // a server that will not start must not take the whole run down with it.
222
+ const mcpHub = await startMcp(agent, (m) => process.stderr.write(`\x1b[33m⚠ ${m}\x1b[0m\n`));
215
223
  // Register the account's Scalix Cloud platform tools (one attempt; failure
216
224
  // is stated once and the run continues with local tools only).
217
225
  const cloudStatus = await agent.refreshCloudTools();
218
226
  if (cloudStatus.state === 'unavailable') {
219
- process.stderr.write(`\x1b[33m☁ Scalix Cloud unavailable — running with local tools only. ${cloudStatus.error ?? ''}\x1b[0m\n`);
227
+ const remedy = cloudStatus.remedy ? ` ${cloudStatus.remedy}` : '';
228
+ process.stderr.write(`\x1b[33m☁ Scalix Cloud unavailable — running with local tools only. ${cloudStatus.error ?? ''}${remedy}\x1b[0m\n`);
220
229
  }
221
230
  let fullResponse = '';
222
231
  agent.on('tool:call', (name) => process.stderr.write(`\x1b[33m▶ ${name}\x1b[0m\n`));
@@ -268,6 +277,7 @@ async function runSinglePrompt(prompt, output, format, outputSchemaPath, beforeE
268
277
  process.stdout.write(formatted + '\n');
269
278
  }
270
279
  await closeBrowser();
280
+ await mcpHub.close(); // stdio servers are child processes; don't orphan them
271
281
  if (beforeExit)
272
282
  await beforeExit();
273
283
  process.exit(0);
@@ -319,14 +329,13 @@ async function runInteractive() {
319
329
  if (active.provider === 'scalix')
320
330
  fetchModelSpecs(baseUrl, apiKey).catch(() => { });
321
331
  const envModels = process.env[ENV_MODEL]
322
- ? process.env[ENV_MODEL].split(',').map((m) => m.trim()).filter(Boolean)
332
+ ? process.env[ENV_MODEL].split(',').map((m) => m.trim()).filter(Boolean).map(normalizeModelId)
323
333
  : [];
324
334
  const selectedModel = envModels.length > 0 ? envModels
325
335
  : config.models.length > 0 ? config.models : DEFAULT_MODELS;
326
336
  if (selectedModel.length === 0) {
327
337
  throw new Error('No models configured. Set SCALIX_MODEL env var or run interactive setup.');
328
338
  }
329
- const mcpTools = loadMcpServices();
330
339
  // Only discover/load project-local plugins in a trusted workspace — plugins
331
340
  // dynamically import and execute code from the project directory.
332
341
  const plugins = isWorkspaceTrusted() ? await discoverPlugins(process.cwd()) : [];
@@ -334,7 +343,7 @@ async function runInteractive() {
334
343
  process.stderr.write('Project plugins skipped (untrusted workspace).\n');
335
344
  }
336
345
  const pluginTools = getPluginTools(plugins);
337
- const allTools = [...defaultTools, ...extraTools, ...webTools, ...advancedTools, ...visionTools, ...voiceTools, ...browserTools, ...mcpTools, ...pluginTools];
346
+ const allTools = [...defaultTools, ...extraTools, ...memoryTools, ...webTools, ...advancedTools, ...visionTools, ...voiceTools, ...browserTools, ...pluginTools];
338
347
  const instructions = await buildEnhancedInstructions();
339
348
  const rl = readline.createInterface({
340
349
  input: process.stdin,
@@ -355,6 +364,8 @@ async function runInteractive() {
355
364
  apiKey,
356
365
  baseUrl,
357
366
  provider: active.provider,
367
+ cloudApiKey: active.cloudApiKey,
368
+ cloudBaseUrl: active.cloudBaseUrl,
358
369
  model: selectedModel,
359
370
  instructions,
360
371
  tools: allTools,
@@ -378,6 +389,9 @@ async function runInteractive() {
378
389
  return answer.trim().toLowerCase() === 'y';
379
390
  },
380
391
  });
392
+ if (agent.getSkippedHookCount() > 0) {
393
+ process.stderr.write(`Project hooks skipped (untrusted workspace): ${agent.getSkippedHookCount()}\n`);
394
+ }
381
395
  let sessionId = generateSessionId();
382
396
  agent.on('thinking:start', () => process.stdout.write('\n\x1b[33mThinking...\x1b[0m'));
383
397
  agent.on('tool:call', (name, args) => {
@@ -408,11 +422,15 @@ async function runInteractive() {
408
422
  console.log(`\x1b[33m\u26A0 Context usage: ${info.percentage}% (${info.tokens}/${info.maxTokens} tokens)\x1b[0m`);
409
423
  }
410
424
  });
425
+ // Connect configured MCP servers and register their tools. Warnings only:
426
+ // a server that will not start must not take the whole run down with it.
427
+ const mcpHub = await startMcp(agent, (m) => process.stderr.write(`\x1b[33m⚠ ${m}\x1b[0m\n`));
411
428
  // Register the account's Scalix Cloud platform tools (one attempt; failure
412
429
  // is stated once and the session continues with local tools only).
413
430
  const cloudStatus = await agent.refreshCloudTools();
414
431
  if (cloudStatus.state === 'unavailable') {
415
- console.log(`\x1b[33m☁ Scalix Cloud unavailable — running with local tools only. ${cloudStatus.error ?? ''}\x1b[0m`);
432
+ const remedy = cloudStatus.remedy ? ` ${cloudStatus.remedy}` : '';
433
+ console.log(`\x1b[33m☁ Scalix Cloud unavailable — running with local tools only. ${cloudStatus.error ?? ''}${remedy}\x1b[0m`);
416
434
  }
417
435
  const toolCount = allTools.length + cloudStatus.toolCount;
418
436
  const modelDisplay = selectedModel.map(shortModel).join(' | ');
@@ -439,6 +457,7 @@ async function runInteractive() {
439
457
  console.log('Goodbye!');
440
458
  rl.close();
441
459
  await closeBrowser().catch(() => { });
460
+ await mcpHub.close().catch(() => { }); // stdio servers are our children
442
461
  process.exit(0);
443
462
  }
444
463
  process.on('SIGINT', () => { shutdown().catch(() => process.exit(1)); });
@@ -502,8 +521,16 @@ async function runInteractive() {
502
521
  console.log(`\nInvalid model name: "${name}"\n`);
503
522
  return true;
504
523
  }
505
- agent.setModel(name);
506
- console.log(`\nSwitched to ${shortModel(name)}\n`);
524
+ const chosen = normalizeModelId(name);
525
+ agent.setModel(chosen);
526
+ // Persist like the TUI does — an in-memory-only switch silently
527
+ // reverted on the next launch. Global config only (no project-override
528
+ // laundering), chain-preserving (chosen model to the front).
529
+ loadGlobalConfig().then((cfg) => {
530
+ cfg.models = [chosen, ...cfg.models.filter((m) => m !== chosen)];
531
+ return saveConfig(cfg);
532
+ }).catch(() => { });
533
+ console.log(`\nSwitched to ${shortModel(chosen)}${process.env[ENV_MODEL] ? '\n(note: SCALIX_MODEL is set and will override this on next launch)' : ''}\n`);
507
534
  return true;
508
535
  }
509
536
  if (cmd === '/compact') {
@@ -596,6 +623,10 @@ async function runInteractive() {
596
623
  return true;
597
624
  }
598
625
  if (cmd === '/hooks') {
626
+ const skipped = agent.getSkippedHookCount();
627
+ if (skipped > 0) {
628
+ console.log(`\n${skipped} project hook(s) skipped — untrusted workspace.`);
629
+ }
599
630
  console.log('\n' + hooksSummary(agent.getHooks()) + '\n');
600
631
  return true;
601
632
  }