scalix 1.0.0 → 1.1.0

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
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useState, useEffect, useCallback, useRef, useMemo, memo } from 'react';
3
- import { render, Box, Text, Spacer, useInput, useApp, useStdout } from 'ink';
3
+ import { render, Box, Text, useInput, useApp, useStdout } from 'ink';
4
4
  import Spinner from 'ink-spinner';
5
5
  import TextInput from 'ink-text-input';
6
6
  import Gradient from 'ink-gradient';
@@ -17,13 +17,14 @@ import { webTools } from './tools-web.js';
17
17
  import { advancedTools } from './tools-advanced.js';
18
18
  import { visionTools } from './tools-vision.js';
19
19
  import { voiceTools } from './tools-voice.js';
20
- import { browserTools } from './tools-browser.js';
20
+ import { browserTools, closeBrowser } from './tools-browser.js';
21
21
  import { loadMcpServices } from './mcp.js';
22
22
  import { renderMarkdown } from './markdown.js';
23
23
  import { costTracker } from './cost-tracker.js';
24
24
  import { saveSession, listSessions, loadSession, generateSessionId } from './sessions.js';
25
25
  import { logger } from './logger.js';
26
26
  import { VERSION, PRODUCT_NAME, DEFAULT_MODELS, DEFAULT_BASE_URL, DEFAULT_INSTRUCTIONS, ENV_API_KEY, ENV_MODEL, TOOL_DEFAULTS, HEADLESS_DEFAULTS, fetchModelSpecs } from './constants.js';
27
+ import { checkForUpdate, getUpdateBanner, performUpdate } from './update-checker.js';
27
28
  import { loadConfig, saveConfig, isConfigured, DEFAULT_LIMITS, THINKING_MODES } from './settings.js';
28
29
  import { getModeDescription, PERMISSION_MODES, setSandboxRoot } from './sandbox.js';
29
30
  import { buildProjectIndex, indexToContext } from './project-index.js';
@@ -36,12 +37,13 @@ import { hooksSummary } from './hooks.js';
36
37
  import { spawnSubAgent, pickCheapestModel } from './sub-agent.js';
37
38
  import { createCheckpoint, undoLastCheckpoint, listCheckpoints } from './git-undo.js';
38
39
  import { loadScopedRules, rulesToContext } from './scoped-rules.js';
40
+ import { AgentRegistry } from './agent-registry.js';
39
41
  logger.setLevel('silent');
40
42
  setSandboxRoot(process.cwd());
41
43
  // Suppress noisy React 19 development-mode reconciler warnings that leak to stderr
42
44
  const _origConsoleError = console.error;
43
45
  console.error = (...args) => {
44
- if (typeof args[0] === 'string' && args[0] === 'Warning: Encountered two children with the same key, `%s`. Keys should be unique so that components maintain their identity across updates. Non-unique keys may cause children to be duplicated and/or omitted — the behavior is unsupported and could change in a future version.')
46
+ if (typeof args[0] === 'string' && args[0].startsWith('Warning:') && args[0].includes('same key'))
45
47
  return;
46
48
  _origConsoleError(...args);
47
49
  };
@@ -71,7 +73,7 @@ async function buildEnhancedInstructions() {
71
73
  let instructions = baseInstructions;
72
74
  const cwd = process.cwd();
73
75
  try {
74
- const gitInfo = collectGitInfo(cwd);
76
+ const gitInfo = await collectGitInfo(cwd);
75
77
  if (gitInfo.isRepo)
76
78
  instructions += '\n\n' + gitInfoToContext(gitInfo);
77
79
  }
@@ -151,7 +153,7 @@ function initAgent(config) {
151
153
  });
152
154
  loadCustomCommands(process.cwd()).then((cmds) => {
153
155
  customCommands = cmds;
154
- }).catch(() => { });
156
+ }).catch((err) => { logger.warn('Failed to load custom commands', { error: err.message }); });
155
157
  return agent;
156
158
  }
157
159
  // ─── Helpers ───
@@ -300,7 +302,7 @@ function SetupScreen({ onComplete, existingConfig }) {
300
302
  if (result.length > 0) {
301
303
  setFetchedModels(result.map((m) => ({ id: m.id, name: m.name })));
302
304
  }
303
- }).catch(() => { }).finally(() => {
305
+ }).catch((err) => { logger.warn('Failed to fetch model specs', { error: err.message }); }).finally(() => {
304
306
  setFetchingModels(false);
305
307
  setStep('models');
306
308
  });
@@ -375,6 +377,8 @@ function SetupScreen({ onComplete, existingConfig }) {
375
377
  limits,
376
378
  permissionMode: existingConfig?.permissionMode || 'default',
377
379
  thinkingMode: existingConfig?.thinkingMode || 'off',
380
+ security: existingConfig?.security || {},
381
+ injectionDetection: existingConfig?.injectionDetection || { enabled: true, sensitivity: 'medium' },
378
382
  };
379
383
  saveConfig(newConfig).then(() => {
380
384
  onComplete(newConfig);
@@ -398,11 +402,14 @@ const StatusBar = memo(function StatusBar({ status, model, toolCount, thinkStart
398
402
  const modeColor = mode === 'yolo' ? 'red' : mode === 'auto' ? 'yellow' : mode === 'plan' ? 'blue' : 'green';
399
403
  const statusIcon = status === 'thinking' ? '\u25D4' : status === 'error' ? '\u2718' : '\u25CF';
400
404
  const statusColor = status === 'thinking' ? 'yellow' : status === 'error' ? 'red' : 'green';
405
+ const activeAgents = AgentRegistry.getInstance().listActive().length;
401
406
  const leftParts = [];
402
407
  leftParts.push(shortModel(model));
403
408
  if (tokens > 0)
404
409
  leftParts.push(`${formatTokens(tokens)} tok`);
405
410
  leftParts.push(`${toolCount} tools`);
411
+ if (activeAgents > 0)
412
+ leftParts.push(`${activeAgents} agent${activeAgents > 1 ? 's' : ''}`);
406
413
  const leftText = leftParts.join(' \u00B7 ');
407
414
  const rightLen = mode.length + VERSION.length + 6 + (elapsedStr ? elapsedStr.length + 3 : 0);
408
415
  const gap = Math.max(cols - leftText.length - rightLen - 8, 2);
@@ -510,6 +517,7 @@ const StreamingArea = memo(function StreamingArea({ text, reasoning, model, maxL
510
517
  return (_jsxs(Box, { flexDirection: "column", paddingLeft: 3, paddingRight: 2, marginTop: 1, children: [displayReasoning.length > 0 && (_jsx(Box, { paddingLeft: 0, marginBottom: 0, children: _jsxs(Text, { color: "magenta", dimColor: true, italic: true, wrap: "wrap", children: ['\u2502', " ", displayReasoning] }) })), displayText.length > 0 ? (_jsxs(Text, { wrap: "wrap", children: [renderMarkdown(displayText), _jsx(Text, { color: "magentaBright", children: '\u258E' })] })) : displayReasoning.length === 0 ? (_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: "magentaBright", children: _jsx(Spinner, { type: "dots" }) }), _jsxs(Text, { dimColor: true, children: ["Thinking", model ? ` (${model})` : '', "..."] })] })) : null] }));
511
518
  });
512
519
  function ConfirmPrompt({ toolName, args, onRespond, onScrollUp, onScrollDown }) {
520
+ const { cols } = useTermSize();
513
521
  useInput((ch, key) => {
514
522
  if (ch.toLowerCase() === 'y')
515
523
  onRespond(true);
@@ -520,8 +528,26 @@ function ConfirmPrompt({ toolName, args, onRespond, onScrollUp, onScrollDown })
520
528
  else if (key.pageDown || (key.downArrow && key.shift))
521
529
  onScrollDown?.();
522
530
  });
523
- const preview = typeof args === 'string' ? args : JSON.stringify(args, null, 2);
524
- return (_jsxs(Box, { flexDirection: "column", paddingX: 1, marginX: 0, paddingY: 0, children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: "yellow", children: '\u25CB' }), _jsx(Text, { color: "yellow", bold: true, children: toolName }), _jsx(Text, { dimColor: true, children: truncate(preview, 80) })] }), _jsxs(Box, { gap: 2, paddingLeft: 3, children: [_jsx(Text, { color: "green", bold: true, children: "(y)" }), _jsx(Text, { dimColor: true, children: "allow" }), _jsx(Text, { color: "red", bold: true, children: "(n)" }), _jsx(Text, { dimColor: true, children: "deny" })] })] }));
531
+ let parsed = {};
532
+ try {
533
+ parsed = typeof args === 'string' ? JSON.parse(args) : args ?? {};
534
+ }
535
+ catch {
536
+ parsed = {};
537
+ }
538
+ const isCommand = toolName === 'run_command';
539
+ const isWrite = toolName === 'write_file' || toolName === 'edit_file' || toolName === 'notebook_edit';
540
+ const isGit = toolName === 'git_commit';
541
+ const highlight = parsed.command ?? parsed.filePath ?? parsed.file_path ?? parsed.notebook_path ?? parsed.message ?? null;
542
+ const highlightLabel = isCommand ? 'Command' : isGit ? 'Message' : isWrite ? 'File' : 'Args';
543
+ const highlightStr = highlight ? String(highlight) : null;
544
+ const extraFields = Object.entries(parsed).filter(([k]) => !['command', 'filePath', 'file_path', 'notebook_path', 'message', 'content'].includes(k));
545
+ const boxWidth = Math.min(cols - 2, 80);
546
+ const border = '\u2500'.repeat(boxWidth - 2);
547
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsxs(Text, { color: "yellow", children: ['\u250C', border, '\u2510'] }), _jsxs(Text, { color: "yellow", children: ['\u2502', " ", _jsxs(Text, { color: "yellow", bold: true, children: ['\u26A0', " Permission Required"] }), _jsx(Text, { children: ' '.repeat(Math.max(0, boxWidth - 24)) }), '\u2502'] }), _jsxs(Text, { color: "yellow", children: ['\u2502', " ", _jsx(Text, { dimColor: true, children: "Tool:" }), " ", _jsx(Text, { color: "cyan", bold: true, children: toolName }), _jsx(Text, { children: ' '.repeat(Math.max(0, boxWidth - 9 - toolName.length)) }), '\u2502'] }), highlightStr && (_jsxs(Text, { color: "yellow", children: ['\u2502', " ", _jsxs(Text, { dimColor: true, children: [highlightLabel, ":"] }), " ", _jsx(Text, { color: "white", bold: true, children: truncate(highlightStr, boxWidth - highlightLabel.length - 6) }), _jsx(Text, { children: ' '.repeat(Math.max(0, boxWidth - highlightLabel.length - 4 - Math.min(highlightStr.length, boxWidth - highlightLabel.length - 6))) }), '\u2502'] })), extraFields.slice(0, 3).map(([k, v]) => {
548
+ const val = truncate(String(v), boxWidth - k.length - 6);
549
+ return (_jsxs(Text, { color: "yellow", children: ['\u2502', " ", _jsxs(Text, { dimColor: true, children: [k, ":"] }), " ", _jsx(Text, { children: val }), _jsx(Text, { children: ' '.repeat(Math.max(0, boxWidth - k.length - 4 - val.length)) }), '\u2502'] }, k));
550
+ }), _jsxs(Text, { color: "yellow", children: ['\u251C', border, '\u2524'] }), _jsxs(Text, { color: "yellow", children: ['\u2502', " ", _jsx(Text, { color: "green", bold: true, children: "[Y]" }), _jsx(Text, { children: " Allow " }), _jsx(Text, { color: "red", bold: true, children: "[N]" }), _jsx(Text, { children: " Deny " }), _jsx(Text, { dimColor: true, children: "ESC to deny" }), _jsx(Text, { children: ' '.repeat(Math.max(0, boxWidth - 33)) }), '\u2502'] }), _jsxs(Text, { color: "yellow", children: ['\u2514', border, '\u2518'] })] }));
525
551
  }
526
552
  function ModelPicker({ currentModels, onSelect, onCancel }) {
527
553
  const [customInput, setCustomInput] = useState('');
@@ -607,7 +633,7 @@ function SettingsEditor({ onSave, onClose }) {
607
633
  const [selectedKey, setSelectedKey] = useState(null);
608
634
  const [editValue, setEditValue] = useState('');
609
635
  const [currentConfig, setCurrentConfig] = useState(null);
610
- const reload = useCallback(() => { loadConfig().then(setCurrentConfig).catch(() => { }); }, []);
636
+ const reload = useCallback(() => { loadConfig().then(setCurrentConfig).catch((err) => { logger.warn('Failed to load config', { error: err.message }); }); }, []);
611
637
  useEffect(() => { reload(); }, [reload]);
612
638
  useInput((_ch, key) => {
613
639
  if (key.escape) {
@@ -898,6 +924,7 @@ const InputSection = memo(function InputSection({ onSubmit, disabled, onAbort, o
898
924
  { cmd: '/hooks', desc: 'Show lifecycle hooks' },
899
925
  { cmd: '/commands', desc: 'Show custom commands' },
900
926
  { cmd: '/delegate', desc: 'Run task with sub-agent' },
927
+ { cmd: '/agents', desc: 'Show active agents & usage' },
901
928
  { cmd: '/checkpoint', desc: 'Create git checkpoint' },
902
929
  { cmd: '/undo', desc: 'Undo last checkpoint' },
903
930
  { cmd: '/checkpoints', desc: 'List recent checkpoints' },
@@ -936,7 +963,7 @@ const InputSection = memo(function InputSection({ onSubmit, disabled, onAbort, o
936
963
  onChange('');
937
964
  handleSubmit(item.value);
938
965
  }
939
- }, limit: 10 }), _jsxs(Text, { dimColor: true, italic: true, children: ['↑↓', " navigate ", '•', " Enter select ", '•', " keep typing to filter"] })] })), stash.length > 0 && !disabled && (_jsxs(Box, { paddingX: 3, children: [_jsxs(Text, { color: "magenta", dimColor: true, children: ['⚑', " Stashed: ", truncate(stash, 40), " "] }), _jsx(Text, { dimColor: true, children: "(Ctrl+S to restore)" })] })), _jsx(Box, { paddingX: 2, children: disabled ? (_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: "magentaBright", children: _jsx(Spinner, { type: "dots" }) }), _jsx(Text, { dimColor: true, children: "Working..." }), _jsx(Spacer, {}), _jsx(Text, { dimColor: true, children: "^C interrupt" })] })) : (_jsxs(Box, { flexGrow: 1, children: [_jsx(Box, { flexShrink: 0, width: 2, children: _jsx(Text, { color: "magentaBright", bold: true, children: '❯' }) }), _jsx(Box, { flexGrow: 1, children: _jsx(TextInput, { value: value, onChange: onChange, onSubmit: handleSubmit, placeholder: "Type a message...", showCursor: true, focus: !disabled && !pickerActive }) })] })) })] }));
966
+ }, limit: 10 }), _jsxs(Text, { dimColor: true, italic: true, children: ['↑↓', " navigate ", '•', " Enter select ", '•', " keep typing to filter"] })] })), stash.length > 0 && !disabled && (_jsxs(Box, { paddingX: 3, children: [_jsxs(Text, { color: "magenta", dimColor: true, children: ['⚑', " Stashed: ", truncate(stash, 40), " "] }), _jsx(Text, { dimColor: true, children: "(Ctrl+S to restore)" })] })), _jsx(Box, { paddingX: 2, children: _jsxs(Box, { flexGrow: 1, children: [_jsx(Box, { flexShrink: 0, width: 2, children: disabled ? (_jsx(Text, { color: "magentaBright", children: _jsx(Spinner, { type: "dots" }) })) : (_jsx(Text, { color: "magentaBright", bold: true, children: '❯' })) }), _jsx(Box, { flexGrow: 1, children: _jsx(TextInput, { value: value, onChange: onChange, onSubmit: handleSubmit, placeholder: disabled ? 'Queue a message... (sent after current task)' : 'Type a message...', showCursor: true, focus: !pickerActive }) }), disabled && (_jsx(Box, { flexShrink: 0, children: _jsx(Text, { dimColor: true, children: " ^C interrupt" }) }))] }) })] }));
940
967
  });
941
968
  // ─── App ───
942
969
  function estimateLines(item, cols) {
@@ -1031,6 +1058,7 @@ function App({ config }) {
1031
1058
  const [currentMode, setCurrentMode] = useState(agent?.getPermissionMode() ?? 'default');
1032
1059
  const [selectingMode, setSelectingMode] = useState(false);
1033
1060
  const [editingSettings, setEditingSettings] = useState(false);
1061
+ const [agentVersion, setAgentVersion] = useState(0);
1034
1062
  const counter = useRef(0);
1035
1063
  const showBannerRef = useRef(true);
1036
1064
  const modelsRef = useRef(models);
@@ -1235,7 +1263,7 @@ function App({ config }) {
1235
1263
  currentAgent.off('context:compress', onContextCompress);
1236
1264
  currentAgent.off('context:usage', onContextUsage);
1237
1265
  };
1238
- }, [add]);
1266
+ }, [add, agentVersion]);
1239
1267
  // Confirmation bridge (event-driven)
1240
1268
  useEffect(() => {
1241
1269
  const onPending = () => {
@@ -1246,6 +1274,11 @@ function App({ config }) {
1246
1274
  confirmEmitter.on('pending', onPending);
1247
1275
  return () => { confirmEmitter.off('pending', onPending); };
1248
1276
  }, [confirmInfo]);
1277
+ useEffect(() => {
1278
+ const banner = getUpdateBanner();
1279
+ if (banner)
1280
+ add('system', banner);
1281
+ }, [add]);
1249
1282
  const handleConfirm = useCallback((ok) => {
1250
1283
  if (pendingConfirm) {
1251
1284
  pendingConfirm.resolve(ok);
@@ -1295,10 +1328,12 @@ function App({ config }) {
1295
1328
  ' /watch <cmd> \u2014 run command, auto-fix on failure',
1296
1329
  ' /create <d> \u2014 scaffold a new project',
1297
1330
  ' /delegate <t> \u2014 run task with sub-agent',
1331
+ ' /agents \u2014 show active agents & usage',
1298
1332
  '',
1299
1333
  'Tools & Config:',
1300
1334
  ' /copy \u2014 copy last response to clipboard',
1301
1335
  ' /status \u2014 show version, model & connection info',
1336
+ ' /update \u2014 check for and install updates',
1302
1337
  ' /doctor \u2014 diagnose installation',
1303
1338
  ' /settings \u2014 view & configure limits',
1304
1339
  ' /setup \u2014 reconfigure API key, URL & models',
@@ -1331,7 +1366,7 @@ function App({ config }) {
1331
1366
  if (msgs.length > 0) {
1332
1367
  saveSession(sessionId, msgs, models[0] || 'unknown', undefined, costTracker.getState()).then((meta) => {
1333
1368
  add('system', `Auto-saved: ${meta.name}`);
1334
- }).catch(() => { });
1369
+ }).catch((err) => { logger.warn('Failed to auto-save session', { error: err.message }); });
1335
1370
  }
1336
1371
  agent.clearHistory();
1337
1372
  setItems([]);
@@ -1449,7 +1484,7 @@ function App({ config }) {
1449
1484
  loadConfig().then(cfg => {
1450
1485
  cfg.permissionMode = arg;
1451
1486
  saveConfig(cfg);
1452
- }).catch(() => { });
1487
+ }).catch((err) => { logger.warn('Failed to save config', { error: err.message }); });
1453
1488
  add('system', `Mode: ${arg.toUpperCase()} \u2014 ${getModeDescription(arg)}`);
1454
1489
  }
1455
1490
  else {
@@ -1464,7 +1499,7 @@ function App({ config }) {
1464
1499
  loadConfig().then(cfg => {
1465
1500
  cfg.thinkingMode = arg;
1466
1501
  saveConfig(cfg);
1467
- }).catch(() => { });
1502
+ }).catch((err) => { logger.warn('Failed to save config', { error: err.message }); });
1468
1503
  add('system', `Thinking: ${arg.toUpperCase()}${arg === 'off' ? '' : ' \u2014 model will reason before responding'}`);
1469
1504
  }
1470
1505
  else {
@@ -1510,14 +1545,15 @@ function App({ config }) {
1510
1545
  return true;
1511
1546
  }
1512
1547
  if (cmd === '/git') {
1513
- const gitInfo = collectGitInfo(process.cwd());
1514
- if (!gitInfo.isRepo) {
1515
- add('system', 'Not a git repository.');
1516
- }
1517
- else {
1518
- const lines = gitInfoToContext(gitInfo).split('\n');
1519
- lines.forEach((l) => add('system', l));
1520
- }
1548
+ collectGitInfo(process.cwd()).then((gitInfo) => {
1549
+ if (!gitInfo.isRepo) {
1550
+ add('system', 'Not a git repository.');
1551
+ }
1552
+ else {
1553
+ const lines = gitInfoToContext(gitInfo).split('\n');
1554
+ lines.forEach((l) => add('system', l));
1555
+ }
1556
+ }).catch(() => { add('system', 'Failed to collect git info.'); });
1521
1557
  return true;
1522
1558
  }
1523
1559
  if (cmd === '/plugins') {
@@ -1597,6 +1633,23 @@ function App({ config }) {
1597
1633
  });
1598
1634
  return true;
1599
1635
  }
1636
+ if (cmd === '/agents') {
1637
+ const registry = AgentRegistry.getInstance();
1638
+ const summary = registry.getUsageSummary();
1639
+ const tree = registry.getAgentTree();
1640
+ const lines = [
1641
+ `Active: ${summary.activeCount} | Completed: ${summary.completedCount} | Failed: ${summary.failedCount}`,
1642
+ `Total tokens: ${summary.totalTokens.toLocaleString()}`,
1643
+ ];
1644
+ if (tree && tree !== '(no agents)') {
1645
+ lines.push('', 'Agent tree:', tree);
1646
+ }
1647
+ else {
1648
+ lines.push('', 'No agents have been spawned yet.');
1649
+ }
1650
+ lines.forEach(l => add('system', l));
1651
+ return true;
1652
+ }
1600
1653
  if (cmd === '/checkpoint') {
1601
1654
  const label = text.slice(12).trim() || undefined;
1602
1655
  createCheckpoint(process.cwd(), label).then((hash) => {
@@ -1871,6 +1924,32 @@ function App({ config }) {
1871
1924
  add('system', `Messages: ${msgCount}`);
1872
1925
  add('system', `Session: ${sessionId}`);
1873
1926
  add('system', `CWD: ${process.cwd()}`);
1927
+ const updateBanner = getUpdateBanner();
1928
+ if (updateBanner)
1929
+ add('system', updateBanner);
1930
+ return true;
1931
+ }
1932
+ if (cmd === '/update') {
1933
+ add('system', 'Checking for updates...');
1934
+ checkForUpdate().then((info) => {
1935
+ if (info && info.updateAvailable) {
1936
+ add('system', `New version available: ${info.current} → ${info.latest}`);
1937
+ add('system', 'Installing update...');
1938
+ performUpdate().then(({ success, output }) => {
1939
+ if (success) {
1940
+ add('system', `Updated successfully. Restart Scalix to use v${info.latest}.`);
1941
+ }
1942
+ else {
1943
+ add('system', `Update failed: ${output}\nTry manually: npm install -g scalix@latest`);
1944
+ }
1945
+ });
1946
+ }
1947
+ else {
1948
+ add('system', `Scalix v${VERSION} is up to date.`);
1949
+ }
1950
+ }).catch(() => {
1951
+ add('system', 'Failed to check for updates. Try: npm install -g scalix@latest');
1952
+ });
1874
1953
  return true;
1875
1954
  }
1876
1955
  if (cmd === '/context') {
@@ -1919,7 +1998,7 @@ function App({ config }) {
1919
1998
  if (cmd === '/exit' || cmd === '/quit') {
1920
1999
  const msgs = agent.getMessages();
1921
2000
  if (msgs.length > 0) {
1922
- saveSession(sessionId, msgs, models[0] || 'unknown', undefined, costTracker.getState()).catch(() => { }).finally(() => process.exit(0));
2001
+ saveSession(sessionId, msgs, models[0] || 'unknown', undefined, costTracker.getState()).catch((err) => { logger.warn('Failed to save session on exit', { error: err.message }); }).finally(() => process.exit(0));
1923
2002
  }
1924
2003
  else {
1925
2004
  process.exit(0);
@@ -1932,7 +2011,7 @@ function App({ config }) {
1932
2011
  loadConfig().then(cfg => {
1933
2012
  cfg.permissionMode = 'plan';
1934
2013
  saveConfig(cfg);
1935
- }).catch(() => { });
2014
+ }).catch((err) => { logger.warn('Failed to save config', { error: err.message }); });
1936
2015
  add('system', `Mode: PLAN — ${getModeDescription('plan')}`);
1937
2016
  return true;
1938
2017
  }
@@ -2016,10 +2095,22 @@ function App({ config }) {
2016
2095
  const isLoadingRef = useRef(false);
2017
2096
  useEffect(() => { isLoadingRef.current = isLoading; }, [isLoading]);
2018
2097
  const multilineRef = useRef([]);
2098
+ const messageQueueRef = useRef([]);
2019
2099
  const handleSubmit = useCallback(async (text) => {
2020
2100
  const trimmed = text.trim();
2021
- if (!trimmed || isLoadingRef.current)
2101
+ if (!trimmed)
2102
+ return;
2103
+ // Allow /mode even while the agent is working
2104
+ if (isLoadingRef.current && trimmed.startsWith('/mode')) {
2105
+ handleCommand(trimmed);
2106
+ return;
2107
+ }
2108
+ // Queue messages while agent is working
2109
+ if (isLoadingRef.current) {
2110
+ messageQueueRef.current.push(trimmed);
2111
+ add('system', `Queued (${messageQueueRef.current.length}): ${truncate(trimmed, 60)}`);
2022
2112
  return;
2113
+ }
2023
2114
  if (trimmed.endsWith('\\')) {
2024
2115
  if (multilineRef.current.length >= 50) {
2025
2116
  add('system', 'Multiline limit reached (50 lines). Sending now.');
@@ -2062,7 +2153,7 @@ function App({ config }) {
2062
2153
  }
2063
2154
  const correction = detectCorrection(finalText);
2064
2155
  if (correction) {
2065
- addNote(process.cwd(), `[correction] ${correction}`).catch(() => { });
2156
+ addNote(process.cwd(), `[correction] ${correction}`).catch((err) => { logger.warn('Failed to save correction note', { error: err.message }); });
2066
2157
  }
2067
2158
  add('user', finalText);
2068
2159
  setIsLoading(true);
@@ -2078,6 +2169,24 @@ function App({ config }) {
2078
2169
  add('error', e.message || 'Unknown error');
2079
2170
  }
2080
2171
  finally {
2172
+ // Process queued messages sequentially
2173
+ while (messageQueueRef.current.length > 0) {
2174
+ const next = messageQueueRef.current.shift();
2175
+ const remaining = messageQueueRef.current.length;
2176
+ if (remaining > 0)
2177
+ add('system', `${remaining} more queued`);
2178
+ add('user', next);
2179
+ setStatus('thinking');
2180
+ setThinkStart(Date.now());
2181
+ try {
2182
+ const qResult = await agent.send(next);
2183
+ if (!qResult || !qResult.trim())
2184
+ add('system', 'Agent returned no response.');
2185
+ }
2186
+ catch (e) {
2187
+ add('error', e.message || 'Unknown error');
2188
+ }
2189
+ }
2081
2190
  setIsLoading(false);
2082
2191
  setStatus('ready');
2083
2192
  setThinkStart(null);
@@ -2087,6 +2196,7 @@ function App({ config }) {
2087
2196
  setSessionList(null);
2088
2197
  setIsLoading(true);
2089
2198
  setStatus('thinking');
2199
+ setThinkStart(Date.now());
2090
2200
  loadSession(session.id).then((data) => {
2091
2201
  agent.loadMessages(data.messages);
2092
2202
  sessionId = session.id;
@@ -2112,6 +2222,7 @@ function App({ config }) {
2112
2222
  }).finally(() => {
2113
2223
  setIsLoading(false);
2114
2224
  setStatus('ready');
2225
+ setThinkStart(null);
2115
2226
  });
2116
2227
  }, [add]);
2117
2228
  const handleAbort = useCallback(() => {
@@ -2131,7 +2242,7 @@ function App({ config }) {
2131
2242
  const handleClearScreen = useCallback(() => {
2132
2243
  const msgs = agent.getMessages();
2133
2244
  if (msgs.length > 0) {
2134
- saveSession(sessionId, msgs, models[0] || 'unknown', undefined, costTracker.getState()).catch(() => { });
2245
+ saveSession(sessionId, msgs, models[0] || 'unknown', undefined, costTracker.getState()).catch((err) => { logger.warn('Failed to save session on clear', { error: err.message }); });
2135
2246
  }
2136
2247
  agent.clearHistory();
2137
2248
  setItems([]);
@@ -2146,6 +2257,7 @@ function App({ config }) {
2146
2257
  try {
2147
2258
  initAgent(newConfig);
2148
2259
  setModels(agent.getModels());
2260
+ setAgentVersion(v => v + 1);
2149
2261
  add('system', 'Configuration updated. Agent reinitialized.');
2150
2262
  }
2151
2263
  catch (err) {
@@ -2157,8 +2269,18 @@ function App({ config }) {
2157
2269
  const bannerHeight = showBanner ? 5 : 0;
2158
2270
  const statusBarHeight = 1;
2159
2271
  const bottomHeight = useMemo(() => {
2160
- if (confirmInfo)
2161
- return 5;
2272
+ if (confirmInfo) {
2273
+ let parsed = {};
2274
+ try {
2275
+ parsed = typeof confirmInfo.args === 'string' ? JSON.parse(confirmInfo.args) : confirmInfo.args ?? {};
2276
+ }
2277
+ catch {
2278
+ parsed = {};
2279
+ }
2280
+ const extraCount = Math.min(Object.keys(parsed).filter(k => !['command', 'filePath', 'file_path', 'notebook_path', 'message', 'content'].includes(k)).length, 3);
2281
+ const hasHighlight = !!(parsed.command ?? parsed.filePath ?? parsed.file_path ?? parsed.notebook_path ?? parsed.message);
2282
+ return 5 + (hasHighlight ? 1 : 0) + extraCount;
2283
+ }
2162
2284
  if (selectingModel)
2163
2285
  return 6 + Math.min(models.length + 2, 12);
2164
2286
  if (selectingMode)
@@ -2197,7 +2319,7 @@ function App({ config }) {
2197
2319
  loadConfig().then(cfg => {
2198
2320
  cfg.models = [v];
2199
2321
  saveConfig(cfg);
2200
- }).catch(() => { });
2322
+ }).catch((err) => { logger.warn('Failed to save config', { error: err.message }); });
2201
2323
  add('system', `Model switched to ${shortModel(v)}`);
2202
2324
  }, onCancel: () => setSelectingModel(false) })) : selectingMode ? (_jsx(ModePicker, { currentMode: currentMode, onSelect: (mode) => {
2203
2325
  agent.setPermissionMode(mode);
@@ -2206,7 +2328,7 @@ function App({ config }) {
2206
2328
  loadConfig().then(cfg => {
2207
2329
  cfg.permissionMode = mode;
2208
2330
  saveConfig(cfg);
2209
- }).catch(() => { });
2331
+ }).catch((err) => { logger.warn('Failed to save config', { error: err.message }); });
2210
2332
  add('system', `Mode: ${mode.toUpperCase()} \u2014 ${getModeDescription(mode)}`);
2211
2333
  }, onCancel: () => setSelectingMode(false) })) : editingSettings ? (_jsx(SettingsEditor, { onSave: (key, value) => {
2212
2334
  loadConfig().then((c) => {
@@ -2247,7 +2369,10 @@ function Root() {
2247
2369
  setConfig(c);
2248
2370
  if (isConfigured(c)) {
2249
2371
  // Fetch model specs from API in background (uses user-defined/built-in as fallback)
2250
- fetchModelSpecs(c.baseUrl, c.apiKey).catch(() => { });
2372
+ fetchModelSpecs(c.baseUrl, c.apiKey).catch((err) => { logger.warn('Failed to fetch model specs', { error: err.message }); });
2373
+ if (!process.env.NO_UPDATE_NOTIFIER && !c.disableUpdateCheck) {
2374
+ checkForUpdate().catch(() => { });
2375
+ }
2251
2376
  try {
2252
2377
  initAgent(c);
2253
2378
  setReady(true);
@@ -2257,7 +2382,7 @@ function Root() {
2257
2382
  }
2258
2383
  }
2259
2384
  }).catch(() => {
2260
- setConfig({ apiKey: '', baseUrl: DEFAULT_BASE_URL, models: [...DEFAULT_MODELS], customModels: {}, limits: { ...DEFAULT_LIMITS }, permissionMode: 'default', thinkingMode: 'off' });
2385
+ setConfig({ apiKey: '', baseUrl: DEFAULT_BASE_URL, models: [...DEFAULT_MODELS], customModels: {}, limits: { ...DEFAULT_LIMITS }, permissionMode: 'default', thinkingMode: 'off', security: {}, injectionDetection: { enabled: true, sensitivity: 'medium' } });
2261
2386
  });
2262
2387
  }, []);
2263
2388
  const handleSetupComplete = useCallback((c) => {
@@ -2324,6 +2449,6 @@ function _restoreTerminal() {
2324
2449
  _origWrite('\x1b[?25h'); // show cursor
2325
2450
  }
2326
2451
  process.on('exit', _restoreTerminal);
2327
- process.on('SIGINT', () => { _restoreTerminal(); process.exit(0); });
2328
- process.on('SIGTERM', () => { _restoreTerminal(); process.exit(0); });
2452
+ process.on('SIGINT', () => { _restoreTerminal(); closeBrowser().finally(() => process.exit(0)); });
2453
+ process.on('SIGTERM', () => { _restoreTerminal(); closeBrowser().finally(() => process.exit(0)); });
2329
2454
  render(_jsx(Root, {}), { exitOnCtrlC: false, patchConsole: false });
package/dist/constants.js CHANGED
@@ -1,4 +1,4 @@
1
- export const VERSION = '1.0.0';
1
+ export const VERSION = '1.1.0';
2
2
  export const PRODUCT_NAME = 'Scalix-Coder';
3
3
  export const USER_AGENT = `${PRODUCT_NAME}/${VERSION}`;
4
4
  export const DEFAULT_BASE_URL = process.env.SCALIX_BASE_URL || 'https://api.scalix.world/v1';
@@ -199,10 +199,16 @@ export const TOOL_DEFAULTS = {
199
199
  export const SUB_AGENT_DEFAULTS = {
200
200
  timeoutMs: 600_000,
201
201
  agentToolTimeoutMs: 600_000,
202
- maxSteps: 100,
203
- exploreMaxSteps: 50,
204
- maxContextTokens: 800_000,
205
- compressionThreshold: 0.85,
202
+ maxSteps: 500,
203
+ exploreMaxSteps: 200,
204
+ maxContextTokens: 1_000_000,
205
+ compressionThreshold: 0.80,
206
+ };
207
+ // ─── Multi-Agent Defaults ───
208
+ export const MULTI_AGENT_DEFAULTS = {
209
+ maxConcurrentAgents: 10,
210
+ maxDepth: 5,
211
+ globalTokenBudget: 5_000_000,
206
212
  };
207
213
  // ─── Headless Defaults ───
208
214
  export const HEADLESS_DEFAULTS = {
@@ -263,16 +269,37 @@ export const TOKEN_ESTIMATION = {
263
269
  simpleCharsPerToken: 4,
264
270
  };
265
271
  // ─── Network Security ───
266
- export const PRIVATE_NETWORK_HOSTNAMES = new Set(['localhost', '127.0.0.1', '::1', '0.0.0.0']);
272
+ export const PRIVATE_NETWORK_HOSTNAMES = new Set([
273
+ 'localhost', '127.0.0.1', '::1', '[::1]', '0.0.0.0',
274
+ '::ffff:127.0.0.1', '[::ffff:127.0.0.1]',
275
+ ]);
267
276
  export const PRIVATE_NETWORK_SUFFIXES = ['.local', '.internal'];
268
277
  export const PRIVATE_NETWORK_CIDRS = [
269
278
  /^10\./,
270
279
  /^172\.(1[6-9]|2\d|3[01])\./,
271
280
  /^192\.168\./,
272
281
  /^169\.254\./,
282
+ /^\[?fe80:/i,
283
+ /^\[?fc00:/i,
284
+ /^\[?fd[0-9a-f]{2}:/i,
285
+ /^\[?::ffff:(10\.|172\.(1[6-9]|2\d|3[01])\.|192\.168\.|127\.)/i,
273
286
  ];
287
+ let _networkOverrides = {};
288
+ export function setNetworkSecurityOverrides(overrides) {
289
+ _networkOverrides = {
290
+ allowAll: overrides.allowAll,
291
+ blockedHosts: overrides.blockedHosts ? new Set(overrides.blockedHosts.map(h => h.toLowerCase())) : undefined,
292
+ allowedHosts: overrides.allowedHosts ? new Set(overrides.allowedHosts.map(h => h.toLowerCase())) : undefined,
293
+ };
294
+ }
274
295
  export function isPrivateHost(hostname) {
275
296
  const lower = hostname.toLowerCase();
297
+ if (_networkOverrides.allowAll)
298
+ return false;
299
+ if (_networkOverrides.allowedHosts?.has(lower))
300
+ return false;
301
+ if (_networkOverrides.blockedHosts?.has(lower))
302
+ return true;
276
303
  if (PRIVATE_NETWORK_HOSTNAMES.has(lower))
277
304
  return true;
278
305
  if (PRIVATE_NETWORK_SUFFIXES.some(s => lower.endsWith(s)))
@@ -298,6 +325,13 @@ export const COMPRESSION_DEFAULTS = {
298
325
  keepRatio: 0.4,
299
326
  truncateDisplay: 1000,
300
327
  };
328
+ // ─── Update Check Defaults ───
329
+ export const UPDATE_DEFAULTS = {
330
+ checkIntervalMs: 24 * 60 * 60 * 1000,
331
+ registryUrl: 'https://registry.npmjs.org/scalix/latest',
332
+ fetchTimeoutMs: 5_000,
333
+ npmPackageName: 'scalix',
334
+ };
301
335
  // ─── Complexity Patterns (for model routing) ───
302
336
  export const COMPLEXITY_PATTERNS = {
303
337
  simple: [
@@ -4,6 +4,7 @@ import { collectGitInfo, gitInfoToContext } from './git-context.js';
4
4
  import { loadMemory, memoryToContext } from './project-memory.js';
5
5
  import { buildProjectIndex, indexToContext } from './project-index.js';
6
6
  import { loadScopedRules, rulesToContext } from './scoped-rules.js';
7
+ import { retrieveContext, snippetsToContext, RETRIEVAL_DEFAULTS } from './context-retriever.js';
7
8
  async function readJsonSafe(filePath) {
8
9
  try {
9
10
  return JSON.parse(await fs.readFile(filePath, 'utf-8'));
@@ -102,7 +103,7 @@ export async function buildProjectContext(cwd) {
102
103
  const projectType = await detectProjectType(cwd);
103
104
  // Gather context in parallel
104
105
  const [gitCtx, memory, rules, projectConfig] = await Promise.all([
105
- Promise.resolve(collectGitInfo(cwd)).then(gitInfoToContext).catch(() => ''),
106
+ collectGitInfo(cwd).then(gitInfoToContext).catch(() => ''),
106
107
  loadMemory(cwd).then(memoryToContext).catch(() => ''),
107
108
  loadScopedRules(cwd).then(rulesToContext).catch(() => ''),
108
109
  loadProjectConfig(cwd),
@@ -149,3 +150,9 @@ export function contextToInstructions(baseInstructions, ctx) {
149
150
  }
150
151
  return sections.filter(Boolean).join('\n\n');
151
152
  }
153
+ export async function buildQueryContext(query, cwd, config) {
154
+ const index = await buildProjectIndex(cwd);
155
+ const effectiveConfig = config ? { ...RETRIEVAL_DEFAULTS, ...config } : RETRIEVAL_DEFAULTS;
156
+ const result = await retrieveContext(query, index, effectiveConfig);
157
+ return snippetsToContext(result.snippets);
158
+ }