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.
- package/.env.example +29 -0
- package/CHANGELOG.md +122 -0
- package/LICENSE +100 -0
- package/README.md +300 -2
- package/bin/scalix.js +19 -0
- package/dist/agent-definitions.js +100 -0
- package/dist/agent-registry.js +226 -0
- package/dist/agent-replay.js +166 -0
- package/dist/agent.js +1130 -0
- package/dist/api-client.js +77 -0
- package/dist/auto-verify.js +136 -0
- package/dist/change-tracker.js +53 -0
- package/dist/cli.js +2454 -0
- package/dist/commands.js +71 -0
- package/dist/config.js +16 -0
- package/dist/constants.js +358 -0
- package/dist/context-builder.js +158 -0
- package/dist/context-manager.js +148 -0
- package/dist/context-retriever.js +257 -0
- package/dist/cost-tracker.js +85 -0
- package/dist/diff.js +140 -0
- package/dist/git-context.js +183 -0
- package/dist/git-undo.js +67 -0
- package/dist/grounding-checker.js +154 -0
- package/dist/headless.js +743 -0
- package/dist/hooks.js +100 -0
- package/dist/injection-detector.js +86 -0
- package/dist/ipc-server.js +232 -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 +376 -0
- package/dist/project-memory.js +165 -0
- package/dist/safe-math.js +194 -0
- package/dist/sandbox.js +403 -0
- package/dist/scoped-rules.js +82 -0
- package/dist/sessions.js +106 -0
- package/dist/settings.js +260 -0
- package/dist/sub-agent.js +154 -0
- package/dist/task-board.js +105 -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 +487 -0
- package/dist/tools-browser.js +534 -0
- package/dist/tools-extra.js +390 -0
- package/dist/tools-vision.js +116 -0
- package/dist/tools-voice.js +147 -0
- package/dist/tools-web.js +346 -0
- package/dist/tools.js +380 -0
- package/dist/types.js +1 -0
- package/dist/update-checker.js +111 -0
- package/dist/worktree.js +172 -0
- package/docs/DISTRIBUTION.md +301 -0
- package/package.json +71 -6
package/dist/plugins.js
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { pathToFileURL } from 'node:url';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { PROJECT_CONFIG_DIR, PLUGIN_DEFAULTS } from './constants.js';
|
|
6
|
+
const PLUGINS_DIR = `${PROJECT_CONFIG_DIR}/plugins`;
|
|
7
|
+
const manifestSchema = z.object({
|
|
8
|
+
name: z.string(),
|
|
9
|
+
version: z.string().default('1.0.0'),
|
|
10
|
+
description: z.string().default(''),
|
|
11
|
+
tools: z.array(z.object({
|
|
12
|
+
name: z.string(),
|
|
13
|
+
description: z.string(),
|
|
14
|
+
inputSchema: z.record(z.string(), z.unknown()).default({}),
|
|
15
|
+
handler: z.string(),
|
|
16
|
+
})),
|
|
17
|
+
permissions: z.object({
|
|
18
|
+
allowedTools: z.array(z.string()).optional(),
|
|
19
|
+
blockedTools: z.array(z.string()).optional(),
|
|
20
|
+
permissionMode: z.enum(['plan', 'default', 'auto']).optional(),
|
|
21
|
+
}).optional(),
|
|
22
|
+
});
|
|
23
|
+
let _pluginCache = new Map();
|
|
24
|
+
const CACHE_TTL_MS = PLUGIN_DEFAULTS.cacheTtlMs;
|
|
25
|
+
async function loadPluginHandler(pluginDir, handlerPath) {
|
|
26
|
+
const fullPath = path.resolve(pluginDir, handlerPath);
|
|
27
|
+
const normalizedFull = path.normalize(fullPath);
|
|
28
|
+
const normalizedDir = path.normalize(pluginDir) + path.sep;
|
|
29
|
+
if (!normalizedFull.startsWith(normalizedDir)) {
|
|
30
|
+
throw new Error(`Plugin handler path escapes plugin directory: ${handlerPath}`);
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
await fs.access(fullPath);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
throw new Error(`Plugin handler not found: ${fullPath}`);
|
|
37
|
+
}
|
|
38
|
+
const fileUrl = pathToFileURL(fullPath).href;
|
|
39
|
+
const mod = await import(fileUrl);
|
|
40
|
+
if (typeof mod.default === 'function')
|
|
41
|
+
return mod.default;
|
|
42
|
+
if (typeof mod.execute === 'function')
|
|
43
|
+
return mod.execute;
|
|
44
|
+
if (typeof mod.handler === 'function')
|
|
45
|
+
return mod.handler;
|
|
46
|
+
throw new Error(`Plugin handler ${handlerPath} must export default, execute, or handler function`);
|
|
47
|
+
}
|
|
48
|
+
export async function loadPlugin(pluginDir) {
|
|
49
|
+
const manifestPath = path.join(pluginDir, 'plugin.json');
|
|
50
|
+
try {
|
|
51
|
+
await fs.access(manifestPath);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
const raw = await fs.readFile(manifestPath, 'utf-8');
|
|
58
|
+
const parsed = JSON.parse(raw);
|
|
59
|
+
const manifest = manifestSchema.parse(parsed);
|
|
60
|
+
const tools = [];
|
|
61
|
+
for (const toolDef of manifest.tools) {
|
|
62
|
+
try {
|
|
63
|
+
const handler = await loadPluginHandler(pluginDir, toolDef.handler);
|
|
64
|
+
tools.push({
|
|
65
|
+
type: 'function',
|
|
66
|
+
function: {
|
|
67
|
+
name: `plugin_${manifest.name}_${toolDef.name}`,
|
|
68
|
+
description: `[Plugin: ${manifest.name}] ${toolDef.description}`,
|
|
69
|
+
parameters: toolDef.inputSchema,
|
|
70
|
+
execute: handler,
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
process.stderr.write(`Warning: failed to load plugin tool ${toolDef.name}: ${err.message}\n`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return { manifest, tools, path: pluginDir };
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
process.stderr.write(`Warning: failed to load plugin from ${pluginDir}: ${err.message}\n`);
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
export async function discoverPlugins(projectRoot) {
|
|
86
|
+
const cached = _pluginCache.get(projectRoot);
|
|
87
|
+
if (cached && Date.now() - cached.timestamp < CACHE_TTL_MS) {
|
|
88
|
+
return cached.plugins;
|
|
89
|
+
}
|
|
90
|
+
const pluginsDir = path.join(projectRoot, PLUGINS_DIR);
|
|
91
|
+
const plugins = [];
|
|
92
|
+
try {
|
|
93
|
+
await fs.access(pluginsDir);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return [];
|
|
97
|
+
}
|
|
98
|
+
try {
|
|
99
|
+
const entries = await fs.readdir(pluginsDir, { withFileTypes: true });
|
|
100
|
+
for (const entry of entries) {
|
|
101
|
+
if (!entry.isDirectory())
|
|
102
|
+
continue;
|
|
103
|
+
const pluginDir = path.join(pluginsDir, entry.name);
|
|
104
|
+
const plugin = await loadPlugin(pluginDir);
|
|
105
|
+
if (plugin)
|
|
106
|
+
plugins.push(plugin);
|
|
107
|
+
}
|
|
108
|
+
const rootPlugin = await loadPlugin(pluginsDir);
|
|
109
|
+
if (rootPlugin)
|
|
110
|
+
plugins.push(rootPlugin);
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
return [];
|
|
114
|
+
}
|
|
115
|
+
_pluginCache.set(projectRoot, { plugins, timestamp: Date.now() });
|
|
116
|
+
return plugins;
|
|
117
|
+
}
|
|
118
|
+
export function getPluginTools(plugins) {
|
|
119
|
+
return plugins.flatMap((p) => p.tools);
|
|
120
|
+
}
|
|
121
|
+
export function pluginsSummary(plugins) {
|
|
122
|
+
if (plugins.length === 0)
|
|
123
|
+
return 'No plugins loaded.';
|
|
124
|
+
const lines = [`${plugins.length} plugin(s) loaded:`];
|
|
125
|
+
for (const p of plugins) {
|
|
126
|
+
lines.push(` ${p.manifest.name} v${p.manifest.version} (${p.tools.length} tools)`);
|
|
127
|
+
for (const t of p.tools) {
|
|
128
|
+
lines.push(` - ${t.function.name}: ${t.function.description}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return lines.join('\n');
|
|
132
|
+
}
|
|
@@ -0,0 +1,376 @@
|
|
|
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
|
+
if (f.size > PROJECT_INDEX_DEFAULTS.symbolParseMaxChars)
|
|
281
|
+
return [];
|
|
282
|
+
const content = await fs.readFile(f.path, 'utf-8').catch(() => '');
|
|
283
|
+
if (!content)
|
|
284
|
+
return [];
|
|
285
|
+
const fileImports = extractImports(content, f.relativePath, f.ext);
|
|
286
|
+
allImports.push(...fileImports);
|
|
287
|
+
return extractSymbols(f.path, f.relativePath);
|
|
288
|
+
}));
|
|
289
|
+
symbols.push(...batchResults.flat());
|
|
290
|
+
}
|
|
291
|
+
const techStack = detectTechStack(files);
|
|
292
|
+
const tree = buildTree(files);
|
|
293
|
+
const totalFiles = files.length;
|
|
294
|
+
const totalCode = codeFiles.length;
|
|
295
|
+
const totalSize = files.reduce((sum, f) => sum + f.size, 0);
|
|
296
|
+
const sizeMB = (totalSize / 1_000_000).toFixed(1);
|
|
297
|
+
const summary = [
|
|
298
|
+
`Project: ${path.basename(root)}`,
|
|
299
|
+
`Files: ${totalFiles} total, ${totalCode} code files (${sizeMB} MB)`,
|
|
300
|
+
`Tech: ${techStack.join(', ') || 'Unknown'}`,
|
|
301
|
+
`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)`,
|
|
302
|
+
`Imports: ${allImports.length} local dependencies tracked`,
|
|
303
|
+
].join('\n');
|
|
304
|
+
return {
|
|
305
|
+
root,
|
|
306
|
+
files,
|
|
307
|
+
symbols,
|
|
308
|
+
imports: allImports,
|
|
309
|
+
tree,
|
|
310
|
+
techStack,
|
|
311
|
+
summary,
|
|
312
|
+
indexedAt: Date.now(),
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
const INDEX_CACHE_FILE = '.scalix/project-index.json';
|
|
316
|
+
const INDEX_CACHE_MAX_AGE_MS = 10 * 60 * 1000; // 10 minutes
|
|
317
|
+
export async function loadCachedIndex(root) {
|
|
318
|
+
try {
|
|
319
|
+
const cachePath = path.join(root, INDEX_CACHE_FILE);
|
|
320
|
+
const raw = await fs.readFile(cachePath, 'utf-8');
|
|
321
|
+
const cached = JSON.parse(raw);
|
|
322
|
+
if (Date.now() - cached.indexedAt < INDEX_CACHE_MAX_AGE_MS) {
|
|
323
|
+
return cached;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
catch { }
|
|
327
|
+
return null;
|
|
328
|
+
}
|
|
329
|
+
async function saveCachedIndex(root, index) {
|
|
330
|
+
try {
|
|
331
|
+
const cacheDir = path.join(root, '.scalix');
|
|
332
|
+
await fs.mkdir(cacheDir, { recursive: true });
|
|
333
|
+
const cachePath = path.join(root, INDEX_CACHE_FILE);
|
|
334
|
+
await fs.writeFile(cachePath, JSON.stringify(index), 'utf-8');
|
|
335
|
+
}
|
|
336
|
+
catch { }
|
|
337
|
+
}
|
|
338
|
+
export async function buildProjectIndex(root, forceRefresh = false) {
|
|
339
|
+
if (!forceRefresh) {
|
|
340
|
+
const cached = await loadCachedIndex(root);
|
|
341
|
+
if (cached)
|
|
342
|
+
return cached;
|
|
343
|
+
}
|
|
344
|
+
const index = await buildProjectIndexFresh(root);
|
|
345
|
+
await saveCachedIndex(root, index);
|
|
346
|
+
return index;
|
|
347
|
+
}
|
|
348
|
+
export function indexToContext(index) {
|
|
349
|
+
const topSymbols = index.symbols.slice(0, 100);
|
|
350
|
+
const symbolList = topSymbols
|
|
351
|
+
.map((s) => ` ${s.kind} ${s.name} (${s.file}:${s.line})`)
|
|
352
|
+
.join('\n');
|
|
353
|
+
// Build dependency graph summary: most-imported files
|
|
354
|
+
const importCounts = new Map();
|
|
355
|
+
for (const imp of index.imports) {
|
|
356
|
+
importCounts.set(imp.to, (importCounts.get(imp.to) ?? 0) + 1);
|
|
357
|
+
}
|
|
358
|
+
const topDeps = [...importCounts.entries()]
|
|
359
|
+
.sort((a, b) => b[1] - a[1])
|
|
360
|
+
.slice(0, 20)
|
|
361
|
+
.map(([dep, count]) => ` ${dep} (imported by ${count} files)`)
|
|
362
|
+
.join('\n');
|
|
363
|
+
return [
|
|
364
|
+
'## Project Overview',
|
|
365
|
+
index.summary,
|
|
366
|
+
'',
|
|
367
|
+
'## File Tree',
|
|
368
|
+
index.tree,
|
|
369
|
+
'',
|
|
370
|
+
'## Key Symbols',
|
|
371
|
+
symbolList || ' (no symbols extracted)',
|
|
372
|
+
'',
|
|
373
|
+
'## Key Dependencies',
|
|
374
|
+
topDeps || ' (no local imports tracked)',
|
|
375
|
+
].join('\n');
|
|
376
|
+
}
|
|
@@ -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
|
+
}
|