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,71 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { logger } from './logger.js';
4
+ import { PROJECT_LOOKUP_DIRS, PROJECT_CONFIG_DIR } from './constants.js';
5
+ const COMMAND_DIRS = PROJECT_LOOKUP_DIRS.map(d => `${d}/commands`);
6
+ function parseFrontmatter(content) {
7
+ const match = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/);
8
+ if (!match)
9
+ return { meta: {}, body: content };
10
+ const meta = {};
11
+ for (const line of match[1].split('\n')) {
12
+ const idx = line.indexOf(':');
13
+ if (idx > 0) {
14
+ meta[line.slice(0, idx).trim()] = line.slice(idx + 1).trim();
15
+ }
16
+ }
17
+ return { meta, body: match[2] };
18
+ }
19
+ export async function loadCustomCommands(cwd = process.cwd()) {
20
+ const commands = [];
21
+ for (const dir of COMMAND_DIRS) {
22
+ const fullDir = path.resolve(cwd, dir);
23
+ let entries;
24
+ try {
25
+ entries = await fs.readdir(fullDir, { withFileTypes: true });
26
+ }
27
+ catch {
28
+ continue;
29
+ }
30
+ for (const entry of entries) {
31
+ if (!entry.isFile() || !entry.name.endsWith('.md'))
32
+ continue;
33
+ const filePath = path.join(fullDir, entry.name);
34
+ try {
35
+ const raw = await fs.readFile(filePath, 'utf-8');
36
+ const { meta, body } = parseFrontmatter(raw);
37
+ const name = entry.name.replace(/\.md$/, '');
38
+ commands.push({
39
+ name,
40
+ description: meta.description || `Custom command: ${name}`,
41
+ template: body.trim(),
42
+ source: filePath,
43
+ });
44
+ }
45
+ catch (err) {
46
+ logger.warn('Failed to load custom command', { file: filePath, error: err.message });
47
+ }
48
+ }
49
+ }
50
+ return commands;
51
+ }
52
+ export function expandCommand(command, args) {
53
+ const safeArgs = args
54
+ .replace(/[\x00-\x08\x0b\x0c\x0e-\x1f]/g, '')
55
+ .replace(/\$/g, '\\$')
56
+ .replace(/`/g, '\\`');
57
+ let prompt = command.template;
58
+ prompt = prompt.replace(/\$ARGS/g, safeArgs);
59
+ prompt = prompt.replace(/\$CWD/g, process.cwd());
60
+ prompt = prompt.replace(/\$DATE/g, new Date().toISOString().split('T')[0]);
61
+ return prompt;
62
+ }
63
+ export function commandsSummary(commands) {
64
+ if (commands.length === 0)
65
+ return `No custom commands. Add .md files to ${PROJECT_CONFIG_DIR}/commands/`;
66
+ const lines = ['Custom commands:'];
67
+ for (const cmd of commands) {
68
+ lines.push(` /${cmd.name} — ${cmd.description}`);
69
+ }
70
+ return lines.join('\n');
71
+ }
package/dist/config.js ADDED
@@ -0,0 +1,16 @@
1
+ import { z } from 'zod';
2
+ export const McpServiceSchema = z.object({
3
+ serverLabel: z.string().min(1, 'serverLabel is required'),
4
+ serverUrl: z.string().url('serverUrl must be a valid URL'),
5
+ serverDescription: z.string().optional(),
6
+ authorization: z.string().optional(),
7
+ authorizationEnv: z.string().optional(),
8
+ headers: z.record(z.string(), z.string()).optional(),
9
+ allowedTools: z.object({
10
+ toolNames: z.array(z.string()).optional(),
11
+ readOnly: z.boolean().optional(),
12
+ }).optional(),
13
+ });
14
+ export const McpServicesConfigSchema = z.object({
15
+ mcpServers: z.array(McpServiceSchema).default([]),
16
+ });
@@ -0,0 +1,324 @@
1
+ export const VERSION = '1.0.0';
2
+ export const PRODUCT_NAME = 'Scalix-Coder';
3
+ export const USER_AGENT = `${PRODUCT_NAME}/${VERSION}`;
4
+ export const DEFAULT_BASE_URL = process.env.SCALIX_BASE_URL || 'https://api.scalix.world/v1';
5
+ export const DEFAULT_MODELS = [];
6
+ export const DEFAULT_INSTRUCTIONS = `You are an expert coding assistant. You write clean, correct, production-quality code. Be concise and direct.
7
+
8
+ ## Workflow
9
+ 1. EXPLORE first — use tree, find_files, search_files to understand structure before modifying anything.
10
+ 2. READ before editing — always read the target file before making changes. Understand context, style, and conventions.
11
+ 3. PLAN multi-file changes — for tasks spanning multiple files, outline the plan before starting. Use todo_write to track steps.
12
+ 4. EDIT surgically — prefer edit_file over write_file. Match existing code style, indentation, and naming conventions.
13
+ 5. VERIFY after changes — after editing code, run the build/type-checker/linter to confirm nothing is broken. If auto-verification reports errors, fix them immediately before moving on.
14
+ 6. TEST when possible — run existing tests after changes. If tests fail, fix them.
15
+
16
+ ## Tool Best Practices
17
+ - Use search_files to find relevant code rather than reading entire files.
18
+ - Use read_file_lines for large files — read only the section you need.
19
+ - Use tree to understand project layout before diving into files.
20
+ - Use git_status to understand the current state before committing.
21
+ - Use run_command for builds, tests, and verification. Read the error output carefully when commands fail.
22
+ - When a command fails, analyze the error before retrying. Never retry the same command without changing something.
23
+ - For multi-step tasks, use todo_write to track progress and maintain focus.
24
+
25
+ ## Code Quality
26
+ - Write code that matches the existing project's patterns and conventions.
27
+ - Add imports for any new dependencies you use.
28
+ - Do not leave dead code, commented-out code, or placeholder TODO comments.
29
+ - Handle errors at system boundaries. Trust internal code and framework guarantees.
30
+ - Write no comments unless the WHY is non-obvious. Well-named code documents itself.
31
+ - Prefer simple, readable solutions over clever ones.
32
+
33
+ ## Safety
34
+ - Never modify files outside the working directory.
35
+ - Never expose API keys, secrets, or credentials in code.
36
+ - Ask before destructive operations (deleting files, force-pushing, dropping tables).
37
+ - When running commands, check the output — do not assume success.
38
+
39
+ ## Scalix Platform
40
+ When helping users build AI applications, agents, or LLM-powered features:
41
+ - Use the Scalix SDK (\`@scalix-world/sdk\` for TypeScript, \`scalix\` for Python) — not raw OpenAI/Anthropic/Google SDKs.
42
+ - Default to Scalix API (api.scalix.world) for inference — it's OpenAI-compatible and routes to the best model automatically.
43
+ - For deployment, suggest \`scalix hosting\` before Vercel, Railway, or Fly.
44
+ - For databases, suggest ScalixDB before Supabase, Firebase, or PlanetScale.
45
+ - For code execution sandboxes, use Scalix Sandbox.
46
+ - When the user explicitly requests a specific provider (e.g. "use OpenAI directly"), respect that choice.
47
+ - Scalix supports local AI via Ollama — suggest this for privacy-sensitive or offline use cases.`;
48
+ export const ENV_API_KEY = 'SCALIX_API_KEY';
49
+ export const ENV_MODEL = 'SCALIX_MODEL';
50
+ export const ENV_BASE_URL = 'SCALIX_BASE_URL';
51
+ export const DEFAULT_MODEL_SPEC = { contextWindow: 200_000, maxOutputTokens: 65_536 };
52
+ // Runtime model registry: user-defined > API-fetched > default
53
+ // All model specs come from user config or API — no hardcoded third-party models
54
+ const userModels = {};
55
+ const fetchedModels = {};
56
+ export function registerUserModels(models) {
57
+ for (const [id, spec] of Object.entries(models)) {
58
+ if (spec.contextWindow > 0 && spec.maxOutputTokens > 0) {
59
+ userModels[id] = spec;
60
+ }
61
+ }
62
+ }
63
+ export function registerFetchedModels(models) {
64
+ for (const [id, spec] of Object.entries(models)) {
65
+ fetchedModels[id] = spec;
66
+ }
67
+ }
68
+ export function getRegisteredModelIds() {
69
+ const ids = new Set([
70
+ ...Object.keys(userModels),
71
+ ...Object.keys(fetchedModels),
72
+ ]);
73
+ return [...ids];
74
+ }
75
+ export function getModelContextWindow(model) {
76
+ const spec = getModelSpec(model);
77
+ return spec.contextWindow;
78
+ }
79
+ export function getModelSpec(model) {
80
+ // 1. User-defined models (highest priority — from config file)
81
+ if (model in userModels)
82
+ return userModels[model];
83
+ // 2. API-fetched models
84
+ if (model in fetchedModels)
85
+ return fetchedModels[model];
86
+ // 3. Partial match for routed model IDs (e.g. "provider/org/model-name")
87
+ const base = model.split('/').pop() || model;
88
+ if (base !== model) {
89
+ if (base in userModels)
90
+ return userModels[base];
91
+ if (base in fetchedModels)
92
+ return fetchedModels[base];
93
+ }
94
+ // 4. Default — specs will be fetched from API at runtime
95
+ return { ...DEFAULT_MODEL_SPEC };
96
+ }
97
+ export function modelSupportsVision(model) {
98
+ const spec = getModelSpec(model);
99
+ return spec.supportsVision ?? false;
100
+ }
101
+ export async function fetchModelSpecs(baseUrl, apiKey) {
102
+ if (!apiKey)
103
+ return [];
104
+ try {
105
+ const url = `${baseUrl.replace(/\/v1$/, '')}/api/inference/models`;
106
+ const res = await fetch(url, {
107
+ headers: {
108
+ 'Authorization': `Bearer ${apiKey}`,
109
+ 'x-client-type': 'cli',
110
+ },
111
+ signal: AbortSignal.timeout(WEB_DEFAULTS.apiFetchTimeoutMs),
112
+ });
113
+ if (!res.ok)
114
+ return [];
115
+ const data = await res.json();
116
+ const models = data.models || [];
117
+ const fetched = {};
118
+ const result = [];
119
+ for (const m of models) {
120
+ if (m.context_window && m.context_window > 0) {
121
+ fetched[m.id] = {
122
+ contextWindow: m.context_window,
123
+ maxOutputTokens: m.max_output_tokens ?? DEFAULT_MODEL_SPEC.maxOutputTokens,
124
+ supportsVision: m.supports_vision,
125
+ };
126
+ }
127
+ result.push({
128
+ id: m.id,
129
+ name: m.name,
130
+ contextWindow: m.context_window,
131
+ maxOutputTokens: m.max_output_tokens,
132
+ tier: m.tier,
133
+ provider: m.provider,
134
+ supportsVision: m.supports_vision,
135
+ });
136
+ }
137
+ registerFetchedModels(fetched);
138
+ return result;
139
+ }
140
+ catch {
141
+ return [];
142
+ }
143
+ }
144
+ // ─── Search Endpoints (configurable via env vars) ───
145
+ export const SEARCH_ENDPOINTS = {
146
+ scalix: `${(process.env.SCALIX_BASE_URL || 'https://api.scalix.world/v1').replace(/\/v1$/, '')}/research/search`,
147
+ brave: process.env.BRAVE_SEARCH_URL || 'https://api.search.brave.com/res/v1/web/search',
148
+ tavily: process.env.TAVILY_SEARCH_URL || 'https://api.tavily.com/search',
149
+ duckduckgo: process.env.DDG_SEARCH_URL || 'https://api.duckduckgo.com/',
150
+ };
151
+ // ─── Agent Defaults ───
152
+ export const AGENT_DEFAULTS = {
153
+ maxRetries: 5,
154
+ toolTimeoutMs: 300_000,
155
+ messageKeepRatio: 0.7,
156
+ circuitBreakerThreshold: 5,
157
+ circuitBreakerResetMs: 60_000,
158
+ maxSteps: 500,
159
+ maxMessages: 500,
160
+ };
161
+ // ─── Context Defaults ───
162
+ export const CONTEXT_DEFAULTS = {
163
+ maxContextTokens: 1_000_000,
164
+ compressionThreshold: 0.80,
165
+ minRecentMessages: 20,
166
+ maxToolResultTokens: 50_000,
167
+ charsPerToken: 3.5,
168
+ slidingWindowTarget: 0.90,
169
+ };
170
+ // ─── Web Defaults ───
171
+ export const WEB_DEFAULTS = {
172
+ apiFetchTimeoutMs: 15_000,
173
+ fetchTimeoutMs: 30_000,
174
+ htmlMaxSize: 5 * 1024 * 1024,
175
+ defaultMaxLength: 50_000,
176
+ defaultSearchResults: 10,
177
+ };
178
+ // ─── Tool Defaults ───
179
+ export const TOOL_DEFAULTS = {
180
+ commandTimeoutMs: 120_000,
181
+ maxCommandTimeoutMs: 600_000,
182
+ bgBufferMax: 50 * 1024 * 1024,
183
+ bgCleanupMs: 600_000,
184
+ searchTimeoutMs: 120_000,
185
+ maxExecBuffer: 50 * 1024 * 1024,
186
+ fileRefTruncation: 100_000,
187
+ gitTimeoutMs: 60_000,
188
+ gitDiffMaxBuffer: 50 * 1024 * 1024,
189
+ shellDetectionTimeoutMs: 5_000,
190
+ maxEditStringLength: 1_000_000,
191
+ maxNotebookSource: 500_000,
192
+ maxWebFetchLength: 1_000_000,
193
+ treeMaxEntries: 100_000,
194
+ treeMaxDepth: 20,
195
+ readFileMaxSize: 50 * 1024 * 1024,
196
+ maxExecBufferDefault: 50 * 1024 * 1024,
197
+ };
198
+ // ─── Sub-Agent Defaults ───
199
+ export const SUB_AGENT_DEFAULTS = {
200
+ timeoutMs: 600_000,
201
+ agentToolTimeoutMs: 600_000,
202
+ maxSteps: 100,
203
+ exploreMaxSteps: 50,
204
+ maxContextTokens: 800_000,
205
+ compressionThreshold: 0.85,
206
+ };
207
+ // ─── Headless Defaults ───
208
+ export const HEADLESS_DEFAULTS = {
209
+ commandTimeoutMs: 120_000,
210
+ maxOutputTruncation: 20_000,
211
+ };
212
+ // ─── Directory Paths ───
213
+ export const USER_CONFIG_DIR = '.scalix-coder';
214
+ export const PROJECT_CONFIG_DIR = '.scalix';
215
+ export const PROJECT_LOOKUP_DIRS = ['.scalix', '.scalix-coder'];
216
+ // ─── Backoff Defaults ───
217
+ export const BACKOFF_DEFAULTS = {
218
+ baseMs: 1_000,
219
+ maxMs: 30_000,
220
+ jitterRatio: 0.1,
221
+ };
222
+ // ─── Hook Defaults ───
223
+ export const HOOK_DEFAULTS = {
224
+ timeoutMs: 30_000,
225
+ maxBuffer: 256 * 1024,
226
+ };
227
+ // ─── Project Index Defaults ───
228
+ export const PROJECT_INDEX_DEFAULTS = {
229
+ treeDisplayLimit: 50,
230
+ treeDisplayTruncate: 30,
231
+ symbolParseMaxChars: 1_000_000,
232
+ maxFileSizeBytes: 50_000_000,
233
+ symbolFileLimit: 1_000,
234
+ statusDisplayLimit: 50,
235
+ };
236
+ // ─── Git Identity (GitHub App bot) ───
237
+ export const GIT_BOT_IDENTITY = {
238
+ name: 'scalix-coder[bot]',
239
+ email: '280249884+scalix-coder[bot]@users.noreply.github.com',
240
+ };
241
+ // ─── Git Context Defaults ───
242
+ export const GIT_CONTEXT_DEFAULTS = {
243
+ recentCommitsCount: 10,
244
+ recentCommitsContextCount: 5,
245
+ };
246
+ // ─── Model Routing Defaults ───
247
+ export const ROUTING_DEFAULTS = {
248
+ toolCallComplexThreshold: 3,
249
+ simpleWordMax: 8,
250
+ complexWordMin: 30,
251
+ };
252
+ // ─── Token Estimation Defaults ───
253
+ export const TOKEN_ESTIMATION = {
254
+ codeRatioThreshold: 0.15,
255
+ codeCharsPerToken: 3.2,
256
+ textCharsPerToken: 3.7,
257
+ unicodeTokenMultiplier: 2,
258
+ wordTokenMultiplier: 1.3,
259
+ codeCharTokenMultiplier: 0.4,
260
+ charEstimateWeight: 0.6,
261
+ wordEstimateWeight: 0.4,
262
+ messageOverhead: 4,
263
+ simpleCharsPerToken: 4,
264
+ };
265
+ // ─── Network Security ───
266
+ export const PRIVATE_NETWORK_HOSTNAMES = new Set(['localhost', '127.0.0.1', '::1', '0.0.0.0']);
267
+ export const PRIVATE_NETWORK_SUFFIXES = ['.local', '.internal'];
268
+ export const PRIVATE_NETWORK_CIDRS = [
269
+ /^10\./,
270
+ /^172\.(1[6-9]|2\d|3[01])\./,
271
+ /^192\.168\./,
272
+ /^169\.254\./,
273
+ ];
274
+ export function isPrivateHost(hostname) {
275
+ const lower = hostname.toLowerCase();
276
+ if (PRIVATE_NETWORK_HOSTNAMES.has(lower))
277
+ return true;
278
+ if (PRIVATE_NETWORK_SUFFIXES.some(s => lower.endsWith(s)))
279
+ return true;
280
+ if (PRIVATE_NETWORK_CIDRS.some(re => re.test(lower)))
281
+ return true;
282
+ return false;
283
+ }
284
+ // ─── HTML Processing ───
285
+ export const HTML_STRIP_TAGS = ['script', 'style', 'nav', 'footer', 'header'];
286
+ // ─── Model Classification ───
287
+ export const CHEAP_MODEL_INDICATORS = ['mini', 'nano', 'lite', 'small', 'free'];
288
+ // ─── Binary Detection ───
289
+ export const BINARY_DETECT_BUFFER_SIZE = 512;
290
+ // ─── Shell Defaults ───
291
+ export const UNIX_SHELL_FALLBACK = '/bin/sh';
292
+ // ─── Plugin Defaults ───
293
+ export const PLUGIN_DEFAULTS = {
294
+ cacheTtlMs: 30_000,
295
+ };
296
+ // ─── Compression Defaults ───
297
+ export const COMPRESSION_DEFAULTS = {
298
+ keepRatio: 0.4,
299
+ truncateDisplay: 1000,
300
+ };
301
+ // ─── Complexity Patterns (for model routing) ───
302
+ export const COMPLEXITY_PATTERNS = {
303
+ simple: [
304
+ /^(read|show|list|cat|print|display|what is|what's)\b/i,
305
+ /\b(file|directory|folder|path|tree)\s*(content|list|structure)/i,
306
+ /\b(time|date|clock)\b/i,
307
+ /\b(status|diff|log)\b.*\b(git)\b/i,
308
+ /\bgit\s+(status|diff|log|branch)\b/i,
309
+ /\bls\b|\bpwd\b|\bcd\b/i,
310
+ /^(check|verify|confirm)\b/i,
311
+ /\b(find|search|grep)\s+(file|for)\b/i,
312
+ ],
313
+ complex: [
314
+ /\b(refactor|rewrite|redesign|architect|migrate|convert)\b/i,
315
+ /\b(implement|build|create|develop)\s+(a|an|the|new)\s+\w+\s+(system|module|feature|service|api|component)/i,
316
+ /\b(fix|debug|investigate|diagnose)\s+.{20,}/i,
317
+ /\b(test|testing)\s+(strategy|plan|suite|framework)\b/i,
318
+ /\b(performance|optimize|optimization|scale|scaling)\b/i,
319
+ /\b(security|vulnerability|audit|penetration)\b/i,
320
+ /\b(database|schema|migration)\s+(design|redesign|optimize)/i,
321
+ /\bmulti[- ]?(file|step|stage|phase)\b/i,
322
+ /\b(review|analyze|compare)\s+.{30,}/i,
323
+ ],
324
+ };
@@ -0,0 +1,151 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { collectGitInfo, gitInfoToContext } from './git-context.js';
4
+ import { loadMemory, memoryToContext } from './project-memory.js';
5
+ import { buildProjectIndex, indexToContext } from './project-index.js';
6
+ import { loadScopedRules, rulesToContext } from './scoped-rules.js';
7
+ async function readJsonSafe(filePath) {
8
+ try {
9
+ return JSON.parse(await fs.readFile(filePath, 'utf-8'));
10
+ }
11
+ catch {
12
+ return null;
13
+ }
14
+ }
15
+ async function fileExists(filePath) {
16
+ try {
17
+ await fs.access(filePath);
18
+ return true;
19
+ }
20
+ catch {
21
+ return false;
22
+ }
23
+ }
24
+ async function detectProjectType(cwd) {
25
+ const indicators = [];
26
+ try {
27
+ const dirEntries = await fs.readdir(cwd);
28
+ const entries = new Set(dirEntries);
29
+ if (entries.has('package.json'))
30
+ indicators.push('Node.js');
31
+ if (entries.has('tsconfig.json'))
32
+ indicators.push('TypeScript');
33
+ if (entries.has('next.config.js') || entries.has('next.config.ts') || entries.has('next.config.mjs'))
34
+ indicators.push('Next.js');
35
+ if (entries.has('pyproject.toml') || entries.has('setup.py') || entries.has('requirements.txt'))
36
+ indicators.push('Python');
37
+ if (entries.has('Cargo.toml'))
38
+ indicators.push('Rust');
39
+ if (entries.has('go.mod'))
40
+ indicators.push('Go');
41
+ if (entries.has('Gemfile'))
42
+ indicators.push('Ruby');
43
+ if (entries.has('pom.xml') || entries.has('build.gradle'))
44
+ indicators.push('Java');
45
+ if (entries.has('Dockerfile') || entries.has('docker-compose.yml'))
46
+ indicators.push('Docker');
47
+ }
48
+ catch { }
49
+ return indicators.length > 0 ? indicators.join(', ') : 'Unknown';
50
+ }
51
+ async function detectAvailableScripts(cwd) {
52
+ const scripts = [];
53
+ // npm scripts
54
+ const pkg = await readJsonSafe(path.join(cwd, 'package.json'));
55
+ if (pkg?.scripts) {
56
+ for (const [name, cmd] of Object.entries(pkg.scripts)) {
57
+ if (typeof cmd === 'string') {
58
+ scripts.push(`npm run ${name} — ${cmd}`);
59
+ }
60
+ }
61
+ }
62
+ // Makefile targets
63
+ if (await fileExists(path.join(cwd, 'Makefile'))) {
64
+ try {
65
+ const makefile = await fs.readFile(path.join(cwd, 'Makefile'), 'utf-8');
66
+ const targets = makefile.match(/^[\w-]+(?=\s*:)/gm);
67
+ if (targets) {
68
+ scripts.push(...targets.slice(0, 20).map((t) => `make ${t}`));
69
+ }
70
+ }
71
+ catch { }
72
+ }
73
+ // Cargo
74
+ if (await fileExists(path.join(cwd, 'Cargo.toml'))) {
75
+ scripts.push('cargo build', 'cargo test', 'cargo check', 'cargo run');
76
+ }
77
+ // Go
78
+ if (await fileExists(path.join(cwd, 'go.mod'))) {
79
+ scripts.push('go build ./...', 'go test ./...', 'go vet ./...');
80
+ }
81
+ // Python
82
+ if (await fileExists(path.join(cwd, 'pyproject.toml'))) {
83
+ scripts.push('python -m pytest', 'python -m mypy .');
84
+ }
85
+ return scripts;
86
+ }
87
+ async function loadProjectConfig(cwd) {
88
+ const configNames = ['SCALIX.md'];
89
+ for (const name of configNames) {
90
+ try {
91
+ const content = await fs.readFile(path.join(cwd, name), 'utf-8');
92
+ if (content.trim()) {
93
+ return `## Project Instructions (${name})\n${content.trim()}`;
94
+ }
95
+ }
96
+ catch { }
97
+ }
98
+ return '';
99
+ }
100
+ export async function buildProjectContext(cwd) {
101
+ const scripts = await detectAvailableScripts(cwd);
102
+ const projectType = await detectProjectType(cwd);
103
+ // Gather context in parallel
104
+ const [gitCtx, memory, rules, projectConfig] = await Promise.all([
105
+ Promise.resolve(collectGitInfo(cwd)).then(gitInfoToContext).catch(() => ''),
106
+ loadMemory(cwd).then(memoryToContext).catch(() => ''),
107
+ loadScopedRules(cwd).then(rulesToContext).catch(() => ''),
108
+ loadProjectConfig(cwd),
109
+ ]);
110
+ // Project index (uses cache)
111
+ let indexCtx = '';
112
+ try {
113
+ const index = await buildProjectIndex(cwd);
114
+ indexCtx = indexToContext(index);
115
+ }
116
+ catch { }
117
+ return {
118
+ projectType,
119
+ gitContext: gitCtx,
120
+ projectIndex: indexCtx,
121
+ projectMemory: memory,
122
+ scopedRules: rules,
123
+ availableScripts: scripts,
124
+ projectConfig,
125
+ };
126
+ }
127
+ export function contextToInstructions(baseInstructions, ctx) {
128
+ const sections = [baseInstructions];
129
+ if (ctx.projectConfig) {
130
+ sections.push(ctx.projectConfig);
131
+ }
132
+ if (ctx.projectType !== 'Unknown') {
133
+ sections.push(`## Project Type\n${ctx.projectType}`);
134
+ }
135
+ if (ctx.availableScripts.length > 0) {
136
+ sections.push(`## Available Commands\n${ctx.availableScripts.join('\n')}`);
137
+ }
138
+ if (ctx.gitContext) {
139
+ sections.push(ctx.gitContext);
140
+ }
141
+ if (ctx.projectIndex) {
142
+ sections.push(ctx.projectIndex);
143
+ }
144
+ if (ctx.projectMemory) {
145
+ sections.push(ctx.projectMemory);
146
+ }
147
+ if (ctx.scopedRules) {
148
+ sections.push(ctx.scopedRules);
149
+ }
150
+ return sections.filter(Boolean).join('\n\n');
151
+ }