scalix 0.0.1 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/CHANGELOG.md +122 -0
  2. package/LICENSE +100 -0
  3. package/README.md +300 -2
  4. package/app.py +55 -0
  5. package/bin/scalix.js +19 -0
  6. package/dist/agent-replay.js +166 -0
  7. package/dist/agent.js +852 -0
  8. package/dist/auto-verify.js +136 -0
  9. package/dist/change-tracker.js +53 -0
  10. package/dist/cli.js +2329 -0
  11. package/dist/commands.js +71 -0
  12. package/dist/config.js +16 -0
  13. package/dist/constants.js +324 -0
  14. package/dist/context-builder.js +151 -0
  15. package/dist/context-manager.js +148 -0
  16. package/dist/cost-tracker.js +85 -0
  17. package/dist/diff.js +140 -0
  18. package/dist/git-context.js +180 -0
  19. package/dist/git-undo.js +67 -0
  20. package/dist/headless.js +654 -0
  21. package/dist/hooks.js +94 -0
  22. package/dist/ipc-server.js +228 -0
  23. package/dist/logger.js +45 -0
  24. package/dist/markdown.js +41 -0
  25. package/dist/mcp.js +49 -0
  26. package/dist/model-router.js +68 -0
  27. package/dist/plugins.js +132 -0
  28. package/dist/project-index.js +374 -0
  29. package/dist/project-memory.js +165 -0
  30. package/dist/safe-math.js +194 -0
  31. package/dist/sandbox.js +279 -0
  32. package/dist/scoped-rules.js +82 -0
  33. package/dist/sessions.js +71 -0
  34. package/dist/settings.js +155 -0
  35. package/dist/sub-agent.js +79 -0
  36. package/dist/token-counter.js +47 -0
  37. package/dist/tool-helper.js +97 -0
  38. package/dist/tool-selector.js +77 -0
  39. package/dist/tools-advanced.js +232 -0
  40. package/dist/tools-browser.js +273 -0
  41. package/dist/tools-extra.js +383 -0
  42. package/dist/tools-vision.js +115 -0
  43. package/dist/tools-voice.js +147 -0
  44. package/dist/tools-web.js +346 -0
  45. package/dist/tools.js +370 -0
  46. package/dist/types.js +1 -0
  47. package/docs/DISTRIBUTION.md +301 -0
  48. package/package.json +67 -6
@@ -0,0 +1,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
+ }