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.
- package/CHANGELOG.md +122 -0
- package/LICENSE +100 -0
- package/README.md +300 -2
- package/app.py +55 -0
- package/bin/scalix.js +19 -0
- package/dist/agent-replay.js +166 -0
- package/dist/agent.js +852 -0
- package/dist/auto-verify.js +136 -0
- package/dist/change-tracker.js +53 -0
- package/dist/cli.js +2329 -0
- package/dist/commands.js +71 -0
- package/dist/config.js +16 -0
- package/dist/constants.js +324 -0
- package/dist/context-builder.js +151 -0
- package/dist/context-manager.js +148 -0
- package/dist/cost-tracker.js +85 -0
- package/dist/diff.js +140 -0
- package/dist/git-context.js +180 -0
- package/dist/git-undo.js +67 -0
- package/dist/headless.js +654 -0
- package/dist/hooks.js +94 -0
- package/dist/ipc-server.js +228 -0
- package/dist/logger.js +45 -0
- package/dist/markdown.js +41 -0
- package/dist/mcp.js +49 -0
- package/dist/model-router.js +68 -0
- package/dist/plugins.js +132 -0
- package/dist/project-index.js +374 -0
- package/dist/project-memory.js +165 -0
- package/dist/safe-math.js +194 -0
- package/dist/sandbox.js +279 -0
- package/dist/scoped-rules.js +82 -0
- package/dist/sessions.js +71 -0
- package/dist/settings.js +155 -0
- package/dist/sub-agent.js +79 -0
- package/dist/token-counter.js +47 -0
- package/dist/tool-helper.js +97 -0
- package/dist/tool-selector.js +77 -0
- package/dist/tools-advanced.js +232 -0
- package/dist/tools-browser.js +273 -0
- package/dist/tools-extra.js +383 -0
- package/dist/tools-vision.js +115 -0
- package/dist/tools-voice.js +147 -0
- package/dist/tools-web.js +346 -0
- package/dist/tools.js +370 -0
- package/dist/types.js +1 -0
- package/docs/DISTRIBUTION.md +301 -0
- 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` |
|