scalix 0.0.1 → 1.0.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.
Files changed (48) hide show
  1. package/CHANGELOG.md +122 -0
  2. package/LICENSE +100 -0
  3. package/README.md +300 -2
  4. package/app.py +55 -0
  5. package/bin/scalix.js +19 -0
  6. package/dist/agent-replay.js +166 -0
  7. package/dist/agent.js +852 -0
  8. package/dist/auto-verify.js +136 -0
  9. package/dist/change-tracker.js +53 -0
  10. package/dist/cli.js +2329 -0
  11. package/dist/commands.js +71 -0
  12. package/dist/config.js +16 -0
  13. package/dist/constants.js +324 -0
  14. package/dist/context-builder.js +151 -0
  15. package/dist/context-manager.js +148 -0
  16. package/dist/cost-tracker.js +85 -0
  17. package/dist/diff.js +140 -0
  18. package/dist/git-context.js +180 -0
  19. package/dist/git-undo.js +67 -0
  20. package/dist/headless.js +654 -0
  21. package/dist/hooks.js +94 -0
  22. package/dist/ipc-server.js +228 -0
  23. package/dist/logger.js +45 -0
  24. package/dist/markdown.js +41 -0
  25. package/dist/mcp.js +49 -0
  26. package/dist/model-router.js +68 -0
  27. package/dist/plugins.js +132 -0
  28. package/dist/project-index.js +374 -0
  29. package/dist/project-memory.js +165 -0
  30. package/dist/safe-math.js +194 -0
  31. package/dist/sandbox.js +279 -0
  32. package/dist/scoped-rules.js +82 -0
  33. package/dist/sessions.js +71 -0
  34. package/dist/settings.js +155 -0
  35. package/dist/sub-agent.js +79 -0
  36. package/dist/token-counter.js +47 -0
  37. package/dist/tool-helper.js +97 -0
  38. package/dist/tool-selector.js +77 -0
  39. package/dist/tools-advanced.js +232 -0
  40. package/dist/tools-browser.js +273 -0
  41. package/dist/tools-extra.js +383 -0
  42. package/dist/tools-vision.js +115 -0
  43. package/dist/tools-voice.js +147 -0
  44. package/dist/tools-web.js +346 -0
  45. package/dist/tools.js +370 -0
  46. package/dist/types.js +1 -0
  47. package/docs/DISTRIBUTION.md +301 -0
  48. package/package.json +67 -6
@@ -0,0 +1,654 @@
1
+ import readline from 'node:readline';
2
+ import fs from 'node:fs/promises';
3
+ import { execSync } from 'node:child_process';
4
+ import { createAgent } from './agent.js';
5
+ import { defaultTools } from './tools.js';
6
+ import { extraTools } from './tools-extra.js';
7
+ import { webTools } from './tools-web.js';
8
+ import { advancedTools } from './tools-advanced.js';
9
+ import { visionTools } from './tools-vision.js';
10
+ import { voiceTools } from './tools-voice.js';
11
+ import { browserTools } from './tools-browser.js';
12
+ import { loadMcpServices } from './mcp.js';
13
+ import { costTracker } from './cost-tracker.js';
14
+ import { saveSession, loadSession, listSessions, generateSessionId } from './sessions.js';
15
+ import { logger } from './logger.js';
16
+ import { DEFAULT_MODELS, DEFAULT_INSTRUCTIONS, ENV_API_KEY, ENV_MODEL, ENV_BASE_URL, HEADLESS_DEFAULTS, fetchModelSpecs } from './constants.js';
17
+ import { loadConfig, DEFAULT_LIMITS } from './settings.js';
18
+ import { getModeDescription, PERMISSION_MODES, setSandboxRoot } from './sandbox.js';
19
+ import { collectGitInfo, gitInfoToContext } from './git-context.js';
20
+ import { loadMemory, autoDetectConventions, saveMemory, memoryToContext, addNote, detectCorrection } from './project-memory.js';
21
+ import { buildProjectIndex } from './project-index.js';
22
+ import { buildProjectContext, contextToInstructions } from './context-builder.js';
23
+ import { discoverPlugins, getPluginTools, pluginsSummary } from './plugins.js';
24
+ import { loadCustomCommands, expandCommand, commandsSummary } from './commands.js';
25
+ import { hooksSummary } from './hooks.js';
26
+ import { spawnSubAgent, pickCheapestModel } from './sub-agent.js';
27
+ import { createCheckpoint, undoLastCheckpoint, listCheckpoints } from './git-undo.js';
28
+ import { runIpcServer } from './ipc-server.js';
29
+ function shortModel(m) {
30
+ return m.split('/').pop().replace(/:free$/, '').replace(/-instruct$/, '');
31
+ }
32
+ function parseArgs() {
33
+ const args = process.argv.slice(2);
34
+ let mode = 'interactive';
35
+ let prompt;
36
+ let output;
37
+ let format;
38
+ for (let i = 0; i < args.length; i++) {
39
+ if (args[i] === 'run' && i + 1 < args.length) {
40
+ mode = 'run';
41
+ prompt = args[i + 1];
42
+ i++;
43
+ }
44
+ else if (args[i] === '--output' && i + 1 < args.length) {
45
+ output = args[i + 1];
46
+ i++;
47
+ }
48
+ else if (args[i] === '--format' && i + 1 < args.length) {
49
+ format = args[i + 1];
50
+ i++;
51
+ }
52
+ else if (args[i] === '--pipe' || args[i] === '-p') {
53
+ mode = 'pipe';
54
+ }
55
+ else if (args[i] === '--ipc') {
56
+ mode = 'ipc';
57
+ }
58
+ else if (args[i] === '--help' || args[i] === '-h') {
59
+ console.log(`
60
+ Scalix - Headless Mode
61
+
62
+ Usage:
63
+ scalix Interactive REPL
64
+ scalix run "prompt" Run a single prompt and exit
65
+ scalix --pipe Read prompt from stdin
66
+ scalix --ipc IPC mode (JSONL over stdin/stdout, used by VS Code extension)
67
+ scalix run "prompt" --output file.patch --format patch
68
+
69
+ Options:
70
+ run <prompt> Execute a single prompt non-interactively
71
+ --pipe, -p Read prompt from stdin (for piping)
72
+ --ipc IPC server mode for VS Code extension integration
73
+ --output <file> Write result to file instead of stdout
74
+ --format <fmt> Output format: text (default), json, patch, markdown
75
+ --help, -h Show this help
76
+ `);
77
+ process.exit(0);
78
+ }
79
+ }
80
+ if (!process.stdin.isTTY && mode === 'interactive') {
81
+ mode = 'pipe';
82
+ }
83
+ return { mode, prompt, output, format };
84
+ }
85
+ async function buildEnhancedInstructions() {
86
+ let base = DEFAULT_INSTRUCTIONS;
87
+ const cwd = process.cwd();
88
+ try {
89
+ const memory = await loadMemory(cwd);
90
+ const autoConventions = await autoDetectConventions(cwd);
91
+ for (const c of autoConventions) {
92
+ if (!memory.conventions.some((mc) => mc.key === c.key)) {
93
+ memory.conventions.push(c);
94
+ }
95
+ }
96
+ if (memory.conventions.length > 0 || memory.patterns.length > 0 || memory.notes.length > 0) {
97
+ await saveMemory(cwd, memory);
98
+ }
99
+ }
100
+ catch { }
101
+ const ctx = await buildProjectContext(cwd);
102
+ return contextToInstructions(base, ctx);
103
+ }
104
+ async function runSinglePrompt(prompt, output, format) {
105
+ const apiKey = process.env[ENV_API_KEY];
106
+ if (!apiKey) {
107
+ console.error('Error: SCALIX_API_KEY is not set.\nSet it via: export SCALIX_API_KEY=sk-...\nOr run the interactive setup: scalix');
108
+ process.exit(1);
109
+ }
110
+ const config = await loadConfig();
111
+ const limits = config.limits || DEFAULT_LIMITS;
112
+ const baseUrl = process.env[ENV_BASE_URL] || config.baseUrl;
113
+ // Fetch model specs from API (non-blocking — uses built-in/user-defined as fallback)
114
+ fetchModelSpecs(baseUrl, apiKey).catch(() => { });
115
+ const envModels = process.env[ENV_MODEL]
116
+ ? process.env[ENV_MODEL].split(',').map((m) => m.trim()).filter(Boolean)
117
+ : [];
118
+ const selectedModel = envModels.length > 0 ? envModels
119
+ : config.models.length > 0 ? config.models : DEFAULT_MODELS;
120
+ if (selectedModel.length === 0) {
121
+ console.error('Error: No models configured.\nSet SCALIX_MODEL env var or run interactive setup: scalix');
122
+ process.exit(1);
123
+ }
124
+ const mcpTools = loadMcpServices();
125
+ const plugins = await discoverPlugins(process.cwd());
126
+ const pluginTools = getPluginTools(plugins);
127
+ const allTools = [...defaultTools, ...extraTools, ...webTools, ...advancedTools, ...visionTools, ...voiceTools, ...browserTools, ...mcpTools, ...pluginTools];
128
+ const instructions = await buildEnhancedInstructions();
129
+ const agent = createAgent({
130
+ apiKey,
131
+ baseUrl,
132
+ model: selectedModel,
133
+ instructions,
134
+ tools: allTools,
135
+ permissionMode: config.permissionMode || 'default',
136
+ maxSteps: limits.maxSteps,
137
+ maxMessages: limits.maxMessages,
138
+ contextManager: {
139
+ maxContextTokens: limits.maxContextTokens,
140
+ compressionThreshold: limits.compressionThreshold,
141
+ maxToolResultTokens: limits.maxToolResultTokens,
142
+ },
143
+ instructionLoader: buildEnhancedInstructions,
144
+ autoVerify: true,
145
+ });
146
+ let fullResponse = '';
147
+ agent.on('stream:end', (text) => { fullResponse = text; });
148
+ agent.on('tool:call', (name) => process.stderr.write(`\x1b[33m▶ ${name}\x1b[0m\n`));
149
+ agent.on('verify:pass', (r) => process.stderr.write(`\x1b[32m✓ ${r.checker} passed\x1b[0m\n`));
150
+ agent.on('verify:fail', (r) => process.stderr.write(`\x1b[31m✗ ${r.checker} failed: ${r.output.split('\n')[0]}\x1b[0m\n`));
151
+ agent.on('error', (err) => process.stderr.write(`\x1b[31mError: ${err.message}\x1b[0m\n`));
152
+ try {
153
+ await agent.send(prompt);
154
+ }
155
+ catch (err) {
156
+ console.error(`Error: ${err.message}`);
157
+ process.exit(1);
158
+ }
159
+ let formatted;
160
+ switch (format) {
161
+ case 'json':
162
+ formatted = JSON.stringify({
163
+ prompt,
164
+ response: fullResponse,
165
+ model: selectedModel[0],
166
+ tokens: costTracker.totalTokens,
167
+ cost: costTracker.totalCost,
168
+ }, null, 2);
169
+ break;
170
+ case 'patch':
171
+ formatted = fullResponse;
172
+ break;
173
+ case 'markdown':
174
+ formatted = fullResponse;
175
+ break;
176
+ default:
177
+ formatted = fullResponse;
178
+ }
179
+ if (output) {
180
+ await fs.writeFile(output, formatted, 'utf-8');
181
+ process.stderr.write(`Output written to ${output}\n`);
182
+ }
183
+ else {
184
+ process.stdout.write(formatted + '\n');
185
+ }
186
+ process.exit(0);
187
+ }
188
+ async function runPipeMode() {
189
+ const MAX_PIPE_BYTES = 50 * 1024 * 1024;
190
+ const chunks = [];
191
+ 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);
198
+ }
199
+ chunks.push(str);
200
+ }
201
+ const prompt = chunks.join('').trim();
202
+ if (!prompt) {
203
+ console.error('Error: No input received from stdin.');
204
+ process.exit(1);
205
+ }
206
+ await runSinglePrompt(prompt);
207
+ }
208
+ async function runInteractive() {
209
+ const apiKey = process.env[ENV_API_KEY];
210
+ if (!apiKey) {
211
+ throw new Error('SCALIX_API_KEY is not set. Set it via: export SCALIX_API_KEY=sk-...');
212
+ }
213
+ const config = await loadConfig();
214
+ const limits = config.limits || DEFAULT_LIMITS;
215
+ const baseUrl = process.env[ENV_BASE_URL] || config.baseUrl;
216
+ // Fetch model specs from API (non-blocking)
217
+ fetchModelSpecs(baseUrl, apiKey).catch(() => { });
218
+ const envModels = process.env[ENV_MODEL]
219
+ ? process.env[ENV_MODEL].split(',').map((m) => m.trim()).filter(Boolean)
220
+ : [];
221
+ const selectedModel = envModels.length > 0 ? envModels
222
+ : config.models.length > 0 ? config.models : DEFAULT_MODELS;
223
+ if (selectedModel.length === 0) {
224
+ throw new Error('No models configured. Set SCALIX_MODEL env var or run interactive setup.');
225
+ }
226
+ const mcpTools = loadMcpServices();
227
+ const plugins = await discoverPlugins(process.cwd());
228
+ const pluginTools = getPluginTools(plugins);
229
+ const allTools = [...defaultTools, ...extraTools, ...webTools, ...advancedTools, ...visionTools, ...voiceTools, ...browserTools, ...mcpTools, ...pluginTools];
230
+ const instructions = await buildEnhancedInstructions();
231
+ const rl = readline.createInterface({
232
+ input: process.stdin,
233
+ output: process.stdout,
234
+ });
235
+ function askUser(question) {
236
+ return new Promise((resolve) => rl.question(question, resolve));
237
+ }
238
+ const customCmds = await loadCustomCommands(process.cwd());
239
+ const agent = createAgent({
240
+ apiKey,
241
+ baseUrl,
242
+ model: selectedModel,
243
+ instructions,
244
+ tools: allTools,
245
+ permissionMode: config.permissionMode || 'default',
246
+ maxSteps: limits.maxSteps,
247
+ maxMessages: limits.maxMessages,
248
+ contextManager: {
249
+ maxContextTokens: limits.maxContextTokens,
250
+ compressionThreshold: limits.compressionThreshold,
251
+ maxToolResultTokens: limits.maxToolResultTokens,
252
+ },
253
+ instructionLoader: buildEnhancedInstructions,
254
+ autoVerify: true,
255
+ confirmToolCall: async (toolName, args) => {
256
+ const preview = typeof args === 'string' ? args : JSON.stringify(args, null, 2);
257
+ console.log(`\n\u26A0 Tool "${toolName}" wants to execute:`);
258
+ console.log(` ${preview.split('\n').join('\n ')}`);
259
+ const answer = await askUser(' Allow? (y/N): ');
260
+ return answer.trim().toLowerCase() === 'y';
261
+ },
262
+ });
263
+ let sessionId = generateSessionId();
264
+ agent.on('thinking:start', () => process.stdout.write('\n\x1b[33mThinking...\x1b[0m'));
265
+ agent.on('tool:call', (name, args) => {
266
+ const argStr = typeof args === 'string' ? args : JSON.stringify(args);
267
+ process.stderr.write(`\x1b[33m\u25B6 ${name}\x1b[0m ${argStr}\n`);
268
+ });
269
+ agent.on('tool:blocked', (name, reason) => console.log(`\x1b[31m\u2717 ${name}: ${reason}\x1b[0m`));
270
+ agent.on('stream:delta', (delta) => process.stdout.write(delta));
271
+ agent.on('stream:end', () => {
272
+ process.stdout.write('\n\n');
273
+ });
274
+ agent.on('tool:result', (name, result) => {
275
+ let text = typeof result === 'string' ? result : JSON.stringify(result);
276
+ if (text.length > 500) {
277
+ text = text.slice(0, 500) + `... (${(text.length - 500).toLocaleString()} more chars)`;
278
+ }
279
+ process.stderr.write(`\x1b[90m \u2514 ${name}: ${text.split('\n')[0]}\x1b[0m\n`);
280
+ });
281
+ agent.on('usage', (data) => {
282
+ costTracker.record(data.model || selectedModel[0], data);
283
+ });
284
+ agent.on('error', (err) => console.error('\x1b[31mError:\x1b[0m', err.message));
285
+ agent.on('context:compress', (info) => {
286
+ console.log(`\x1b[36m\u25CB Context compressed: ${info.saved} tokens saved (${info.before} \u2192 ${info.after})\x1b[0m`);
287
+ });
288
+ agent.on('context:usage', (info) => {
289
+ if (info.percentage > 80) {
290
+ console.log(`\x1b[33m\u26A0 Context usage: ${info.percentage}% (${info.tokens}/${info.maxTokens} tokens)\x1b[0m`);
291
+ }
292
+ });
293
+ const toolCount = allTools.length;
294
+ const modelDisplay = selectedModel.map(shortModel).join(' | ');
295
+ console.log(`\x1b[36mAgent ready\x1b[0m (${modelDisplay}) with ${toolCount} tools. Type /help for commands.`);
296
+ if (plugins.length > 0) {
297
+ console.log(`\x1b[36mPlugins:\x1b[0m ${plugins.map((p) => p.manifest.name).join(', ')}`);
298
+ }
299
+ console.log('');
300
+ function shutdown() {
301
+ const msgs = agent.getMessages();
302
+ if (msgs.length > 0) {
303
+ saveSession(sessionId, msgs, agent.getModels()[0], undefined, costTracker.getState()).catch(() => { });
304
+ }
305
+ if (costTracker.totalTokens > 0) {
306
+ console.log(`\n\x1b[36mSession: ${costTracker.totalTokens.toLocaleString()} tokens${costTracker.totalCost > 0 ? ` ($${costTracker.totalCost.toFixed(4)})` : ''}\x1b[0m`);
307
+ }
308
+ console.log('Goodbye!');
309
+ rl.close();
310
+ process.exit(0);
311
+ }
312
+ process.on('SIGINT', shutdown);
313
+ process.on('SIGTERM', shutdown);
314
+ const handleCommand = async (input) => {
315
+ const cmd = input.split(' ')[0].toLowerCase();
316
+ if (cmd === '/help') {
317
+ console.log('\nCommands:');
318
+ console.log(' /help \u2014 show this help');
319
+ console.log(' /clear \u2014 reset conversation');
320
+ console.log(' /models \u2014 list active models');
321
+ console.log(' /model <name>\u2014 switch model');
322
+ console.log(' /mode [mode] \u2014 permission mode (plan/default/auto/yolo)');
323
+ console.log(' /compact \u2014 compress conversation context');
324
+ console.log(' /cost \u2014 show token usage');
325
+ console.log(' /save [name] \u2014 save session');
326
+ console.log(' /load [id] \u2014 resume a saved session');
327
+ console.log(' /sessions \u2014 list saved sessions');
328
+ console.log(' /plugins \u2014 show loaded plugins');
329
+ console.log(' /hooks \u2014 show lifecycle hooks');
330
+ console.log(' /commands \u2014 show custom commands');
331
+ console.log(' /delegate <t>\u2014 run task with sub-agent');
332
+ console.log(' /checkpoint \u2014 create git checkpoint');
333
+ console.log(' /undo \u2014 undo last checkpoint');
334
+ console.log(' /checkpoints \u2014 list recent checkpoints');
335
+ console.log(' /memory \u2014 show project memory');
336
+ console.log(' /remember <n>\u2014 save a note about this project');
337
+ console.log(' /git \u2014 show git context');
338
+ console.log(' /index \u2014 scan & index project files');
339
+ console.log(' /quit \u2014 exit');
340
+ console.log('');
341
+ console.log('Input modes:');
342
+ console.log(' !<cmd> \u2014 run shell command (e.g. !ls -la)');
343
+ console.log(' text\\ \u2014 multiline input (end with \\ to continue)\n');
344
+ return true;
345
+ }
346
+ if (cmd === '/clear') {
347
+ const msgs = agent.getMessages();
348
+ if (msgs.length > 0) {
349
+ await saveSession(sessionId, msgs, agent.getModels()[0], undefined, costTracker.getState()).catch(() => { });
350
+ console.log('(auto-saved before clearing)');
351
+ }
352
+ agent.clearHistory();
353
+ costTracker.reset();
354
+ sessionId = generateSessionId();
355
+ console.log('\nConversation cleared.\n');
356
+ return true;
357
+ }
358
+ if (cmd === '/models') {
359
+ console.log(`\nActive: ${agent.getModels().map(shortModel).join(', ')}\n`);
360
+ return true;
361
+ }
362
+ if (cmd === '/model') {
363
+ const name = input.slice(7).trim();
364
+ if (!name) {
365
+ console.log('Usage: /model <model-name>');
366
+ return true;
367
+ }
368
+ if (!/^[a-zA-Z0-9][\w./:@-]*$/.test(name)) {
369
+ console.log(`\nInvalid model name: "${name}"\n`);
370
+ return true;
371
+ }
372
+ agent.setModel(name);
373
+ console.log(`\nSwitched to ${shortModel(name)}\n`);
374
+ return true;
375
+ }
376
+ if (cmd === '/compact') {
377
+ console.log('\nCompacting...');
378
+ const result = await agent.compact();
379
+ console.log(result + '\n');
380
+ return true;
381
+ }
382
+ if (cmd === '/cost') {
383
+ console.log('\n' + costTracker.summary() + '\n');
384
+ return true;
385
+ }
386
+ if (cmd === '/save') {
387
+ const name = input.slice(5).trim() || undefined;
388
+ try {
389
+ const meta = await saveSession(sessionId, agent.getMessages(), agent.getModels()[0], name, costTracker.getState());
390
+ console.log(`\nSession saved: ${meta.name} (${meta.messageCount} messages)\n`);
391
+ }
392
+ catch (e) {
393
+ console.log(`\nFailed to save session: ${e.message}\n`);
394
+ }
395
+ return true;
396
+ }
397
+ if (cmd === '/sessions' || cmd === '/list') {
398
+ const sessions = await listSessions();
399
+ if (sessions.length === 0) {
400
+ console.log('\nNo saved sessions.\n');
401
+ return true;
402
+ }
403
+ console.log('\nSaved sessions (use /load <id> to resume):');
404
+ for (const s of sessions.slice(0, 10)) {
405
+ console.log(` ${s.id.slice(0, 8)} \u2014 ${s.name} (${s.messageCount} msgs, ${new Date(s.updatedAt).toLocaleDateString()})`);
406
+ }
407
+ console.log('');
408
+ return true;
409
+ }
410
+ if (cmd === '/load') {
411
+ const idPrefix = input.slice(5).trim();
412
+ if (!idPrefix) {
413
+ const sessions = await listSessions();
414
+ if (sessions.length === 0) {
415
+ console.log('\nNo saved sessions.\n');
416
+ return true;
417
+ }
418
+ console.log('\nSaved sessions (use /load <id> to resume):');
419
+ for (const s of sessions.slice(0, 10)) {
420
+ console.log(` ${s.id.slice(0, 8)} \u2014 ${s.name} (${s.messageCount} msgs)`);
421
+ }
422
+ console.log('');
423
+ return true;
424
+ }
425
+ const sessions = await listSessions();
426
+ const match = sessions.find((s) => s.id.startsWith(idPrefix));
427
+ if (!match) {
428
+ console.log(`\nNo session found starting with "${idPrefix}".\n`);
429
+ return true;
430
+ }
431
+ try {
432
+ const data = await loadSession(match.id);
433
+ agent.loadMessages(data.messages);
434
+ sessionId = match.id;
435
+ agent.setModel(match.model);
436
+ if (data.costState)
437
+ costTracker.restore(data.costState);
438
+ console.log(`\nResumed: ${match.name} (${match.messageCount} messages)\n`);
439
+ }
440
+ catch (e) {
441
+ console.log(`\nFailed to load session: ${e.message}\n`);
442
+ }
443
+ return true;
444
+ }
445
+ if (cmd === '/mode') {
446
+ const arg = input.slice(6).trim().toLowerCase();
447
+ if (arg && PERMISSION_MODES.includes(arg)) {
448
+ agent.setPermissionMode(arg);
449
+ console.log(`\nMode: ${arg.toUpperCase()} \u2014 ${getModeDescription(arg)}\n`);
450
+ }
451
+ else {
452
+ const current = agent.getPermissionMode();
453
+ console.log(`\nCurrent mode: ${current.toUpperCase()}`);
454
+ for (const m of PERMISSION_MODES) {
455
+ console.log(` ${m === current ? '\u25CF' : '\u25CB'} ${m.padEnd(8)} \u2014 ${getModeDescription(m)}`);
456
+ }
457
+ console.log('');
458
+ }
459
+ return true;
460
+ }
461
+ if (cmd === '/plugins') {
462
+ console.log('\n' + pluginsSummary(plugins) + '\n');
463
+ return true;
464
+ }
465
+ if (cmd === '/hooks') {
466
+ console.log('\n' + hooksSummary(agent.getHooks()) + '\n');
467
+ return true;
468
+ }
469
+ if (cmd === '/commands') {
470
+ console.log('\n' + commandsSummary(customCmds) + '\n');
471
+ return true;
472
+ }
473
+ if (cmd === '/delegate') {
474
+ const prompt = input.slice(10).trim();
475
+ if (!prompt) {
476
+ console.log('Usage: /delegate <task>');
477
+ return true;
478
+ }
479
+ const cheapModel = pickCheapestModel(agent.getModels());
480
+ console.log(`\nDelegating to sub-agent (${shortModel(cheapModel)})...`);
481
+ try {
482
+ const result = await spawnSubAgent({ apiKey: apiKey, baseUrl, models: agent.getModels() }, prompt, { model: cheapModel, tools: 'read-only', maxSteps: 10 });
483
+ console.log(`\n${result.response}\n`);
484
+ console.log(`\x1b[36mSub-agent: ${result.steps} steps, ~${result.tokenEstimate} tokens\x1b[0m\n`);
485
+ }
486
+ catch (err) {
487
+ console.log(`\nSub-agent failed: ${err.message}\n`);
488
+ }
489
+ return true;
490
+ }
491
+ if (cmd === '/checkpoint') {
492
+ const label = input.slice(12).trim() || undefined;
493
+ const hash = await createCheckpoint(process.cwd(), label);
494
+ if (hash)
495
+ console.log(`\nCheckpoint created: ${hash}${label ? ` (${label})` : ''}\n`);
496
+ else
497
+ console.log('\nNo changes to checkpoint (or not a git repo).\n');
498
+ return true;
499
+ }
500
+ if (cmd === '/undo') {
501
+ const r = await undoLastCheckpoint(process.cwd());
502
+ console.log(`\n${r.message}\n`);
503
+ return true;
504
+ }
505
+ if (cmd === '/checkpoints') {
506
+ const list = await listCheckpoints(process.cwd());
507
+ if (list.length === 0)
508
+ console.log('\nNo checkpoints found.\n');
509
+ else {
510
+ console.log('\nRecent checkpoints:');
511
+ list.forEach((l) => console.log(` ${l}`));
512
+ console.log('');
513
+ }
514
+ return true;
515
+ }
516
+ if (cmd === '/memory') {
517
+ const mem = await loadMemory(process.cwd());
518
+ if (mem.conventions.length === 0 && mem.patterns.length === 0 && mem.notes.length === 0) {
519
+ console.log('\nNo project memory yet. Use /remember to add notes.\n');
520
+ }
521
+ else {
522
+ console.log('\n' + memoryToContext(mem) + '\n');
523
+ }
524
+ return true;
525
+ }
526
+ if (cmd === '/remember') {
527
+ const note = input.slice(10).trim();
528
+ if (!note) {
529
+ console.log('Usage: /remember <note about this project>');
530
+ return true;
531
+ }
532
+ try {
533
+ await addNote(process.cwd(), note);
534
+ console.log(`\nRemembered: ${note}\n`);
535
+ }
536
+ catch {
537
+ console.log('\nFailed to save note.\n');
538
+ }
539
+ return true;
540
+ }
541
+ if (cmd === '/git') {
542
+ const gitInfo = collectGitInfo(process.cwd());
543
+ if (!gitInfo.isRepo) {
544
+ console.log('\nNot a git repository.\n');
545
+ }
546
+ else {
547
+ console.log('\n' + gitInfoToContext(gitInfo) + '\n');
548
+ }
549
+ return true;
550
+ }
551
+ if (cmd === '/index') {
552
+ console.log('\nScanning project...');
553
+ try {
554
+ const idx = await buildProjectIndex(process.cwd());
555
+ console.log(idx.summary);
556
+ console.log(`Tech stack: ${idx.techStack.join(', ') || 'Unknown'}`);
557
+ console.log(`Top symbols: ${idx.symbols.slice(0, 10).map((s) => s.name).join(', ')}\n`);
558
+ }
559
+ catch (err) {
560
+ console.log(`Index failed: ${err.message}\n`);
561
+ }
562
+ return true;
563
+ }
564
+ if (cmd === '/quit' || cmd === '/exit') {
565
+ shutdown();
566
+ return true;
567
+ }
568
+ const customCmd = customCmds.find((c) => cmd === `/${c.name}`);
569
+ if (customCmd) {
570
+ const args = input.slice(cmd.length).trim();
571
+ const expanded = expandCommand(customCmd, args);
572
+ console.log(`\nRunning: /${customCmd.name}`);
573
+ try {
574
+ await agent.send(expanded);
575
+ }
576
+ catch (err) {
577
+ console.log(`\nFailed: ${err.message}\n`);
578
+ }
579
+ return true;
580
+ }
581
+ return false;
582
+ };
583
+ const promptLoop = async () => {
584
+ while (true) {
585
+ let input = await askUser('\x1b[34mYou:\x1b[0m ');
586
+ let continuations = 0;
587
+ while (input.endsWith('\\') && continuations < 50) {
588
+ input = input.slice(0, -1) + '\n' + await askUser('\x1b[34m...\x1b[0m ');
589
+ continuations++;
590
+ }
591
+ if (!input.trim())
592
+ continue;
593
+ if (input.startsWith('/') && await handleCommand(input.trim()))
594
+ continue;
595
+ if (input.trim().startsWith('!')) {
596
+ const shellCmd = input.trim().slice(1).trim();
597
+ if (shellCmd) {
598
+ try {
599
+ let output = execSync(shellCmd, { encoding: 'utf-8', timeout: HEADLESS_DEFAULTS.commandTimeoutMs, cwd: process.cwd() }).trim();
600
+ if (output) {
601
+ if (output.length > HEADLESS_DEFAULTS.maxOutputTruncation) {
602
+ output = output.slice(0, HEADLESS_DEFAULTS.maxOutputTruncation) + `\n... (${(output.length - HEADLESS_DEFAULTS.maxOutputTruncation).toLocaleString()} chars truncated)`;
603
+ }
604
+ console.log(output);
605
+ }
606
+ }
607
+ catch (err) {
608
+ console.error(err.stderr?.trim() || err.message);
609
+ }
610
+ }
611
+ continue;
612
+ }
613
+ const correction = detectCorrection(input.trim());
614
+ if (correction)
615
+ addNote(process.cwd(), `[correction] ${correction}`).catch(() => { });
616
+ try {
617
+ await agent.send(input);
618
+ }
619
+ catch (err) {
620
+ logger.error('Send failed', { error: err.message });
621
+ }
622
+ }
623
+ };
624
+ promptLoop().catch((err) => {
625
+ if (err.code !== 'ERR_USE_AFTER_CLOSE') {
626
+ console.error('Fatal error:', err);
627
+ process.exit(1);
628
+ }
629
+ });
630
+ }
631
+ async function main() {
632
+ setSandboxRoot(process.cwd());
633
+ const { mode, prompt, output, format } = parseArgs();
634
+ switch (mode) {
635
+ case 'run':
636
+ if (!prompt) {
637
+ console.error('Error: No prompt provided. Usage: scalix run "your prompt"');
638
+ process.exit(1);
639
+ }
640
+ await runSinglePrompt(prompt, output, format);
641
+ break;
642
+ case 'pipe':
643
+ await runPipeMode();
644
+ break;
645
+ case 'ipc':
646
+ await runIpcServer();
647
+ break;
648
+ case 'interactive':
649
+ default:
650
+ await runInteractive();
651
+ break;
652
+ }
653
+ }
654
+ main().catch(console.error);