scalix 0.0.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/.env.example +29 -0
  2. package/CHANGELOG.md +122 -0
  3. package/LICENSE +100 -0
  4. package/README.md +300 -2
  5. package/bin/scalix.js +19 -0
  6. package/dist/agent-definitions.js +100 -0
  7. package/dist/agent-registry.js +226 -0
  8. package/dist/agent-replay.js +166 -0
  9. package/dist/agent.js +1130 -0
  10. package/dist/api-client.js +77 -0
  11. package/dist/auto-verify.js +136 -0
  12. package/dist/change-tracker.js +53 -0
  13. package/dist/cli.js +2454 -0
  14. package/dist/commands.js +71 -0
  15. package/dist/config.js +16 -0
  16. package/dist/constants.js +358 -0
  17. package/dist/context-builder.js +158 -0
  18. package/dist/context-manager.js +148 -0
  19. package/dist/context-retriever.js +257 -0
  20. package/dist/cost-tracker.js +85 -0
  21. package/dist/diff.js +140 -0
  22. package/dist/git-context.js +183 -0
  23. package/dist/git-undo.js +67 -0
  24. package/dist/grounding-checker.js +154 -0
  25. package/dist/headless.js +743 -0
  26. package/dist/hooks.js +100 -0
  27. package/dist/injection-detector.js +86 -0
  28. package/dist/ipc-server.js +232 -0
  29. package/dist/logger.js +45 -0
  30. package/dist/markdown.js +41 -0
  31. package/dist/mcp.js +49 -0
  32. package/dist/model-router.js +68 -0
  33. package/dist/plugins.js +132 -0
  34. package/dist/project-index.js +376 -0
  35. package/dist/project-memory.js +165 -0
  36. package/dist/safe-math.js +194 -0
  37. package/dist/sandbox.js +403 -0
  38. package/dist/scoped-rules.js +82 -0
  39. package/dist/sessions.js +106 -0
  40. package/dist/settings.js +260 -0
  41. package/dist/sub-agent.js +154 -0
  42. package/dist/task-board.js +105 -0
  43. package/dist/token-counter.js +47 -0
  44. package/dist/tool-helper.js +97 -0
  45. package/dist/tool-selector.js +77 -0
  46. package/dist/tools-advanced.js +487 -0
  47. package/dist/tools-browser.js +534 -0
  48. package/dist/tools-extra.js +390 -0
  49. package/dist/tools-vision.js +116 -0
  50. package/dist/tools-voice.js +147 -0
  51. package/dist/tools-web.js +346 -0
  52. package/dist/tools.js +380 -0
  53. package/dist/types.js +1 -0
  54. package/dist/update-checker.js +111 -0
  55. package/dist/worktree.js +172 -0
  56. package/docs/DISTRIBUTION.md +301 -0
  57. package/package.json +71 -6
@@ -0,0 +1,183 @@
1
+ import { exec } from 'node:child_process';
2
+ import { promisify } from 'node:util';
3
+ import { TOOL_DEFAULTS, GIT_CONTEXT_DEFAULTS, PROJECT_INDEX_DEFAULTS } from './constants.js';
4
+ const execAsync = promisify(exec);
5
+ async function run(cmd, cwd) {
6
+ try {
7
+ const { stdout } = await execAsync(cmd, { cwd, encoding: 'utf-8', timeout: TOOL_DEFAULTS.gitTimeoutMs });
8
+ return stdout.trim();
9
+ }
10
+ catch {
11
+ return '';
12
+ }
13
+ }
14
+ async function isGitRepo(cwd) {
15
+ return (await run('git rev-parse --is-inside-work-tree', cwd)) === 'true';
16
+ }
17
+ async function getBranch(cwd) {
18
+ return (await run('git branch --show-current', cwd)) || (await run('git rev-parse --short HEAD', cwd)) || 'unknown';
19
+ }
20
+ async function getRemoteBranch(cwd) {
21
+ return run('git rev-parse --abbrev-ref @{upstream} 2>/dev/null', cwd);
22
+ }
23
+ async function getRemoteUrl(cwd) {
24
+ return run('git remote get-url origin 2>/dev/null', cwd);
25
+ }
26
+ async function getAheadBehind(cwd) {
27
+ const result = await run('git rev-list --left-right --count @{upstream}...HEAD 2>/dev/null', cwd);
28
+ if (!result)
29
+ return { ahead: 0, behind: 0 };
30
+ const parts = result.split('\t');
31
+ if (parts.length < 2)
32
+ return { ahead: 0, behind: 0 };
33
+ return { ahead: Number(parts[1]) || 0, behind: Number(parts[0]) || 0 };
34
+ }
35
+ async function getStashCount(cwd) {
36
+ const result = await run('git stash list 2>/dev/null', cwd);
37
+ return result ? result.split('\n').filter(Boolean).length : 0;
38
+ }
39
+ async function getStatus(cwd) {
40
+ const result = await run('git status --porcelain', cwd);
41
+ if (!result)
42
+ return [];
43
+ return result.split('\n').filter(Boolean).map((line) => {
44
+ const indexStatus = line[0];
45
+ const workStatus = line[1];
46
+ const filePath = line.slice(3).trim();
47
+ let status = 'modified';
48
+ let staged = false;
49
+ if (indexStatus === '?' && workStatus === '?') {
50
+ status = 'untracked';
51
+ }
52
+ else if (indexStatus === 'A') {
53
+ status = 'added';
54
+ staged = true;
55
+ }
56
+ else if (indexStatus === 'D' || workStatus === 'D') {
57
+ status = 'deleted';
58
+ staged = indexStatus === 'D';
59
+ }
60
+ else if (indexStatus === 'R') {
61
+ status = 'renamed';
62
+ staged = true;
63
+ }
64
+ else if (indexStatus === 'M') {
65
+ status = 'staged';
66
+ staged = true;
67
+ }
68
+ else if (workStatus === 'M') {
69
+ status = 'modified';
70
+ }
71
+ return { path: filePath, status, staged };
72
+ });
73
+ }
74
+ async function getRecentCommits(cwd, count = GIT_CONTEXT_DEFAULTS.recentCommitsCount) {
75
+ const SEP = '<%>';
76
+ const format = `%H${SEP}%h${SEP}%s${SEP}%an${SEP}%ar`;
77
+ const result = await run(`git log --pretty=format:"${format}" --shortstat -n ${count} 2>/dev/null`, cwd);
78
+ if (!result)
79
+ return [];
80
+ const commits = [];
81
+ const lines = result.split('\n');
82
+ let i = 0;
83
+ while (i < lines.length) {
84
+ const line = lines[i].replace(/^"/, '').replace(/"$/, '');
85
+ const parts = line.split(SEP);
86
+ if (parts.length >= 5 && parts[0].length === 40) {
87
+ let filesChanged = 0;
88
+ if (i + 1 < lines.length && lines[i + 1].includes('file')) {
89
+ const match = lines[i + 1].match(/(\d+)\s+file/);
90
+ if (match)
91
+ filesChanged = parseInt(match[1], 10);
92
+ i++;
93
+ }
94
+ if (i + 1 < lines.length && lines[i + 1].trim() === '')
95
+ i++;
96
+ commits.push({
97
+ hash: parts[0],
98
+ shortHash: parts[1],
99
+ message: parts[2],
100
+ author: parts[3],
101
+ date: parts[4],
102
+ filesChanged,
103
+ });
104
+ }
105
+ i++;
106
+ }
107
+ return commits;
108
+ }
109
+ export async function collectGitInfo(cwd) {
110
+ if (!(await isGitRepo(cwd))) {
111
+ return {
112
+ isRepo: false,
113
+ branch: '',
114
+ remoteBranch: '',
115
+ status: [],
116
+ recentCommits: [],
117
+ stashCount: 0,
118
+ aheadBehind: { ahead: 0, behind: 0 },
119
+ remoteUrl: '',
120
+ };
121
+ }
122
+ const [branch, remoteBranch, status, recentCommits, stashCount, aheadBehind, remoteUrl] = await Promise.all([
123
+ getBranch(cwd),
124
+ getRemoteBranch(cwd),
125
+ getStatus(cwd),
126
+ getRecentCommits(cwd, GIT_CONTEXT_DEFAULTS.recentCommitsContextCount),
127
+ getStashCount(cwd),
128
+ getAheadBehind(cwd),
129
+ getRemoteUrl(cwd),
130
+ ]);
131
+ return { isRepo: true, branch, remoteBranch, status, recentCommits, stashCount, aheadBehind, remoteUrl };
132
+ }
133
+ export function gitInfoToContext(info) {
134
+ if (!info.isRepo)
135
+ return '## Git: Not a git repository';
136
+ const lines = ['## Git Context'];
137
+ lines.push(`Branch: ${info.branch}`);
138
+ if (info.remoteBranch) {
139
+ lines.push(`Tracking: ${info.remoteBranch}`);
140
+ }
141
+ const { ahead, behind } = info.aheadBehind;
142
+ if (ahead > 0 || behind > 0) {
143
+ const parts = [];
144
+ if (ahead > 0)
145
+ parts.push(`${ahead} ahead`);
146
+ if (behind > 0)
147
+ parts.push(`${behind} behind`);
148
+ lines.push(`Status: ${parts.join(', ')}`);
149
+ }
150
+ if (info.stashCount > 0) {
151
+ lines.push(`Stashes: ${info.stashCount}`);
152
+ }
153
+ if (info.status.length > 0) {
154
+ lines.push('');
155
+ lines.push('### Working Tree Changes');
156
+ const staged = info.status.filter((s) => s.staged);
157
+ const unstaged = info.status.filter((s) => !s.staged);
158
+ if (staged.length > 0) {
159
+ lines.push('Staged:');
160
+ for (const f of staged.slice(0, PROJECT_INDEX_DEFAULTS.statusDisplayLimit)) {
161
+ lines.push(` ${f.status.padEnd(10)} ${f.path}`);
162
+ }
163
+ if (staged.length > PROJECT_INDEX_DEFAULTS.statusDisplayLimit)
164
+ lines.push(` ... and ${staged.length - PROJECT_INDEX_DEFAULTS.statusDisplayLimit} more`);
165
+ }
166
+ if (unstaged.length > 0) {
167
+ lines.push('Unstaged:');
168
+ for (const f of unstaged.slice(0, PROJECT_INDEX_DEFAULTS.statusDisplayLimit)) {
169
+ lines.push(` ${f.status.padEnd(10)} ${f.path}`);
170
+ }
171
+ if (unstaged.length > PROJECT_INDEX_DEFAULTS.statusDisplayLimit)
172
+ lines.push(` ... and ${unstaged.length - PROJECT_INDEX_DEFAULTS.statusDisplayLimit} more`);
173
+ }
174
+ }
175
+ if (info.recentCommits.length > 0) {
176
+ lines.push('');
177
+ lines.push('### Recent Commits');
178
+ for (const c of info.recentCommits) {
179
+ lines.push(` ${c.shortHash} ${c.message} (${c.author}, ${c.date})`);
180
+ }
181
+ }
182
+ return lines.join('\n');
183
+ }
@@ -0,0 +1,67 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { promisify } from 'node:util';
3
+ import { logger } from './logger.js';
4
+ import { TOOL_DEFAULTS } from './constants.js';
5
+ const execFileAsync = promisify(execFile);
6
+ async function git(args, cwd) {
7
+ const { stdout } = await execFileAsync('git', args, { cwd, timeout: TOOL_DEFAULTS.gitTimeoutMs });
8
+ return stdout.trim();
9
+ }
10
+ async function isGitRepo(cwd) {
11
+ try {
12
+ await git(['rev-parse', '--is-inside-work-tree'], cwd);
13
+ return true;
14
+ }
15
+ catch {
16
+ return false;
17
+ }
18
+ }
19
+ async function hasChanges(cwd) {
20
+ const status = await git(['status', '--porcelain'], cwd);
21
+ return status.length > 0;
22
+ }
23
+ export async function createCheckpoint(cwd, label) {
24
+ if (!await isGitRepo(cwd))
25
+ return null;
26
+ if (!await hasChanges(cwd))
27
+ return null;
28
+ try {
29
+ await git(['add', '-u'], cwd);
30
+ const msg = `[scalix checkpoint] ${label || 'auto'}`;
31
+ await git(['commit', '-m', msg], cwd);
32
+ const hash = await git(['rev-parse', '--short', 'HEAD'], cwd);
33
+ logger.info('Checkpoint created', { hash, label });
34
+ return hash;
35
+ }
36
+ catch (err) {
37
+ logger.warn('Checkpoint failed', { error: err.message });
38
+ return null;
39
+ }
40
+ }
41
+ export async function undoLastCheckpoint(cwd) {
42
+ if (!await isGitRepo(cwd)) {
43
+ return { success: false, message: 'Not a git repository.' };
44
+ }
45
+ try {
46
+ const lastMsg = await git(['log', '-1', '--format=%s'], cwd);
47
+ if (!lastMsg.startsWith('[scalix checkpoint]')) {
48
+ return { success: false, message: 'Last commit is not a scalix checkpoint. Undo only works on agent-created commits.' };
49
+ }
50
+ await git(['reset', '--mixed', 'HEAD~1'], cwd);
51
+ return { success: true, message: `Undid checkpoint: ${lastMsg}` };
52
+ }
53
+ catch (err) {
54
+ return { success: false, message: `Undo failed: ${err.message}` };
55
+ }
56
+ }
57
+ export async function listCheckpoints(cwd, limit = 10) {
58
+ if (!await isGitRepo(cwd))
59
+ return [];
60
+ try {
61
+ const log = await git(['log', `--max-count=${limit}`, '--format=%h %s', '--grep=\\[scalix checkpoint\\]'], cwd);
62
+ return log ? log.split('\n') : [];
63
+ }
64
+ catch {
65
+ return [];
66
+ }
67
+ }
@@ -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
+ }