scalix 1.0.0 → 1.2.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.
@@ -0,0 +1,154 @@
1
+ export const GROUNDING_DEFAULTS = {
2
+ enabled: true,
3
+ maxNudges: 1,
4
+ ungroundedThreshold: 0.5,
5
+ minClaimsToCheck: 2,
6
+ };
7
+ const TRIVIAL_FILES = new Set([
8
+ 'README.md', 'LICENSE', 'package.json', 'tsconfig.json',
9
+ 'Cargo.toml', 'go.mod', 'Makefile', 'Dockerfile',
10
+ '.gitignore', '.env', '.env.example',
11
+ ]);
12
+ const FILE_PATH_RE = /(?:^|[\s`"'(])([.\w][\w./-]*\/[\w./-]+\.\w{1,10})(?=[\s`"'),.:;]|$)/gm;
13
+ const FUNC_CALL_RE = /\b([a-zA-Z_]\w{2,})\s*\(/g;
14
+ const FUNC_DESC_RE = /\b(?:function|method|def|fn|func|handler|callback)\s+([a-zA-Z_]\w{2,})\b/gi;
15
+ const CLASS_RE = /\b(?:class|interface|type|struct|enum|trait)\s+([A-Z]\w{2,})\b/g;
16
+ const LINE_NUM_RE = /\b(?:line\s+|:)(\d{2,5})\b/gi;
17
+ const IMPORT_RE = /(?:import|from|require)\s*\(?['"]([^'"]+)['"]\)?/g;
18
+ function surroundingContext(text, pos, radius = 60) {
19
+ const start = Math.max(0, pos - radius);
20
+ const end = Math.min(text.length, pos + radius);
21
+ return text.slice(start, end).replace(/\n/g, ' ').trim();
22
+ }
23
+ export function extractCodeClaims(text) {
24
+ const seen = new Set();
25
+ const claims = [];
26
+ function add(type, value, position) {
27
+ const key = `${type}:${value}`;
28
+ if (seen.has(key))
29
+ return;
30
+ seen.add(key);
31
+ claims.push({ type, value, context: surroundingContext(text, position), position });
32
+ }
33
+ // File paths (must contain a / to avoid single-word matches)
34
+ for (const m of text.matchAll(FILE_PATH_RE)) {
35
+ const val = m[1];
36
+ const basename = val.split('/').pop() || val;
37
+ if (TRIVIAL_FILES.has(basename) || TRIVIAL_FILES.has(val))
38
+ continue;
39
+ add('file_path', val, m.index + m[0].indexOf(val));
40
+ }
41
+ // Function calls — foo()
42
+ for (const m of text.matchAll(FUNC_CALL_RE)) {
43
+ const name = m[1];
44
+ if (name.length > 50 || /^(if|for|while|switch|return|new|typeof|catch|throw|await|async|const|let|var|function|class|import|export|from|this|self|true|false|null|undefined|console|process|require|module)$/.test(name))
45
+ continue;
46
+ add('function_name', name, m.index);
47
+ }
48
+ // Function descriptions — "the handleSubmit function"
49
+ for (const m of text.matchAll(FUNC_DESC_RE)) {
50
+ add('function_name', m[1], m.index);
51
+ }
52
+ // Class/interface/type names
53
+ for (const m of text.matchAll(CLASS_RE)) {
54
+ if (m[1].length > 50)
55
+ continue;
56
+ add('class_name', m[1], m.index);
57
+ }
58
+ // Line numbers (only >9 to avoid trivial matches)
59
+ for (const m of text.matchAll(LINE_NUM_RE)) {
60
+ const num = parseInt(m[1], 10);
61
+ if (num < 10)
62
+ continue;
63
+ add('line_number', m[1], m.index);
64
+ }
65
+ // Import references
66
+ for (const m of text.matchAll(IMPORT_RE)) {
67
+ if (m[1].startsWith('.')) {
68
+ add('import', m[1], m.index);
69
+ }
70
+ }
71
+ return claims;
72
+ }
73
+ function buildGroundingCorpus(messages) {
74
+ const parts = [];
75
+ for (const m of messages) {
76
+ if (m.role === 'tool' && m.content) {
77
+ parts.push(typeof m.content === 'string' ? m.content : JSON.stringify(m.content));
78
+ }
79
+ if (m.role === 'assistant' && m.tool_calls) {
80
+ for (const tc of m.tool_calls) {
81
+ parts.push(tc.function.arguments);
82
+ }
83
+ }
84
+ }
85
+ return parts.join('\n');
86
+ }
87
+ export function checkGrounding(claims, messages, threshold = GROUNDING_DEFAULTS.ungroundedThreshold, minClaims = GROUNDING_DEFAULTS.minClaimsToCheck) {
88
+ if (claims.length === 0) {
89
+ return { claims, groundedClaims: [], ungroundedClaims: [], groundingRatio: 1, needsVerification: false };
90
+ }
91
+ const corpus = buildGroundingCorpus(messages);
92
+ const corpusLower = corpus.toLowerCase();
93
+ const grounded = [];
94
+ const ungrounded = [];
95
+ for (const claim of claims) {
96
+ let found = false;
97
+ switch (claim.type) {
98
+ case 'file_path': {
99
+ if (corpus.includes(claim.value)) {
100
+ found = true;
101
+ }
102
+ else {
103
+ const basename = claim.value.split('/').pop();
104
+ if (corpus.includes(basename))
105
+ found = true;
106
+ }
107
+ break;
108
+ }
109
+ case 'function_name':
110
+ case 'class_name':
111
+ case 'variable_name': {
112
+ found = corpus.includes(claim.value);
113
+ break;
114
+ }
115
+ case 'line_number': {
116
+ const escaped = claim.value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
117
+ const linePattern = new RegExp(`(?:^|\\n)\\s*${escaped}\\t|:${escaped}[:\\s]`, 'm');
118
+ found = linePattern.test(corpus);
119
+ break;
120
+ }
121
+ case 'import': {
122
+ found = corpus.includes(claim.value);
123
+ break;
124
+ }
125
+ default:
126
+ found = corpusLower.includes(claim.value.toLowerCase());
127
+ }
128
+ if (found)
129
+ grounded.push(claim);
130
+ else
131
+ ungrounded.push(claim);
132
+ }
133
+ const ratio = grounded.length / claims.length;
134
+ const needsVerification = ratio < threshold && ungrounded.length >= minClaims;
135
+ return { claims, groundedClaims: grounded, ungroundedClaims: ungrounded, groundingRatio: ratio, needsVerification };
136
+ }
137
+ export function formatGroundingNudge(result) {
138
+ const items = result.ungroundedClaims.slice(0, 5).map((c) => {
139
+ switch (c.type) {
140
+ case 'file_path': return `- File "${c.value}" (not read in this conversation)`;
141
+ case 'function_name': return `- Function "${c.value}" (not seen in any tool output)`;
142
+ case 'class_name': return `- Type/class "${c.value}" (not seen in any tool output)`;
143
+ case 'line_number': return `- Line ${c.value} (not verified by reading the file)`;
144
+ case 'import': return `- Import "${c.value}" (not verified)`;
145
+ default: return `- "${c.value}" (not verified)`;
146
+ }
147
+ });
148
+ return [
149
+ '[GROUNDING CHECK] Your response references code elements that were not verified by reading the actual source:',
150
+ ...items,
151
+ 'Please use read_file, search_files, or find_files to verify these references before presenting them as facts.',
152
+ 'If you are uncertain about a path or name, say so explicitly rather than guessing.',
153
+ ].join('\n');
154
+ }
package/dist/headless.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import readline from 'node:readline';
2
2
  import fs from 'node:fs/promises';
3
+ import path from 'node:path';
3
4
  import { execSync } from 'node:child_process';
4
5
  import { createAgent } from './agent.js';
5
6
  import { defaultTools } from './tools.js';
@@ -8,13 +9,14 @@ 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
- import { browserTools } from './tools-browser.js';
12
+ import { browserTools, closeBrowser } from './tools-browser.js';
12
13
  import { loadMcpServices } 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
17
  import { DEFAULT_MODELS, DEFAULT_INSTRUCTIONS, ENV_API_KEY, ENV_MODEL, ENV_BASE_URL, HEADLESS_DEFAULTS, fetchModelSpecs } from './constants.js';
17
18
  import { loadConfig, DEFAULT_LIMITS } from './settings.js';
19
+ import { checkForUpdate, getUpdateBanner } from './update-checker.js';
18
20
  import { getModeDescription, PERMISSION_MODES, setSandboxRoot } from './sandbox.js';
19
21
  import { collectGitInfo, gitInfoToContext } from './git-context.js';
20
22
  import { loadMemory, autoDetectConventions, saveMemory, memoryToContext, addNote, detectCorrection } from './project-memory.js';
@@ -26,15 +28,20 @@ import { hooksSummary } from './hooks.js';
26
28
  import { spawnSubAgent, pickCheapestModel } from './sub-agent.js';
27
29
  import { createCheckpoint, undoLastCheckpoint, listCheckpoints } from './git-undo.js';
28
30
  import { runIpcServer } from './ipc-server.js';
29
- function shortModel(m) {
31
+ import { AgentRegistry } from './agent-registry.js';
32
+ export function shortModel(m) {
30
33
  return m.split('/').pop().replace(/:free$/, '').replace(/-instruct$/, '');
31
34
  }
32
- function parseArgs() {
35
+ export function parseArgs() {
33
36
  const args = process.argv.slice(2);
34
37
  let mode = 'interactive';
35
38
  let prompt;
36
39
  let output;
37
40
  let format;
41
+ let permissionMode;
42
+ let outputSchema;
43
+ let worktree = false;
44
+ let worktreeName;
38
45
  for (let i = 0; i < args.length; i++) {
39
46
  if (args[i] === 'run' && i + 1 < args.length) {
40
47
  mode = 'run';
@@ -49,12 +56,32 @@ function parseArgs() {
49
56
  format = args[i + 1];
50
57
  i++;
51
58
  }
59
+ else if (args[i] === '--output-format' && i + 1 < args.length) {
60
+ format = args[i + 1];
61
+ i++;
62
+ }
63
+ else if (args[i] === '--output-schema' && i + 1 < args.length) {
64
+ outputSchema = args[i + 1];
65
+ format = 'json-schema';
66
+ i++;
67
+ }
68
+ else if (args[i] === '--mode' && i + 1 < args.length) {
69
+ permissionMode = args[i + 1];
70
+ i++;
71
+ }
52
72
  else if (args[i] === '--pipe' || args[i] === '-p') {
53
73
  mode = 'pipe';
54
74
  }
55
75
  else if (args[i] === '--ipc') {
56
76
  mode = 'ipc';
57
77
  }
78
+ else if (args[i] === '--worktree' || args[i] === '-w') {
79
+ worktree = true;
80
+ if (i + 1 < args.length && !args[i + 1].startsWith('-') && args[i + 1] !== 'run') {
81
+ worktreeName = args[i + 1];
82
+ i++;
83
+ }
84
+ }
58
85
  else if (args[i] === '--help' || args[i] === '-h') {
59
86
  console.log(`
60
87
  Scalix - Headless Mode
@@ -71,7 +98,10 @@ Options:
71
98
  --pipe, -p Read prompt from stdin (for piping)
72
99
  --ipc IPC server mode for VS Code extension integration
73
100
  --output <file> Write result to file instead of stdout
74
- --format <fmt> Output format: text (default), json, patch, markdown
101
+ --format <fmt> Output format: text (default), json, json-schema, patch, markdown
102
+ --output-format Alias for --format
103
+ --output-schema Path to JSON schema file (implies --format json-schema)
104
+ --worktree, -w Run in an isolated git worktree (optional: --worktree <name>)
75
105
  --help, -h Show this help
76
106
  `);
77
107
  process.exit(0);
@@ -80,7 +110,7 @@ Options:
80
110
  if (!process.stdin.isTTY && mode === 'interactive') {
81
111
  mode = 'pipe';
82
112
  }
83
- return { mode, prompt, output, format };
113
+ return { mode, prompt, output, format, permissionMode, outputSchema, worktree, worktreeName };
84
114
  }
85
115
  async function buildEnhancedInstructions() {
86
116
  let base = DEFAULT_INSTRUCTIONS;
@@ -101,7 +131,7 @@ async function buildEnhancedInstructions() {
101
131
  const ctx = await buildProjectContext(cwd);
102
132
  return contextToInstructions(base, ctx);
103
133
  }
104
- async function runSinglePrompt(prompt, output, format) {
134
+ async function runSinglePrompt(prompt, output, format, outputSchemaPath) {
105
135
  const apiKey = process.env[ENV_API_KEY];
106
136
  if (!apiKey) {
107
137
  console.error('Error: SCALIX_API_KEY is not set.\nSet it via: export SCALIX_API_KEY=sk-...\nOr run the interactive setup: scalix');
@@ -112,6 +142,13 @@ async function runSinglePrompt(prompt, output, format) {
112
142
  const baseUrl = process.env[ENV_BASE_URL] || config.baseUrl;
113
143
  // Fetch model specs from API (non-blocking — uses built-in/user-defined as fallback)
114
144
  fetchModelSpecs(baseUrl, apiKey).catch(() => { });
145
+ if (!process.env.NO_UPDATE_NOTIFIER && !config.disableUpdateCheck) {
146
+ checkForUpdate().then(() => {
147
+ const banner = getUpdateBanner();
148
+ if (banner)
149
+ process.stderr.write(banner + '\n');
150
+ }).catch(() => { });
151
+ }
115
152
  const envModels = process.env[ENV_MODEL]
116
153
  ? process.env[ENV_MODEL].split(',').map((m) => m.trim()).filter(Boolean)
117
154
  : [];
@@ -126,13 +163,30 @@ async function runSinglePrompt(prompt, output, format) {
126
163
  const pluginTools = getPluginTools(plugins);
127
164
  const allTools = [...defaultTools, ...extraTools, ...webTools, ...advancedTools, ...visionTools, ...voiceTools, ...browserTools, ...mcpTools, ...pluginTools];
128
165
  const instructions = await buildEnhancedInstructions();
166
+ let outputSchema;
167
+ if (outputSchemaPath) {
168
+ try {
169
+ const schemaContent = await fs.readFile(outputSchemaPath, 'utf-8');
170
+ const parsed = JSON.parse(schemaContent);
171
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed) || !parsed.type) {
172
+ console.error(`Invalid JSON schema: must be an object with a "type" property`);
173
+ process.exit(1);
174
+ }
175
+ outputSchema = parsed;
176
+ }
177
+ catch (err) {
178
+ console.error(`Error loading output schema from ${outputSchemaPath}: ${err.message}`);
179
+ process.exit(1);
180
+ }
181
+ }
182
+ const outputFormat = (format === 'json' || format === 'json-schema') ? format : undefined;
129
183
  const agent = createAgent({
130
184
  apiKey,
131
185
  baseUrl,
132
186
  model: selectedModel,
133
187
  instructions,
134
188
  tools: allTools,
135
- permissionMode: config.permissionMode || 'default',
189
+ permissionMode: (process.env.SCALIX_PERMISSION_MODE || config.permissionMode || 'default'),
136
190
  maxSteps: limits.maxSteps,
137
191
  maxMessages: limits.maxMessages,
138
192
  contextManager: {
@@ -142,6 +196,10 @@ async function runSinglePrompt(prompt, output, format) {
142
196
  },
143
197
  instructionLoader: buildEnhancedInstructions,
144
198
  autoVerify: true,
199
+ outputFormat,
200
+ outputSchema,
201
+ groundingCheck: true,
202
+ contextRetrieval: true,
145
203
  });
146
204
  let fullResponse = '';
147
205
  agent.on('stream:end', (text) => { fullResponse = text; });
@@ -167,6 +225,9 @@ async function runSinglePrompt(prompt, output, format) {
167
225
  cost: costTracker.totalCost,
168
226
  }, null, 2);
169
227
  break;
228
+ case 'json-schema':
229
+ formatted = fullResponse;
230
+ break;
170
231
  case 'patch':
171
232
  formatted = fullResponse;
172
233
  break;
@@ -183,20 +244,27 @@ async function runSinglePrompt(prompt, output, format) {
183
244
  else {
184
245
  process.stdout.write(formatted + '\n');
185
246
  }
247
+ await closeBrowser();
186
248
  process.exit(0);
187
249
  }
188
250
  async function runPipeMode() {
189
251
  const MAX_PIPE_BYTES = 50 * 1024 * 1024;
190
252
  const chunks = [];
191
253
  let totalBytes = 0;
192
- for await (const chunk of process.stdin) {
193
- const str = chunk.toString();
194
- totalBytes += Buffer.byteLength(str);
195
- if (totalBytes > MAX_PIPE_BYTES) {
196
- console.error(`Error: Pipe input exceeds ${MAX_PIPE_BYTES / 1024 / 1024}MB limit.`);
197
- process.exit(1);
254
+ try {
255
+ for await (const chunk of process.stdin) {
256
+ const str = chunk.toString();
257
+ totalBytes += Buffer.byteLength(str);
258
+ if (totalBytes > MAX_PIPE_BYTES) {
259
+ console.error(`Error: Pipe input exceeds ${MAX_PIPE_BYTES / 1024 / 1024}MB limit.`);
260
+ process.exit(1);
261
+ }
262
+ chunks.push(str);
198
263
  }
199
- chunks.push(str);
264
+ }
265
+ catch (err) {
266
+ console.error(`Error reading pipe input: ${err.message}`);
267
+ process.exit(1);
200
268
  }
201
269
  const prompt = chunks.join('').trim();
202
270
  if (!prompt) {
@@ -232,7 +300,14 @@ async function runInteractive() {
232
300
  input: process.stdin,
233
301
  output: process.stdout,
234
302
  });
303
+ let rlClosed = false;
304
+ rl.on('close', () => {
305
+ rlClosed = true;
306
+ closeBrowser().finally(() => process.exit(0));
307
+ });
235
308
  function askUser(question) {
309
+ if (rlClosed)
310
+ return Promise.resolve('/exit');
236
311
  return new Promise((resolve) => rl.question(question, resolve));
237
312
  }
238
313
  const customCmds = await loadCustomCommands(process.cwd());
@@ -242,7 +317,7 @@ async function runInteractive() {
242
317
  model: selectedModel,
243
318
  instructions,
244
319
  tools: allTools,
245
- permissionMode: config.permissionMode || 'default',
320
+ permissionMode: (process.env.SCALIX_PERMISSION_MODE || config.permissionMode || 'default'),
246
321
  maxSteps: limits.maxSteps,
247
322
  maxMessages: limits.maxMessages,
248
323
  contextManager: {
@@ -252,6 +327,8 @@ async function runInteractive() {
252
327
  },
253
328
  instructionLoader: buildEnhancedInstructions,
254
329
  autoVerify: true,
330
+ groundingCheck: true,
331
+ contextRetrieval: true,
255
332
  confirmToolCall: async (toolName, args) => {
256
333
  const preview = typeof args === 'string' ? args : JSON.stringify(args, null, 2);
257
334
  console.log(`\n\u26A0 Tool "${toolName}" wants to execute:`);
@@ -307,7 +384,7 @@ async function runInteractive() {
307
384
  }
308
385
  console.log('Goodbye!');
309
386
  rl.close();
310
- process.exit(0);
387
+ closeBrowser().finally(() => process.exit(0));
311
388
  }
312
389
  process.on('SIGINT', shutdown);
313
390
  process.on('SIGTERM', shutdown);
@@ -329,6 +406,7 @@ async function runInteractive() {
329
406
  console.log(' /hooks \u2014 show lifecycle hooks');
330
407
  console.log(' /commands \u2014 show custom commands');
331
408
  console.log(' /delegate <t>\u2014 run task with sub-agent');
409
+ console.log(' /agents \u2014 show active agents & usage');
332
410
  console.log(' /checkpoint \u2014 create git checkpoint');
333
411
  console.log(' /undo \u2014 undo last checkpoint');
334
412
  console.log(' /checkpoints \u2014 list recent checkpoints');
@@ -488,6 +566,22 @@ async function runInteractive() {
488
566
  }
489
567
  return true;
490
568
  }
569
+ if (cmd === '/agents') {
570
+ const registry = AgentRegistry.getInstance();
571
+ const summary = registry.getUsageSummary();
572
+ const tree = registry.getAgentTree();
573
+ console.log(`\nActive: ${summary.activeCount} | Completed: ${summary.completedCount} | Failed: ${summary.failedCount}`);
574
+ console.log(`Total tokens: ${summary.totalTokens.toLocaleString()}`);
575
+ if (tree && tree !== '(no agents)') {
576
+ console.log('\nAgent tree:');
577
+ console.log(tree);
578
+ }
579
+ else {
580
+ console.log('No agents have been spawned yet.');
581
+ }
582
+ console.log('');
583
+ return true;
584
+ }
491
585
  if (cmd === '/checkpoint') {
492
586
  const label = input.slice(12).trim() || undefined;
493
587
  const hash = await createCheckpoint(process.cwd(), label);
@@ -539,7 +633,7 @@ async function runInteractive() {
539
633
  return true;
540
634
  }
541
635
  if (cmd === '/git') {
542
- const gitInfo = collectGitInfo(process.cwd());
636
+ const gitInfo = await collectGitInfo(process.cwd());
543
637
  if (!gitInfo.isRepo) {
544
638
  console.log('\nNot a git repository.\n');
545
639
  }
@@ -630,14 +724,46 @@ async function runInteractive() {
630
724
  }
631
725
  async function main() {
632
726
  setSandboxRoot(process.cwd());
633
- const { mode, prompt, output, format } = parseArgs();
727
+ const { mode, prompt, output, format, permissionMode: cliPermissionMode, outputSchema, worktree, worktreeName } = parseArgs();
728
+ if (cliPermissionMode) {
729
+ process.env.SCALIX_PERMISSION_MODE = cliPermissionMode;
730
+ }
731
+ // Set up worktree isolation if requested
732
+ let worktreeInfo = null;
733
+ if (worktree) {
734
+ const { createWorktree, removeWorktree } = await import('./worktree.js');
735
+ const name = worktreeName || `session-${Date.now().toString(36)}`;
736
+ try {
737
+ worktreeInfo = await createWorktree(name, process.cwd());
738
+ process.chdir(worktreeInfo.path);
739
+ setSandboxRoot(worktreeInfo.path);
740
+ console.log(`Working in isolated worktree: ${worktreeInfo.branch} → ${worktreeInfo.path}`);
741
+ }
742
+ catch (err) {
743
+ console.error(`Failed to create worktree: ${err.message}`);
744
+ process.exit(1);
745
+ }
746
+ const cleanup = async () => {
747
+ if (worktreeInfo) {
748
+ try {
749
+ const origCwd = process.env.SCALIX_ORIGINAL_CWD || process.cwd();
750
+ process.chdir(path.dirname(worktreeInfo.path));
751
+ await removeWorktree(worktreeInfo, path.dirname(path.dirname(worktreeInfo.path)));
752
+ }
753
+ catch { }
754
+ }
755
+ };
756
+ process.on('exit', () => { cleanup().catch(() => { }); });
757
+ process.on('SIGINT', () => { cleanup().then(() => process.exit(0)).catch(() => process.exit(1)); });
758
+ process.env.SCALIX_ORIGINAL_CWD = process.cwd();
759
+ }
634
760
  switch (mode) {
635
761
  case 'run':
636
762
  if (!prompt) {
637
763
  console.error('Error: No prompt provided. Usage: scalix run "your prompt"');
638
764
  process.exit(1);
639
765
  }
640
- await runSinglePrompt(prompt, output, format);
766
+ await runSinglePrompt(prompt, output, format, outputSchema);
641
767
  break;
642
768
  case 'pipe':
643
769
  await runPipeMode();
@@ -651,4 +777,7 @@ async function main() {
651
777
  break;
652
778
  }
653
779
  }
654
- main().catch(console.error);
780
+ main().catch((err) => {
781
+ console.error(err);
782
+ process.exit(1);
783
+ });