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
package/dist/tools.js ADDED
@@ -0,0 +1,370 @@
1
+ import { tool } from './tool-helper.js';
2
+ import { z } from 'zod';
3
+ import { exec, spawn } from 'node:child_process';
4
+ import { promisify } from 'node:util';
5
+ import fsSync from 'node:fs';
6
+ import fs from 'node:fs/promises';
7
+ import path from 'node:path';
8
+ import { safeEvaluate } from './safe-math.js';
9
+ import { sandboxPath, isBlockedCommand, detectInjection, sanitizeEnvForCommand, getShellInfo } from './sandbox.js';
10
+ import { DEFAULT_LIMITS } from './settings.js';
11
+ import { TOOL_DEFAULTS, BINARY_DETECT_BUFFER_SIZE } from './constants.js';
12
+ import { computeDiff, formatDiff } from './diff.js';
13
+ let _limits = DEFAULT_LIMITS;
14
+ export function setToolLimits(limits) { _limits = limits; }
15
+ const execAsync = promisify(exec);
16
+ let persistentCwd = process.cwd();
17
+ export function getPersistentCwd() { return persistentCwd; }
18
+ export function setPersistentCwd(dir) { persistentCwd = dir; }
19
+ export function resetPersistentCwd() { persistentCwd = process.cwd(); }
20
+ export const backgroundProcesses = new Map();
21
+ export const timeTool = tool({
22
+ name: 'get_current_time',
23
+ description: 'Get the current date and time',
24
+ inputSchema: z.object({
25
+ timezone: z.string().max(100).optional().describe('Timezone (e.g., "UTC", "America/New_York")')
26
+ }),
27
+ execute: async ({ timezone }) => {
28
+ const tz = timezone || 'UTC';
29
+ try {
30
+ return {
31
+ time: new Date().toLocaleString('en-US', { timeZone: tz }),
32
+ timezone: tz,
33
+ };
34
+ }
35
+ catch {
36
+ return { error: `Invalid timezone: "${tz}". Use IANA names like "America/New_York" or "UTC".` };
37
+ }
38
+ }
39
+ });
40
+ export const calculatorTool = tool({
41
+ name: 'calculate',
42
+ description: 'Perform mathematical calculations. Supports +, -, *, /, %, ^, parentheses, and functions (sqrt, abs, sin, cos, tan, log, floor, ceil, round, min, max, pow, pi, e)',
43
+ inputSchema: z.object({
44
+ expression: z.string().max(1000).describe('Math expression (e.g., "2 + 2", "sqrt(16)")')
45
+ }),
46
+ execute: async ({ expression }) => {
47
+ try {
48
+ const result = safeEvaluate(expression);
49
+ return { expression, result };
50
+ }
51
+ catch (err) {
52
+ return { expression, error: err.message };
53
+ }
54
+ }
55
+ });
56
+ // Extract cd target from a command to persist working directory
57
+ function extractCdTarget(command) {
58
+ const match = command.match(/^\s*cd\s+("([^"]+)"|'([^']+)'|(\S+))\s*(&&|;|$)/);
59
+ if (match)
60
+ return match[2] || match[3] || match[4] || null;
61
+ return null;
62
+ }
63
+ export const runCommandTool = tool({
64
+ name: 'run_command',
65
+ description: 'Run a shell command using the system shell (bash/zsh on Unix, PowerShell/cmd on Windows). Working directory persists between calls. Supports background execution and configurable timeout.',
66
+ inputSchema: z.object({
67
+ command: z.string().max(10000).describe('The command to run'),
68
+ description: z.string().max(500).optional().describe('Short description of what the command does'),
69
+ timeout: z.number().max(TOOL_DEFAULTS.maxCommandTimeoutMs).optional().describe(`Timeout in ms (default ${TOOL_DEFAULTS.commandTimeoutMs}, max ${TOOL_DEFAULTS.maxCommandTimeoutMs})`),
70
+ cwd: z.string().max(500).optional().describe('Working directory override (relative to workspace root)'),
71
+ run_in_background: z.boolean().optional().describe('Run command in background and return PID immediately'),
72
+ }),
73
+ execute: async ({ command, timeout, cwd, run_in_background: runInBg }) => {
74
+ if (isBlockedCommand(command)) {
75
+ return { error: 'Command blocked: matches a destructive pattern', exitCode: -1 };
76
+ }
77
+ const injection = detectInjection(command);
78
+ if (injection) {
79
+ return { error: `Command blocked: ${injection}`, exitCode: -1, requiresReview: true };
80
+ }
81
+ // Resolve working directory: explicit cwd > persistent cwd > process cwd
82
+ let workingDir;
83
+ if (cwd) {
84
+ try {
85
+ workingDir = sandboxPath(cwd);
86
+ }
87
+ catch (err) {
88
+ return { error: `Invalid working directory: ${err.message}`, exitCode: -1 };
89
+ }
90
+ }
91
+ else {
92
+ workingDir = persistentCwd;
93
+ }
94
+ const shell = getShellInfo();
95
+ if (runInBg) {
96
+ const child = spawn(shell.path, [...shell.args, command], {
97
+ detached: true,
98
+ stdio: ['ignore', 'pipe', 'pipe'],
99
+ env: sanitizeEnvForCommand(),
100
+ cwd: workingDir,
101
+ });
102
+ if (!child.pid) {
103
+ return { error: 'Failed to spawn background process: no PID assigned', exitCode: -1 };
104
+ }
105
+ const pid = child.pid;
106
+ const entry = {
107
+ pid,
108
+ command,
109
+ stdout: '',
110
+ stderr: '',
111
+ exitCode: null,
112
+ startedAt: Date.now(),
113
+ lastRead: 0,
114
+ };
115
+ child.stdout?.on('data', (data) => {
116
+ const chunk = data.toString();
117
+ if (entry.stdout.length + chunk.length <= _limits.bgBufferMax) {
118
+ entry.stdout += chunk;
119
+ }
120
+ else if (entry.stdout.length < _limits.bgBufferMax) {
121
+ entry.stdout += chunk.slice(0, _limits.bgBufferMax - entry.stdout.length);
122
+ }
123
+ });
124
+ child.stdout?.on('error', () => { });
125
+ child.stderr?.on('data', (data) => {
126
+ const chunk = data.toString();
127
+ if (entry.stderr.length + chunk.length <= _limits.bgBufferMax) {
128
+ entry.stderr += chunk;
129
+ }
130
+ else if (entry.stderr.length < _limits.bgBufferMax) {
131
+ entry.stderr += chunk.slice(0, _limits.bgBufferMax - entry.stderr.length);
132
+ }
133
+ });
134
+ child.stderr?.on('error', () => { });
135
+ child.on('error', (err) => {
136
+ entry.exitCode = -1;
137
+ entry.stderr += `\nProcess error: ${err.message}`;
138
+ });
139
+ child.on('close', (code) => {
140
+ entry.exitCode = code ?? 1;
141
+ setTimeout(() => { backgroundProcesses.delete(pid); }, _limits.bgCleanupMs).unref();
142
+ });
143
+ backgroundProcesses.set(pid, entry);
144
+ child.unref();
145
+ return { pid, message: `Command started in background (PID: ${pid}). Use the monitor tool to check output.` };
146
+ }
147
+ // For cd-only commands, update persistent cwd directly
148
+ const cdTarget = extractCdTarget(command);
149
+ if (cdTarget && command.trim().match(/^cd\s/)) {
150
+ try {
151
+ const resolved = path.resolve(workingDir, cdTarget);
152
+ await fs.stat(resolved);
153
+ persistentCwd = resolved;
154
+ return { stdout: '', stderr: '', exitCode: 0, cwd: persistentCwd };
155
+ }
156
+ catch (err) {
157
+ return { error: `cd: ${cdTarget}: ${err.code === 'ENOENT' ? 'No such file or directory' : err.message}`, exitCode: 1 };
158
+ }
159
+ }
160
+ let fullCommand = command;
161
+ const CWD_MARKER = '__SCALIX_CWD_PROBE__';
162
+ if (cdTarget && !command.trim().match(/^cd\s+\S+\s*$/)) {
163
+ const cwdProbe = shell.type === 'cmd'
164
+ ? `${command} && echo ${CWD_MARKER} && cd`
165
+ : shell.type === 'pwsh'
166
+ ? `${command}; Write-Host '${CWD_MARKER}'; Get-Location | Select-Object -ExpandProperty Path`
167
+ : `${command} && echo '${CWD_MARKER}' && pwd`;
168
+ fullCommand = cwdProbe;
169
+ }
170
+ try {
171
+ const { stdout, stderr } = await execAsync(fullCommand, {
172
+ timeout: timeout ?? _limits.commandTimeoutMs,
173
+ maxBuffer: TOOL_DEFAULTS.maxExecBufferDefault,
174
+ env: sanitizeEnvForCommand(),
175
+ cwd: workingDir,
176
+ shell: shell.path,
177
+ });
178
+ if (cdTarget && fullCommand !== command) {
179
+ const markerIdx = stdout.indexOf(CWD_MARKER);
180
+ if (markerIdx !== -1) {
181
+ const beforeMarker = stdout.slice(0, markerIdx).trimEnd();
182
+ const afterMarker = stdout.slice(markerIdx + CWD_MARKER.length).trim();
183
+ const newCwd = afterMarker.split('\n')[0]?.trim();
184
+ if (newCwd && path.isAbsolute(newCwd)) {
185
+ persistentCwd = newCwd;
186
+ return { stdout: beforeMarker, stderr, exitCode: 0, cwd: persistentCwd };
187
+ }
188
+ }
189
+ }
190
+ return { stdout, stderr, exitCode: 0, cwd: workingDir };
191
+ }
192
+ catch (error) {
193
+ const result = { stdout: error.stdout, stderr: error.stderr, exitCode: error.code || 1, error: error.message };
194
+ result.autoFixHint = 'The command failed. Analyze the error output above and suggest how to fix it. If it is a build/test/lint error, read the relevant file and fix the issue.';
195
+ return result;
196
+ }
197
+ }
198
+ });
199
+ export const readFileTool = tool({
200
+ name: 'read_file',
201
+ description: 'Read a local file with line numbers (lineN<tab>content). Returns the full file contents. Use read_file_lines if you only need a specific range.',
202
+ inputSchema: z.object({
203
+ filePath: z.string().max(500).describe('Relative path to the file'),
204
+ maxLines: z.number().min(1).optional().describe('Optional: limit output to first N lines'),
205
+ }),
206
+ execute: async ({ filePath, maxLines }) => {
207
+ try {
208
+ const safePath = sandboxPath(filePath);
209
+ const stat = await fs.stat(safePath);
210
+ if (stat.size > _limits.readFileMaxSize)
211
+ return { error: `File too large (${Math.round(stat.size / 1024)}KB, max ${Math.round(_limits.readFileMaxSize / 1024)}KB). Use read_file_lines to read specific sections.` };
212
+ const fd = await fs.open(safePath, 'r');
213
+ const header = Buffer.alloc(BINARY_DETECT_BUFFER_SIZE);
214
+ await fd.read(header, 0, Math.min(BINARY_DETECT_BUFFER_SIZE, stat.size), 0);
215
+ await fd.close();
216
+ if (header.slice(0, Math.min(BINARY_DETECT_BUFFER_SIZE, stat.size)).includes(0x00)) {
217
+ return { error: 'File appears to be binary. Use a specialized tool to inspect binary files.' };
218
+ }
219
+ const content = await fs.readFile(safePath, 'utf-8');
220
+ const lines = content.split('\n');
221
+ if (maxLines && lines.length > maxLines) {
222
+ const numbered = lines.slice(0, maxLines).map((line, i) => `${i + 1}\t${line}`).join('\n');
223
+ return {
224
+ content: numbered,
225
+ totalLines: lines.length,
226
+ truncatedAt: maxLines,
227
+ hint: `Showing first ${maxLines} of ${lines.length} lines.`,
228
+ };
229
+ }
230
+ const numbered = lines.map((line, i) => `${i + 1}\t${line}`).join('\n');
231
+ return { content: numbered, totalLines: lines.length };
232
+ }
233
+ catch (error) {
234
+ return { error: error.message };
235
+ }
236
+ }
237
+ });
238
+ export const writeFileTool = tool({
239
+ name: 'write_file',
240
+ description: 'Write text content to a local file (sandboxed to working directory). Requires user confirmation.',
241
+ inputSchema: z.object({
242
+ filePath: z.string().max(500).describe('Relative path to the file'),
243
+ content: z.string().max(1024 * 1024).describe('The entire text content to write')
244
+ }),
245
+ execute: async ({ filePath, content }) => {
246
+ try {
247
+ const safePath = sandboxPath(filePath);
248
+ let existed = false;
249
+ let oldContent = '';
250
+ try {
251
+ oldContent = await fs.readFile(safePath, 'utf-8');
252
+ existed = true;
253
+ }
254
+ catch { }
255
+ await fs.mkdir(path.dirname(safePath), { recursive: true });
256
+ await fs.writeFile(safePath, content, 'utf-8');
257
+ let diff;
258
+ if (existed) {
259
+ const diffResult = computeDiff(oldContent, content);
260
+ diff = formatDiff(diffResult, filePath);
261
+ }
262
+ return {
263
+ success: true,
264
+ message: `Wrote ${content.length} bytes to ${filePath}`,
265
+ ...(existed ? { warning: 'File already existed and was overwritten' } : {}),
266
+ ...(diff ? { diff } : {}),
267
+ };
268
+ }
269
+ catch (error) {
270
+ return { success: false, error: error.message };
271
+ }
272
+ }
273
+ });
274
+ export const listFilesTool = tool({
275
+ name: 'list_files',
276
+ description: 'List files and directories in a given path (sandboxed to working directory). Supports recursive listing with glob patterns.',
277
+ inputSchema: z.object({
278
+ dirPath: z.string().max(500).optional().describe('Relative path (defaults to current directory)'),
279
+ recursive: z.boolean().optional().describe('Recursively list files (default: false)'),
280
+ pattern: z.string().max(200).optional().describe('Glob pattern filter when recursive (e.g., "**/*.ts")'),
281
+ maxResults: z.number().max(1000).optional().describe('Max results for recursive listing (default: 200)'),
282
+ }),
283
+ execute: async ({ dirPath, recursive, pattern, maxResults }) => {
284
+ try {
285
+ const safePath = sandboxPath(dirPath || '.');
286
+ if (recursive) {
287
+ const fg = (await import('fast-glob')).default;
288
+ const globPattern = pattern || '**/*';
289
+ const files = await fg(globPattern, {
290
+ cwd: safePath,
291
+ ignore: ['node_modules/**', '.git/**', 'dist/**', 'coverage/**'],
292
+ onlyFiles: true,
293
+ stats: true,
294
+ followSymbolicLinks: false,
295
+ });
296
+ const limit = maxResults ?? 200;
297
+ const sorted = files
298
+ .sort((a, b) => (b.stats?.mtimeMs ?? 0) - (a.stats?.mtimeMs ?? 0))
299
+ .slice(0, limit)
300
+ .map((f) => ({
301
+ name: f.path,
302
+ size: f.stats?.size ?? 0,
303
+ isDirectory: false,
304
+ }));
305
+ return { path: safePath, count: sorted.length, total: files.length, files: sorted };
306
+ }
307
+ const entries = await fs.readdir(safePath, { withFileTypes: true });
308
+ const results = await Promise.all(entries.map(async (e) => {
309
+ let size;
310
+ if (!e.isDirectory()) {
311
+ try {
312
+ const stat = await fs.stat(path.join(safePath, e.name));
313
+ size = stat.size;
314
+ }
315
+ catch { }
316
+ }
317
+ return { name: e.name, isDirectory: e.isDirectory(), ...(size !== undefined ? { size } : {}) };
318
+ }));
319
+ const sorted = results.sort((a, b) => {
320
+ if (a.isDirectory !== b.isDirectory)
321
+ return a.isDirectory ? -1 : 1;
322
+ return a.name.localeCompare(b.name);
323
+ });
324
+ return { path: safePath, count: sorted.length, files: sorted };
325
+ }
326
+ catch (error) {
327
+ return { error: error.message };
328
+ }
329
+ }
330
+ });
331
+ export const addMcpServerTool = tool({
332
+ name: 'add_mcp_server',
333
+ description: 'Add an HTTP MCP server persistently. Requires user confirmation.',
334
+ inputSchema: z.object({
335
+ label: z.string().min(1).max(100).describe('A logical name for the MCP server'),
336
+ url: z.string().url().describe('The HTTP SSE URL of the MCP server')
337
+ }),
338
+ execute: async ({ label, url }) => {
339
+ try {
340
+ const mcpConfigPath = path.resolve(process.cwd(), 'mcp-services.json');
341
+ let config = { mcpServers: [] };
342
+ try {
343
+ if (fsSync.existsSync(mcpConfigPath)) {
344
+ const parsed = JSON.parse(fsSync.readFileSync(mcpConfigPath, 'utf-8'));
345
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
346
+ config = parsed;
347
+ if (!Array.isArray(config.mcpServers))
348
+ config.mcpServers = [];
349
+ }
350
+ }
351
+ }
352
+ catch { }
353
+ config.mcpServers.push({ serverLabel: label, serverUrl: url });
354
+ await fs.writeFile(mcpConfigPath, JSON.stringify(config, null, 2), 'utf-8');
355
+ return { success: true, message: `Added MCP server ${label} at ${url}` };
356
+ }
357
+ catch (e) {
358
+ return { success: false, error: e.message };
359
+ }
360
+ }
361
+ });
362
+ export const defaultTools = [
363
+ timeTool,
364
+ calculatorTool,
365
+ runCommandTool,
366
+ readFileTool,
367
+ writeFileTool,
368
+ listFilesTool,
369
+ addMcpServerTool,
370
+ ];
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,301 @@
1
+ # Scalix Coder — Distribution Guide
2
+
3
+ This document covers how to build, distribute, and install Scalix Coder for end users **without exposing source code**.
4
+
5
+ ---
6
+
7
+ ## Table of Contents
8
+
9
+ 1. [Overview](#overview)
10
+ 2. [Building a Release](#building-a-release)
11
+ 3. [What's in the Release](#whats-in-the-release)
12
+ 4. [Hosting the Release](#hosting-the-release)
13
+ 5. [One-Line Installer Setup](#one-line-installer-setup)
14
+ 6. [Manual Installation](#manual-installation)
15
+ 7. [VS Code Extension](#vs-code-extension)
16
+ 8. [Updating Users](#updating-users)
17
+ 9. [Troubleshooting](#troubleshooting)
18
+
19
+ ---
20
+
21
+ ## Overview
22
+
23
+ Scalix Coder is distributed as **compiled JavaScript** — no TypeScript source code is included. Users receive:
24
+
25
+ - Pre-compiled `.js` files (from `dist/`)
26
+ - A CLI entry point (`bin/scalix.js`)
27
+ - One-click install scripts for Windows and Mac/Linux
28
+ - The VS Code extension (`.vsix` file)
29
+
30
+ **What's included:**
31
+ - 31 built-in tools (shell, file, git, web, browser, vision, voice, and more)
32
+ - Security sandbox with path restriction, command blocklist, and injection detection
33
+ - IPC server mode for VS Code extension integration
34
+
35
+ **Requirements for end users:**
36
+ - Node.js 18 or higher
37
+ - npm (comes with Node.js)
38
+ - Windows 10+, macOS 12+, or Linux
39
+
40
+ ---
41
+
42
+ ## Building a Release
43
+
44
+ From the project root, run:
45
+
46
+ ```bash
47
+ npm run release
48
+ ```
49
+
50
+ This will:
51
+ 1. Compile TypeScript to JavaScript (`tsc`)
52
+ 2. Package everything into `release/` directory
53
+ 3. Create both `.tar.gz` (Mac/Linux) and `.zip` (Windows) archives
54
+ 4. Include the VS Code extension `.vsix` if available
55
+ 5. Strip `devDependencies` and dev scripts from `package.json`
56
+
57
+ **Output:**
58
+ ```
59
+ release/
60
+ ├── scalix-coder-v3.0.0/ # Extracted release folder
61
+ │ ├── dist/ # Compiled JavaScript (NO source code)
62
+ │ ├── bin/scalix.js # CLI entry point
63
+ │ ├── package.json # Dependencies only
64
+ │ ├── package-lock.json # Lockfile
65
+ │ ├── install.bat # Windows installer
66
+ │ ├── install.sh # Mac/Linux installer
67
+ │ ├── scalix-coder-0.2.0.vsix # VS Code extension
68
+ │ ├── README.md
69
+ │ ├── LICENSE
70
+ │ └── CHANGELOG.md
71
+ ├── scalix-coder-v3.0.0.tar.gz # Mac/Linux archive
72
+ └── scalix-coder-v3.0.0.zip # Windows archive
73
+ ```
74
+
75
+ ---
76
+
77
+ ## What's in the Release
78
+
79
+ ### Included
80
+ | Item | Description |
81
+ |---|---|
82
+ | `dist/*.js` | Compiled JavaScript (agent, tools, CLI, etc.) |
83
+ | `bin/scalix.js` | CLI entry point — runs `dist/cli.js` |
84
+ | `package.json` | Production dependencies only |
85
+ | `install.bat` | One-click Windows installer |
86
+ | `install.sh` | One-click Mac/Linux installer |
87
+ | `*.vsix` | VS Code extension package |
88
+ | `README.md` | User documentation |
89
+ | `LICENSE` | License file |
90
+
91
+ ### NOT Included (Protected)
92
+ | Item | Why excluded |
93
+ |---|---|
94
+ | `src/*.ts` / `src/*.tsx` | Source code — proprietary |
95
+ | `tests/` | Internal test suite |
96
+ | `tsconfig.json` | Build configuration |
97
+ | `eslint.config.js` | Lint configuration |
98
+ | `vitest.config.ts` | Test configuration |
99
+ | `scripts/` | Build scripts |
100
+ | `.claude/` | Claude Code settings |
101
+ | `.scalix/` | Project memory |
102
+ | `devDependencies` | Development tools |
103
+
104
+ ---
105
+
106
+ ## Hosting the Release
107
+
108
+ Upload the release archives to any file hosting. Options:
109
+
110
+ ### Option A: Your Own Website / Server
111
+ ```
112
+ https://scalix.world/releases/scalix-coder-v3.0.0.tar.gz
113
+ https://scalix.world/releases/scalix-coder-v3.0.0.zip
114
+ https://scalix.world/install.sh
115
+ https://scalix.world/install.ps1
116
+ ```
117
+
118
+ ### Option B: AWS S3 / Google Cloud Storage
119
+ ```bash
120
+ # Upload to S3
121
+ aws s3 cp release/scalix-coder-v3.0.0.tar.gz s3://your-bucket/releases/
122
+ aws s3 cp release/scalix-coder-v3.0.0.zip s3://your-bucket/releases/
123
+ aws s3 cp scripts/install-remote.sh s3://your-bucket/install.sh
124
+ aws s3 cp scripts/install-remote.ps1 s3://your-bucket/install.ps1
125
+ ```
126
+
127
+ ### Option C: GitHub Releases (on a separate public repo)
128
+ Create a public repo (e.g., `scalix-coder-releases`) and upload the archives as release assets.
129
+
130
+ **After uploading, update the URLs in:**
131
+ - `scripts/install-remote.sh` — change `your-domain.com` to your actual URL
132
+ - `scripts/install-remote.ps1` — change `your-domain.com` to your actual URL
133
+
134
+ ---
135
+
136
+ ## One-Line Installer Setup
137
+
138
+ Once files are hosted, users install with a single command:
139
+
140
+ ### Mac / Linux
141
+ ```bash
142
+ curl -fsSL https://scalix.world/install.sh | bash
143
+ ```
144
+
145
+ ### Windows (PowerShell)
146
+ ```powershell
147
+ irm https://scalix.world/install.ps1 | iex
148
+ ```
149
+
150
+ ### What the installer does:
151
+ 1. Checks Node.js 18+ is installed
152
+ 2. Downloads the release archive
153
+ 3. Extracts to `~/.scalix-coder/`
154
+ 4. Runs `npm install --omit=dev`
155
+ 5. Runs `npm link` to register the `scalix` command globally
156
+ 6. Done — user types `scalix` in any terminal
157
+
158
+ ### Custom Download URL
159
+ Users can override the download URL:
160
+ ```bash
161
+ SCALIX_DOWNLOAD_URL=https://internal.company.com/scalix.tar.gz curl -fsSL https://scalix.world/install.sh | bash
162
+ ```
163
+
164
+ ---
165
+
166
+ ## Manual Installation
167
+
168
+ If users prefer manual installation:
169
+
170
+ ### Windows
171
+ 1. Download `scalix-coder-v3.0.0.zip`
172
+ 2. Extract the zip file
173
+ 3. Open the extracted folder
174
+ 4. Double-click `install.bat`
175
+ 5. Open any terminal and type `scalix`
176
+
177
+ ### Mac / Linux
178
+ ```bash
179
+ tar -xzf scalix-coder-v3.0.0.tar.gz
180
+ cd scalix-coder-v3.0.0
181
+ ./install.sh
182
+ ```
183
+
184
+ ### Manual (No Installer Script)
185
+ ```bash
186
+ tar -xzf scalix-coder-v3.0.0.tar.gz
187
+ cd scalix-coder-v3.0.0
188
+ npm install --omit=dev --legacy-peer-deps
189
+ sudo npm link # or without sudo on Windows
190
+ scalix
191
+ ```
192
+
193
+ ---
194
+
195
+ ## VS Code Extension
196
+
197
+ The VS Code extension (`.vsix` file) is bundled in the release.
198
+
199
+ ### Install
200
+ 1. Open VS Code
201
+ 2. Press `Ctrl+Shift+P` (or `Cmd+Shift+P` on Mac)
202
+ 3. Type **"Extensions: Install from VSIX"**
203
+ 4. Select `scalix-coder-0.2.0.vsix` from the extracted folder
204
+ 5. Reload VS Code
205
+
206
+ ### Features
207
+ - Sidebar chat panel with AI assistant
208
+ - Right-click: Explain / Fix / Refactor code selection
209
+ - Model picker and mode selector via slash commands
210
+ - Integrated terminal (`Ctrl+Shift+T`)
211
+ - Open chat in editor area (`Ctrl+Shift+K`)
212
+
213
+ ### Configure
214
+ 1. Press `Ctrl+,` to open Settings
215
+ 2. Search for "Scalix"
216
+ 3. Enter your API key
217
+ 4. Or type `/setup` in the chat
218
+
219
+ ---
220
+
221
+ ## Updating Users
222
+
223
+ When releasing a new version:
224
+
225
+ 1. Update `version` in `package.json`
226
+ 2. Run `npm run release`
227
+ 3. Upload new archives to your hosting (overwrite or add new version)
228
+ 4. Update version number in `scripts/install-remote.sh` and `scripts/install-remote.ps1`
229
+ 5. Users re-run the one-liner to update:
230
+ ```bash
231
+ curl -fsSL https://scalix.world/install.sh | bash
232
+ ```
233
+
234
+ ### Versioning
235
+ - CLI version: `package.json` → `version` (currently `3.1.0`)
236
+ - Extension version: `vscode-extension/package.json` → `version` (currently `0.2.0`)
237
+ - Both versions should be bumped together for coordinated releases
238
+
239
+ ---
240
+
241
+ ## Troubleshooting
242
+
243
+ ### "scalix" command not found after install
244
+ ```bash
245
+ # Check if npm global bin is in PATH
246
+ npm bin -g
247
+
248
+ # On Mac/Linux, add to ~/.bashrc or ~/.zshrc:
249
+ export PATH="$(npm bin -g):$PATH"
250
+
251
+ # On Windows, npm global bin is usually already in PATH.
252
+ # If not, run install.bat as Administrator.
253
+ ```
254
+
255
+ ### Permission denied on Mac/Linux
256
+ ```bash
257
+ sudo npm link
258
+ # or
259
+ sudo ./install.sh
260
+ ```
261
+
262
+ ### Node.js version too old
263
+ ```bash
264
+ node -v # Must be v18+
265
+ ```
266
+ Download latest from https://nodejs.org/
267
+
268
+ ### API key not working
269
+ - Run `/setup` in the chat to re-enter your key
270
+ - Or set the environment variable: `export SCALIX_API_KEY=sk-...`
271
+ - Check Settings in VS Code: `Ctrl+,` → search "Scalix"
272
+
273
+ ### Rate limited
274
+ The agent retries automatically up to 5 times with backoff. If you still see rate limit errors, wait 60 seconds and try again.
275
+
276
+ ---
277
+
278
+ ## Security Notes
279
+
280
+ - **No source code** is distributed — only compiled JavaScript
281
+ - API keys are stored locally at `~/.scalix-coder/config.json` (file permissions: 600)
282
+ - Shell commands have a blocklist for destructive patterns
283
+ - Path traversal is blocked — tools cannot access files outside the workspace
284
+ - All buffers and collections are bounded to prevent memory exhaustion
285
+ - Background processes auto-cleanup after 5 minutes
286
+ - The VS Code extension uses VS Code's Content Security Policy for the webview
287
+ - All webview user content is HTML-escaped before rendering
288
+
289
+ ---
290
+
291
+ ## Quick Reference
292
+
293
+ | Task | Command |
294
+ |---|---|
295
+ | Build release | `npm run release` |
296
+ | Build TypeScript | `npm run build` |
297
+ | Run tests | `npm run test` |
298
+ | Type-check | `npm run typecheck` |
299
+ | Lint | `npm run lint` |
300
+ | All checks | `npm run check` |
301
+ | Build VS Code ext | `cd vscode-extension && npm run build && npm run package` |