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
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import fg from 'fast-glob';
|
|
4
|
+
import { PROJECT_INDEX_DEFAULTS } from './constants.js';
|
|
5
|
+
const IGNORE_DIRS = [
|
|
6
|
+
'node_modules', '.git', 'dist', 'build', 'out', '.next', '.nuxt',
|
|
7
|
+
'.cache', 'coverage', '.turbo', '.vercel', '.svelte-kit', '__pycache__',
|
|
8
|
+
'venv', '.venv', 'target', 'vendor',
|
|
9
|
+
];
|
|
10
|
+
const CODE_EXTENSIONS = new Set([
|
|
11
|
+
'.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs',
|
|
12
|
+
'.py', '.rb', '.go', '.rs', '.java', '.kt', '.swift',
|
|
13
|
+
'.c', '.cpp', '.h', '.hpp', '.cs', '.php',
|
|
14
|
+
'.vue', '.svelte', '.astro',
|
|
15
|
+
'.sql', '.graphql', '.gql',
|
|
16
|
+
'.sh', '.bash', '.zsh',
|
|
17
|
+
]);
|
|
18
|
+
const CONFIG_FILES = new Set([
|
|
19
|
+
'package.json', 'tsconfig.json', 'pyproject.toml', 'Cargo.toml',
|
|
20
|
+
'go.mod', 'Gemfile', 'pom.xml', 'build.gradle', 'Makefile',
|
|
21
|
+
'docker-compose.yml', 'Dockerfile', '.env.example',
|
|
22
|
+
]);
|
|
23
|
+
// JS/TS patterns
|
|
24
|
+
const JS_SYMBOL_PATTERNS = [
|
|
25
|
+
{ kind: 'function', pattern: /^(?:export\s+)?(?:async\s+)?function\s+(\w+)/gm },
|
|
26
|
+
{ kind: 'class', pattern: /^(?:export\s+)?(?:abstract\s+)?class\s+(\w+)/gm },
|
|
27
|
+
{ kind: 'interface', pattern: /^(?:export\s+)?interface\s+(\w+)/gm },
|
|
28
|
+
{ kind: 'type', pattern: /^(?:export\s+)?type\s+(\w+)\s*[=<]/gm },
|
|
29
|
+
{ kind: 'export', pattern: /^export\s+(?:default\s+)?(?:const|let|var|function|class)\s+(\w+)/gm },
|
|
30
|
+
{ kind: 'const', pattern: /^(?:export\s+)?const\s+(\w+)\s*[:=]/gm },
|
|
31
|
+
];
|
|
32
|
+
// Python patterns
|
|
33
|
+
const PY_SYMBOL_PATTERNS = [
|
|
34
|
+
{ kind: 'function', pattern: /^(?:async\s+)?def\s+(\w+)\s*\(/gm },
|
|
35
|
+
{ kind: 'class', pattern: /^class\s+(\w+)[\s(:]/gm },
|
|
36
|
+
{ kind: 'variable', pattern: /^(\w+)\s*(?::\s*\w+)?\s*=/gm },
|
|
37
|
+
];
|
|
38
|
+
// Go patterns
|
|
39
|
+
const GO_SYMBOL_PATTERNS = [
|
|
40
|
+
{ kind: 'function', pattern: /^func\s+(?:\([^)]+\)\s+)?(\w+)\s*\(/gm },
|
|
41
|
+
{ kind: 'type', pattern: /^type\s+(\w+)\s+(?:struct|interface)\b/gm },
|
|
42
|
+
{ kind: 'const', pattern: /^(?:var|const)\s+(\w+)\s/gm },
|
|
43
|
+
];
|
|
44
|
+
// Rust patterns
|
|
45
|
+
const RS_SYMBOL_PATTERNS = [
|
|
46
|
+
{ kind: 'function', pattern: /^(?:pub\s+)?(?:async\s+)?fn\s+(\w+)/gm },
|
|
47
|
+
{ kind: 'type', pattern: /^(?:pub\s+)?struct\s+(\w+)/gm },
|
|
48
|
+
{ kind: 'type', pattern: /^(?:pub\s+)?enum\s+(\w+)/gm },
|
|
49
|
+
{ kind: 'interface', pattern: /^(?:pub\s+)?trait\s+(\w+)/gm },
|
|
50
|
+
{ kind: 'type', pattern: /^impl(?:<[^>]+>)?\s+(\w+)/gm },
|
|
51
|
+
];
|
|
52
|
+
// Java/Kotlin patterns
|
|
53
|
+
const JAVA_SYMBOL_PATTERNS = [
|
|
54
|
+
{ kind: 'class', pattern: /^(?:public\s+)?(?:abstract\s+)?(?:final\s+)?class\s+(\w+)/gm },
|
|
55
|
+
{ kind: 'interface', pattern: /^(?:public\s+)?interface\s+(\w+)/gm },
|
|
56
|
+
{ kind: 'function', pattern: /^\s+(?:public|private|protected|static|\s)*\s+\w+\s+(\w+)\s*\(/gm },
|
|
57
|
+
{ kind: 'type', pattern: /^(?:public\s+)?enum\s+(\w+)/gm },
|
|
58
|
+
];
|
|
59
|
+
// Ruby patterns
|
|
60
|
+
const RB_SYMBOL_PATTERNS = [
|
|
61
|
+
{ kind: 'function', pattern: /^\s*def\s+(?:self\.)?(\w+)/gm },
|
|
62
|
+
{ kind: 'class', pattern: /^\s*class\s+(\w+)/gm },
|
|
63
|
+
{ kind: 'type', pattern: /^\s*module\s+(\w+)/gm },
|
|
64
|
+
];
|
|
65
|
+
// PHP patterns
|
|
66
|
+
const PHP_SYMBOL_PATTERNS = [
|
|
67
|
+
{ kind: 'function', pattern: /^(?:public|private|protected|static|\s)*\s*function\s+(\w+)/gm },
|
|
68
|
+
{ kind: 'class', pattern: /^(?:abstract\s+)?class\s+(\w+)/gm },
|
|
69
|
+
{ kind: 'interface', pattern: /^interface\s+(\w+)/gm },
|
|
70
|
+
{ kind: 'interface', pattern: /^trait\s+(\w+)/gm },
|
|
71
|
+
];
|
|
72
|
+
const LANGUAGE_PATTERNS = {
|
|
73
|
+
'.ts': JS_SYMBOL_PATTERNS,
|
|
74
|
+
'.tsx': JS_SYMBOL_PATTERNS,
|
|
75
|
+
'.js': JS_SYMBOL_PATTERNS,
|
|
76
|
+
'.jsx': JS_SYMBOL_PATTERNS,
|
|
77
|
+
'.mjs': JS_SYMBOL_PATTERNS,
|
|
78
|
+
'.cjs': JS_SYMBOL_PATTERNS,
|
|
79
|
+
'.py': PY_SYMBOL_PATTERNS,
|
|
80
|
+
'.go': GO_SYMBOL_PATTERNS,
|
|
81
|
+
'.rs': RS_SYMBOL_PATTERNS,
|
|
82
|
+
'.java': JAVA_SYMBOL_PATTERNS,
|
|
83
|
+
'.kt': JAVA_SYMBOL_PATTERNS,
|
|
84
|
+
'.rb': RB_SYMBOL_PATTERNS,
|
|
85
|
+
'.php': PHP_SYMBOL_PATTERNS,
|
|
86
|
+
'.swift': JAVA_SYMBOL_PATTERNS,
|
|
87
|
+
'.cs': JAVA_SYMBOL_PATTERNS,
|
|
88
|
+
'.c': GO_SYMBOL_PATTERNS,
|
|
89
|
+
'.cpp': GO_SYMBOL_PATTERNS,
|
|
90
|
+
'.h': GO_SYMBOL_PATTERNS,
|
|
91
|
+
'.hpp': GO_SYMBOL_PATTERNS,
|
|
92
|
+
};
|
|
93
|
+
function detectTechStack(files) {
|
|
94
|
+
const stack = [];
|
|
95
|
+
const names = new Set(files.map((f) => path.basename(f.relativePath)));
|
|
96
|
+
const exts = new Set(files.map((f) => f.ext));
|
|
97
|
+
if (names.has('package.json'))
|
|
98
|
+
stack.push('Node.js');
|
|
99
|
+
if (names.has('tsconfig.json') || exts.has('.ts') || exts.has('.tsx'))
|
|
100
|
+
stack.push('TypeScript');
|
|
101
|
+
if (exts.has('.jsx') || exts.has('.tsx'))
|
|
102
|
+
stack.push('React');
|
|
103
|
+
if (names.has('next.config.js') || names.has('next.config.mjs') || names.has('next.config.ts'))
|
|
104
|
+
stack.push('Next.js');
|
|
105
|
+
if (names.has('nuxt.config.ts') || names.has('nuxt.config.js'))
|
|
106
|
+
stack.push('Nuxt');
|
|
107
|
+
if (exts.has('.vue'))
|
|
108
|
+
stack.push('Vue');
|
|
109
|
+
if (exts.has('.svelte'))
|
|
110
|
+
stack.push('Svelte');
|
|
111
|
+
if (names.has('pyproject.toml') || names.has('setup.py') || exts.has('.py'))
|
|
112
|
+
stack.push('Python');
|
|
113
|
+
if (names.has('Cargo.toml') || exts.has('.rs'))
|
|
114
|
+
stack.push('Rust');
|
|
115
|
+
if (names.has('go.mod') || exts.has('.go'))
|
|
116
|
+
stack.push('Go');
|
|
117
|
+
if (names.has('Gemfile') || exts.has('.rb'))
|
|
118
|
+
stack.push('Ruby');
|
|
119
|
+
if (names.has('pom.xml') || names.has('build.gradle') || exts.has('.java'))
|
|
120
|
+
stack.push('Java');
|
|
121
|
+
if (exts.has('.kt'))
|
|
122
|
+
stack.push('Kotlin');
|
|
123
|
+
if (exts.has('.swift'))
|
|
124
|
+
stack.push('Swift');
|
|
125
|
+
if (names.has('Dockerfile') || names.has('docker-compose.yml'))
|
|
126
|
+
stack.push('Docker');
|
|
127
|
+
if (names.has('.github'))
|
|
128
|
+
stack.push('GitHub Actions');
|
|
129
|
+
if (names.has('tailwind.config.js') || names.has('tailwind.config.ts'))
|
|
130
|
+
stack.push('Tailwind CSS');
|
|
131
|
+
if (names.has('prisma'))
|
|
132
|
+
stack.push('Prisma');
|
|
133
|
+
if (names.has('drizzle.config.ts'))
|
|
134
|
+
stack.push('Drizzle');
|
|
135
|
+
return stack;
|
|
136
|
+
}
|
|
137
|
+
function buildTree(files, maxDepth = 3) {
|
|
138
|
+
const dirs = new Map();
|
|
139
|
+
for (const f of files) {
|
|
140
|
+
const parts = f.relativePath.split('/');
|
|
141
|
+
if (parts.length > maxDepth + 1)
|
|
142
|
+
continue;
|
|
143
|
+
const dir = parts.length === 1 ? '.' : parts.slice(0, -1).join('/');
|
|
144
|
+
if (!dirs.has(dir))
|
|
145
|
+
dirs.set(dir, []);
|
|
146
|
+
dirs.get(dir).push(parts[parts.length - 1]);
|
|
147
|
+
}
|
|
148
|
+
const lines = [];
|
|
149
|
+
const sortedDirs = [...dirs.keys()].sort();
|
|
150
|
+
for (const dir of sortedDirs) {
|
|
151
|
+
const depth = dir === '.' ? 0 : dir.split('/').length;
|
|
152
|
+
const indent = ' '.repeat(depth);
|
|
153
|
+
if (dir !== '.') {
|
|
154
|
+
const dirName = dir.split('/').pop();
|
|
155
|
+
lines.push(`${indent}${dirName}/`);
|
|
156
|
+
}
|
|
157
|
+
const fileIndent = ' '.repeat(depth + (dir === '.' ? 0 : 1));
|
|
158
|
+
const dirFiles = dirs.get(dir).sort();
|
|
159
|
+
if (dirFiles.length <= PROJECT_INDEX_DEFAULTS.treeDisplayLimit) {
|
|
160
|
+
for (const file of dirFiles) {
|
|
161
|
+
lines.push(`${fileIndent}${file}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
for (const file of dirFiles.slice(0, PROJECT_INDEX_DEFAULTS.treeDisplayTruncate)) {
|
|
166
|
+
lines.push(`${fileIndent}${file}`);
|
|
167
|
+
}
|
|
168
|
+
lines.push(`${fileIndent}... and ${dirFiles.length - PROJECT_INDEX_DEFAULTS.treeDisplayTruncate} more files`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return lines.join('\n');
|
|
172
|
+
}
|
|
173
|
+
async function extractSymbols(filePath, relativePath) {
|
|
174
|
+
const ext = path.extname(filePath);
|
|
175
|
+
if (!CODE_EXTENSIONS.has(ext))
|
|
176
|
+
return [];
|
|
177
|
+
const patterns = LANGUAGE_PATTERNS[ext];
|
|
178
|
+
if (!patterns)
|
|
179
|
+
return [];
|
|
180
|
+
try {
|
|
181
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
182
|
+
if (content.length > PROJECT_INDEX_DEFAULTS.symbolParseMaxChars)
|
|
183
|
+
return [];
|
|
184
|
+
const symbols = [];
|
|
185
|
+
for (const { kind, pattern } of patterns) {
|
|
186
|
+
const regex = new RegExp(pattern.source, pattern.flags);
|
|
187
|
+
let match;
|
|
188
|
+
while ((match = regex.exec(content)) !== null) {
|
|
189
|
+
const name = match[1];
|
|
190
|
+
if (!name || name.length < 2)
|
|
191
|
+
continue;
|
|
192
|
+
if (/^(if|for|while|switch|return|import|from|const|let|var|self|this|else|elif|case|catch|try)$/.test(name))
|
|
193
|
+
continue;
|
|
194
|
+
const lineNum = content.substring(0, match.index).split('\n').length;
|
|
195
|
+
if (symbols.some((s) => s.name === name && s.kind === kind && s.file === relativePath))
|
|
196
|
+
continue;
|
|
197
|
+
symbols.push({ name, kind, file: relativePath, line: lineNum });
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return symbols;
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
return [];
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
// Import patterns for dependency tracking
|
|
207
|
+
const IMPORT_PATTERNS = [
|
|
208
|
+
// JS/TS: import { x } from './y' or import x from './y' or require('./y')
|
|
209
|
+
{ ext: new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs']),
|
|
210
|
+
pattern: /(?:import\s+(?:{([^}]+)}\s+from\s+|(\w+)\s+from\s+)?['"]([^'"]+)['"]|require\s*\(\s*['"]([^'"]+)['"]\s*\))/g },
|
|
211
|
+
// Python: from x import y or import x
|
|
212
|
+
{ ext: new Set(['.py']),
|
|
213
|
+
pattern: /(?:from\s+([\w.]+)\s+import\s+([\w,\s*]+)|import\s+([\w.]+))/g },
|
|
214
|
+
// Go: import "x" or import ( "x" )
|
|
215
|
+
{ ext: new Set(['.go']),
|
|
216
|
+
pattern: /import\s+(?:(?:\w+\s+)?"([^"]+)"|[\s\S]*?"([^"]+)")/g },
|
|
217
|
+
// Rust: use x::y
|
|
218
|
+
{ ext: new Set(['.rs']),
|
|
219
|
+
pattern: /use\s+([\w:]+(?:::\{[^}]+\})?)/g },
|
|
220
|
+
];
|
|
221
|
+
function extractImports(content, relativePath, ext) {
|
|
222
|
+
const imports = [];
|
|
223
|
+
for (const { ext: exts, pattern } of IMPORT_PATTERNS) {
|
|
224
|
+
if (!exts.has(ext))
|
|
225
|
+
continue;
|
|
226
|
+
const regex = new RegExp(pattern.source, pattern.flags);
|
|
227
|
+
let match;
|
|
228
|
+
while ((match = regex.exec(content)) !== null) {
|
|
229
|
+
const modulePath = match[3] || match[4] || match[1] || match[5] || match[6] || '';
|
|
230
|
+
if (!modulePath)
|
|
231
|
+
continue;
|
|
232
|
+
// Only track local imports (starting with . or relative paths)
|
|
233
|
+
const isLocal = modulePath.startsWith('.') || modulePath.startsWith('/');
|
|
234
|
+
if (!isLocal)
|
|
235
|
+
continue;
|
|
236
|
+
const importedSymbols = (match[1] || match[2] || '').split(',').map((s) => s.trim().split(/\s+as\s+/)[0]).filter(Boolean);
|
|
237
|
+
imports.push({
|
|
238
|
+
from: relativePath,
|
|
239
|
+
to: modulePath,
|
|
240
|
+
symbols: importedSymbols,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return imports;
|
|
245
|
+
}
|
|
246
|
+
async function buildProjectIndexFresh(root) {
|
|
247
|
+
const ignorePattern = IGNORE_DIRS.map((d) => `**/${d}/**`);
|
|
248
|
+
const globbed = await fg('**/*', {
|
|
249
|
+
cwd: root,
|
|
250
|
+
ignore: ignorePattern,
|
|
251
|
+
onlyFiles: true,
|
|
252
|
+
stats: true,
|
|
253
|
+
dot: false,
|
|
254
|
+
followSymbolicLinks: false,
|
|
255
|
+
suppressErrors: true,
|
|
256
|
+
});
|
|
257
|
+
const files = [];
|
|
258
|
+
for (const entry of globbed) {
|
|
259
|
+
const stats = entry.stats;
|
|
260
|
+
if (!stats || stats.size > PROJECT_INDEX_DEFAULTS.maxFileSizeBytes)
|
|
261
|
+
continue;
|
|
262
|
+
files.push({
|
|
263
|
+
path: path.join(root, entry.path),
|
|
264
|
+
relativePath: entry.path,
|
|
265
|
+
size: stats.size,
|
|
266
|
+
ext: path.extname(entry.path),
|
|
267
|
+
modifiedAt: stats.mtimeMs,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
files.sort((a, b) => b.modifiedAt - a.modifiedAt);
|
|
271
|
+
const codeFiles = files.filter((f) => CODE_EXTENSIONS.has(f.ext));
|
|
272
|
+
const symbolFiles = codeFiles.slice(0, PROJECT_INDEX_DEFAULTS.symbolFileLimit);
|
|
273
|
+
// Extract symbols and imports in batches to avoid OOM on huge repos
|
|
274
|
+
const BATCH_SIZE = 50;
|
|
275
|
+
const allImports = [];
|
|
276
|
+
const symbols = [];
|
|
277
|
+
for (let i = 0; i < symbolFiles.length; i += BATCH_SIZE) {
|
|
278
|
+
const batch = symbolFiles.slice(i, i + BATCH_SIZE);
|
|
279
|
+
const batchResults = await Promise.all(batch.map(async (f) => {
|
|
280
|
+
const content = await fs.readFile(f.path, 'utf-8').catch(() => '');
|
|
281
|
+
if (!content)
|
|
282
|
+
return [];
|
|
283
|
+
const fileImports = extractImports(content, f.relativePath, f.ext);
|
|
284
|
+
allImports.push(...fileImports);
|
|
285
|
+
return extractSymbols(f.path, f.relativePath);
|
|
286
|
+
}));
|
|
287
|
+
symbols.push(...batchResults.flat());
|
|
288
|
+
}
|
|
289
|
+
const techStack = detectTechStack(files);
|
|
290
|
+
const tree = buildTree(files);
|
|
291
|
+
const totalFiles = files.length;
|
|
292
|
+
const totalCode = codeFiles.length;
|
|
293
|
+
const totalSize = files.reduce((sum, f) => sum + f.size, 0);
|
|
294
|
+
const sizeMB = (totalSize / 1_000_000).toFixed(1);
|
|
295
|
+
const summary = [
|
|
296
|
+
`Project: ${path.basename(root)}`,
|
|
297
|
+
`Files: ${totalFiles} total, ${totalCode} code files (${sizeMB} MB)`,
|
|
298
|
+
`Tech: ${techStack.join(', ') || 'Unknown'}`,
|
|
299
|
+
`Symbols: ${symbols.length} (${symbols.filter((s) => s.kind === 'function').length} functions, ${symbols.filter((s) => s.kind === 'class').length} classes, ${symbols.filter((s) => s.kind === 'interface').length} interfaces)`,
|
|
300
|
+
`Imports: ${allImports.length} local dependencies tracked`,
|
|
301
|
+
].join('\n');
|
|
302
|
+
return {
|
|
303
|
+
root,
|
|
304
|
+
files,
|
|
305
|
+
symbols,
|
|
306
|
+
imports: allImports,
|
|
307
|
+
tree,
|
|
308
|
+
techStack,
|
|
309
|
+
summary,
|
|
310
|
+
indexedAt: Date.now(),
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
const INDEX_CACHE_FILE = '.scalix/project-index.json';
|
|
314
|
+
const INDEX_CACHE_MAX_AGE_MS = 10 * 60 * 1000; // 10 minutes
|
|
315
|
+
export async function loadCachedIndex(root) {
|
|
316
|
+
try {
|
|
317
|
+
const cachePath = path.join(root, INDEX_CACHE_FILE);
|
|
318
|
+
const raw = await fs.readFile(cachePath, 'utf-8');
|
|
319
|
+
const cached = JSON.parse(raw);
|
|
320
|
+
if (Date.now() - cached.indexedAt < INDEX_CACHE_MAX_AGE_MS) {
|
|
321
|
+
return cached;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
catch { }
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
327
|
+
async function saveCachedIndex(root, index) {
|
|
328
|
+
try {
|
|
329
|
+
const cacheDir = path.join(root, '.scalix');
|
|
330
|
+
await fs.mkdir(cacheDir, { recursive: true });
|
|
331
|
+
const cachePath = path.join(root, INDEX_CACHE_FILE);
|
|
332
|
+
await fs.writeFile(cachePath, JSON.stringify(index), 'utf-8');
|
|
333
|
+
}
|
|
334
|
+
catch { }
|
|
335
|
+
}
|
|
336
|
+
export async function buildProjectIndex(root, forceRefresh = false) {
|
|
337
|
+
if (!forceRefresh) {
|
|
338
|
+
const cached = await loadCachedIndex(root);
|
|
339
|
+
if (cached)
|
|
340
|
+
return cached;
|
|
341
|
+
}
|
|
342
|
+
const index = await buildProjectIndexFresh(root);
|
|
343
|
+
await saveCachedIndex(root, index);
|
|
344
|
+
return index;
|
|
345
|
+
}
|
|
346
|
+
export function indexToContext(index) {
|
|
347
|
+
const topSymbols = index.symbols.slice(0, 100);
|
|
348
|
+
const symbolList = topSymbols
|
|
349
|
+
.map((s) => ` ${s.kind} ${s.name} (${s.file}:${s.line})`)
|
|
350
|
+
.join('\n');
|
|
351
|
+
// Build dependency graph summary: most-imported files
|
|
352
|
+
const importCounts = new Map();
|
|
353
|
+
for (const imp of index.imports) {
|
|
354
|
+
importCounts.set(imp.to, (importCounts.get(imp.to) ?? 0) + 1);
|
|
355
|
+
}
|
|
356
|
+
const topDeps = [...importCounts.entries()]
|
|
357
|
+
.sort((a, b) => b[1] - a[1])
|
|
358
|
+
.slice(0, 20)
|
|
359
|
+
.map(([dep, count]) => ` ${dep} (imported by ${count} files)`)
|
|
360
|
+
.join('\n');
|
|
361
|
+
return [
|
|
362
|
+
'## Project Overview',
|
|
363
|
+
index.summary,
|
|
364
|
+
'',
|
|
365
|
+
'## File Tree',
|
|
366
|
+
index.tree,
|
|
367
|
+
'',
|
|
368
|
+
'## Key Symbols',
|
|
369
|
+
symbolList || ' (no symbols extracted)',
|
|
370
|
+
'',
|
|
371
|
+
'## Key Dependencies',
|
|
372
|
+
topDeps || ' (no local imports tracked)',
|
|
373
|
+
].join('\n');
|
|
374
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { PROJECT_CONFIG_DIR } from './constants.js';
|
|
4
|
+
const SCALIX_DIR = PROJECT_CONFIG_DIR;
|
|
5
|
+
const MEMORY_FILE = 'memory.json';
|
|
6
|
+
function emptyMemory() {
|
|
7
|
+
return {
|
|
8
|
+
conventions: [],
|
|
9
|
+
patterns: [],
|
|
10
|
+
notes: [],
|
|
11
|
+
lastUpdated: new Date().toISOString(),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
function memoryPath(projectRoot) {
|
|
15
|
+
return path.join(projectRoot, SCALIX_DIR, MEMORY_FILE);
|
|
16
|
+
}
|
|
17
|
+
async function ensureDir(projectRoot) {
|
|
18
|
+
await fs.mkdir(path.join(projectRoot, SCALIX_DIR), { recursive: true });
|
|
19
|
+
}
|
|
20
|
+
export async function loadMemory(projectRoot) {
|
|
21
|
+
try {
|
|
22
|
+
const raw = await fs.readFile(memoryPath(projectRoot), 'utf-8');
|
|
23
|
+
const parsed = JSON.parse(raw);
|
|
24
|
+
return { ...emptyMemory(), ...parsed };
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return { ...emptyMemory() };
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export async function saveMemory(projectRoot, memory) {
|
|
31
|
+
await ensureDir(projectRoot);
|
|
32
|
+
memory.lastUpdated = new Date().toISOString();
|
|
33
|
+
await fs.writeFile(memoryPath(projectRoot), JSON.stringify(memory, null, 2), 'utf-8');
|
|
34
|
+
}
|
|
35
|
+
export async function addConvention(projectRoot, key, value, source = 'user') {
|
|
36
|
+
const memory = await loadMemory(projectRoot);
|
|
37
|
+
const existing = memory.conventions.findIndex((c) => c.key === key);
|
|
38
|
+
if (existing >= 0) {
|
|
39
|
+
memory.conventions[existing] = { key, value, source };
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
memory.conventions.push({ key, value, source });
|
|
43
|
+
}
|
|
44
|
+
await saveMemory(projectRoot, memory);
|
|
45
|
+
}
|
|
46
|
+
export async function addPattern(projectRoot, pattern) {
|
|
47
|
+
const memory = await loadMemory(projectRoot);
|
|
48
|
+
const existing = memory.patterns.findIndex((p) => p.name === pattern.name);
|
|
49
|
+
if (existing >= 0) {
|
|
50
|
+
memory.patterns[existing] = pattern;
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
memory.patterns.push(pattern);
|
|
54
|
+
}
|
|
55
|
+
await saveMemory(projectRoot, memory);
|
|
56
|
+
}
|
|
57
|
+
export async function addNote(projectRoot, note) {
|
|
58
|
+
const trimmed = note.trim();
|
|
59
|
+
if (!trimmed)
|
|
60
|
+
return;
|
|
61
|
+
const memory = await loadMemory(projectRoot);
|
|
62
|
+
if (!memory.notes.some((n) => n.toLowerCase() === trimmed.toLowerCase())) {
|
|
63
|
+
memory.notes.push(trimmed);
|
|
64
|
+
if (memory.notes.length > 200)
|
|
65
|
+
memory.notes = memory.notes.slice(-200);
|
|
66
|
+
await saveMemory(projectRoot, memory);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const CORRECTION_PATTERNS = [
|
|
70
|
+
/^no[,.]?\s+(don'?t|do not|never|stop|avoid|please don'?t)\s+/i,
|
|
71
|
+
/^(don'?t|do not|never|stop|avoid)\s+/i,
|
|
72
|
+
/^(always|make sure|remember to)\s+(?:use|avoid|check|ensure|handle|validate|include|add|run)\s+/i,
|
|
73
|
+
/^(use|prefer)\s+\S+\s+(instead|over|not)\s+/i,
|
|
74
|
+
];
|
|
75
|
+
export function detectCorrection(message) {
|
|
76
|
+
const trimmed = message.trim().split('\n')[0];
|
|
77
|
+
if (trimmed.length < 8 || trimmed.length > 200)
|
|
78
|
+
return null;
|
|
79
|
+
for (const p of CORRECTION_PATTERNS) {
|
|
80
|
+
if (p.test(trimmed))
|
|
81
|
+
return trimmed;
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
export async function autoDetectConventions(projectRoot) {
|
|
86
|
+
const conventions = [];
|
|
87
|
+
try {
|
|
88
|
+
const pkg = JSON.parse(await fs.readFile(path.join(projectRoot, 'package.json'), 'utf-8'));
|
|
89
|
+
if (pkg.type === 'module')
|
|
90
|
+
conventions.push({ key: 'module_system', value: 'ESM', source: 'auto' });
|
|
91
|
+
else
|
|
92
|
+
conventions.push({ key: 'module_system', value: 'CommonJS', source: 'auto' });
|
|
93
|
+
if (pkg.scripts?.test)
|
|
94
|
+
conventions.push({ key: 'test_command', value: pkg.scripts.test, source: 'auto' });
|
|
95
|
+
if (pkg.scripts?.lint)
|
|
96
|
+
conventions.push({ key: 'lint_command', value: pkg.scripts.lint, source: 'auto' });
|
|
97
|
+
if (pkg.scripts?.build)
|
|
98
|
+
conventions.push({ key: 'build_command', value: pkg.scripts.build, source: 'auto' });
|
|
99
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
100
|
+
if (deps.prettier)
|
|
101
|
+
conventions.push({ key: 'formatter', value: 'Prettier', source: 'auto' });
|
|
102
|
+
if (deps.eslint)
|
|
103
|
+
conventions.push({ key: 'linter', value: 'ESLint', source: 'auto' });
|
|
104
|
+
if (deps.vitest)
|
|
105
|
+
conventions.push({ key: 'test_framework', value: 'Vitest', source: 'auto' });
|
|
106
|
+
if (deps.jest)
|
|
107
|
+
conventions.push({ key: 'test_framework', value: 'Jest', source: 'auto' });
|
|
108
|
+
if (deps.mocha)
|
|
109
|
+
conventions.push({ key: 'test_framework', value: 'Mocha', source: 'auto' });
|
|
110
|
+
if (deps.tailwindcss)
|
|
111
|
+
conventions.push({ key: 'css_framework', value: 'Tailwind CSS', source: 'auto' });
|
|
112
|
+
if (deps.prisma || deps['@prisma/client'])
|
|
113
|
+
conventions.push({ key: 'orm', value: 'Prisma', source: 'auto' });
|
|
114
|
+
if (deps.drizzle)
|
|
115
|
+
conventions.push({ key: 'orm', value: 'Drizzle', source: 'auto' });
|
|
116
|
+
}
|
|
117
|
+
catch { }
|
|
118
|
+
try {
|
|
119
|
+
const tsconfig = JSON.parse(await fs.readFile(path.join(projectRoot, 'tsconfig.json'), 'utf-8'));
|
|
120
|
+
if (tsconfig.compilerOptions?.strict)
|
|
121
|
+
conventions.push({ key: 'typescript_strict', value: 'true', source: 'auto' });
|
|
122
|
+
if (tsconfig.compilerOptions?.target)
|
|
123
|
+
conventions.push({ key: 'ts_target', value: tsconfig.compilerOptions.target, source: 'auto' });
|
|
124
|
+
}
|
|
125
|
+
catch { }
|
|
126
|
+
try {
|
|
127
|
+
await fs.access(path.join(projectRoot, '.prettierrc'));
|
|
128
|
+
conventions.push({ key: 'has_prettier_config', value: 'true', source: 'auto' });
|
|
129
|
+
}
|
|
130
|
+
catch { }
|
|
131
|
+
try {
|
|
132
|
+
const gitignore = await fs.readFile(path.join(projectRoot, '.gitignore'), 'utf-8');
|
|
133
|
+
if (gitignore.includes('.env'))
|
|
134
|
+
conventions.push({ key: 'env_files_ignored', value: 'true', source: 'auto' });
|
|
135
|
+
}
|
|
136
|
+
catch { }
|
|
137
|
+
return conventions;
|
|
138
|
+
}
|
|
139
|
+
export function memoryToContext(memory) {
|
|
140
|
+
const lines = ['## Project Memory'];
|
|
141
|
+
if (memory.conventions.length > 0) {
|
|
142
|
+
lines.push('');
|
|
143
|
+
lines.push('### Conventions');
|
|
144
|
+
for (const c of memory.conventions) {
|
|
145
|
+
lines.push(` ${c.key}: ${c.value}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (memory.patterns.length > 0) {
|
|
149
|
+
lines.push('');
|
|
150
|
+
lines.push('### Patterns');
|
|
151
|
+
for (const p of memory.patterns) {
|
|
152
|
+
lines.push(` ${p.name}: ${p.description}`);
|
|
153
|
+
if (p.example)
|
|
154
|
+
lines.push(` Example: ${p.example}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (memory.notes.length > 0) {
|
|
158
|
+
lines.push('');
|
|
159
|
+
lines.push('### Notes');
|
|
160
|
+
for (const n of memory.notes.slice(-10)) {
|
|
161
|
+
lines.push(` - ${n}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return lines.join('\n');
|
|
165
|
+
}
|