scalix 1.0.0 → 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/README.md +1 -1
- package/dist/agent-definitions.js +100 -0
- package/dist/agent-registry.js +226 -0
- package/dist/agent.js +352 -74
- package/dist/api-client.js +77 -0
- package/dist/cli.js +160 -35
- package/dist/constants.js +40 -6
- package/dist/context-builder.js +8 -1
- package/dist/context-retriever.js +257 -0
- package/dist/git-context.js +32 -29
- package/dist/grounding-checker.js +154 -0
- package/dist/headless.js +109 -20
- package/dist/hooks.js +7 -1
- package/dist/injection-detector.js +86 -0
- package/dist/ipc-server.js +6 -2
- package/dist/project-index.js +2 -0
- package/dist/sandbox.js +135 -11
- package/dist/sessions.js +37 -2
- package/dist/settings.js +111 -6
- package/dist/sub-agent.js +83 -8
- package/dist/task-board.js +105 -0
- package/dist/tools-advanced.js +304 -49
- package/dist/tools-browser.js +280 -19
- package/dist/tools-extra.js +7 -0
- package/dist/tools-vision.js +4 -3
- package/dist/tools.js +13 -3
- package/dist/update-checker.js +111 -0
- package/dist/worktree.js +172 -0
- package/package.json +6 -2
- package/app.py +0 -55
package/.env.example
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Required — your Scalix API key
|
|
2
|
+
SCALIX_API_KEY=sk-...
|
|
3
|
+
|
|
4
|
+
# Optional — custom API base URL (defaults to Scalix cloud)
|
|
5
|
+
# SCALIX_BASE_URL=https://api.scalix.world/v1
|
|
6
|
+
|
|
7
|
+
# Optional — override the default model
|
|
8
|
+
# SCALIX_MODEL=
|
|
9
|
+
|
|
10
|
+
# Optional — permission mode: plan | default | auto | yolo
|
|
11
|
+
# SCALIX_PERMISSION_MODE=default
|
|
12
|
+
|
|
13
|
+
# Optional — speech-to-text model for voice input
|
|
14
|
+
# SCALIX_STT_MODEL=
|
|
15
|
+
|
|
16
|
+
# Optional — log level: debug | info | warn | error
|
|
17
|
+
# LOG_LEVEL=info
|
|
18
|
+
|
|
19
|
+
# Optional — shell override (defaults to $SHELL)
|
|
20
|
+
# SCALIX_SHELL=/bin/zsh
|
|
21
|
+
|
|
22
|
+
# Optional — web search API keys (enable web_search tool)
|
|
23
|
+
# BRAVE_API_KEY=
|
|
24
|
+
# TAVILY_API_KEY=
|
|
25
|
+
|
|
26
|
+
# Optional — custom search endpoint overrides
|
|
27
|
+
# BRAVE_SEARCH_URL=
|
|
28
|
+
# TAVILY_SEARCH_URL=
|
|
29
|
+
# DDG_SEARCH_URL=
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
AI-powered coding agent for the terminal and VS Code — built by [Scalix World](https://scalix.world).
|
|
4
4
|
|
|
5
|
-

|
|
6
6
|

|
|
7
7
|

|
|
8
8
|
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import { USER_CONFIG_DIR, PROJECT_LOOKUP_DIRS } from './constants.js';
|
|
5
|
+
const VALID_TOOL_LEVELS = new Set(['read-only', 'standard', 'all']);
|
|
6
|
+
const BUILT_IN_TYPES = new Set(['general', 'explore', 'plan']);
|
|
7
|
+
export function parseAgentDefinition(content, filename) {
|
|
8
|
+
const frontmatterMatch = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/);
|
|
9
|
+
if (!frontmatterMatch)
|
|
10
|
+
return null;
|
|
11
|
+
const [, frontmatter, body] = frontmatterMatch;
|
|
12
|
+
const instructions = body.trim();
|
|
13
|
+
if (!instructions)
|
|
14
|
+
return null;
|
|
15
|
+
const fields = {};
|
|
16
|
+
for (const line of frontmatter.split('\n')) {
|
|
17
|
+
const colonIdx = line.indexOf(':');
|
|
18
|
+
if (colonIdx === -1)
|
|
19
|
+
continue;
|
|
20
|
+
const key = line.slice(0, colonIdx).trim();
|
|
21
|
+
const val = line.slice(colonIdx + 1).trim();
|
|
22
|
+
fields[key] = val;
|
|
23
|
+
}
|
|
24
|
+
const name = fields.name || path.basename(filename, path.extname(filename));
|
|
25
|
+
if (BUILT_IN_TYPES.has(name))
|
|
26
|
+
return null;
|
|
27
|
+
const description = fields.description || `Custom agent: ${name}`;
|
|
28
|
+
const tools = VALID_TOOL_LEVELS.has(fields.tools) ? fields.tools : 'standard';
|
|
29
|
+
const maxSteps = fields.maxSteps ? parseInt(fields.maxSteps, 10) : undefined;
|
|
30
|
+
const model = fields.model && fields.model !== '(optional override)' ? fields.model : undefined;
|
|
31
|
+
const allowSpawning = fields.allowSpawning === 'true' ? true : fields.allowSpawning === 'false' ? false : undefined;
|
|
32
|
+
return {
|
|
33
|
+
name,
|
|
34
|
+
description,
|
|
35
|
+
tools,
|
|
36
|
+
maxSteps: maxSteps && Number.isFinite(maxSteps) && maxSteps > 0 ? maxSteps : undefined,
|
|
37
|
+
model,
|
|
38
|
+
allowSpawning,
|
|
39
|
+
instructions,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
async function loadDefinitionsFromDir(dir) {
|
|
43
|
+
const defs = new Map();
|
|
44
|
+
try {
|
|
45
|
+
const entries = await fs.readdir(dir);
|
|
46
|
+
for (const entry of entries) {
|
|
47
|
+
if (!entry.endsWith('.md'))
|
|
48
|
+
continue;
|
|
49
|
+
try {
|
|
50
|
+
const content = await fs.readFile(path.join(dir, entry), 'utf-8');
|
|
51
|
+
const def = parseAgentDefinition(content, entry);
|
|
52
|
+
if (def)
|
|
53
|
+
defs.set(def.name, def);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
// skip unreadable files
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// directory doesn't exist — that's fine
|
|
62
|
+
}
|
|
63
|
+
return defs;
|
|
64
|
+
}
|
|
65
|
+
export async function loadAgentDefinitions(projectDir) {
|
|
66
|
+
const globalDir = path.join(os.homedir(), USER_CONFIG_DIR, 'agents');
|
|
67
|
+
const globalDefs = await loadDefinitionsFromDir(globalDir);
|
|
68
|
+
const projectDefs = new Map();
|
|
69
|
+
if (projectDir) {
|
|
70
|
+
for (const lookupDir of PROJECT_LOOKUP_DIRS) {
|
|
71
|
+
const dir = path.join(projectDir, lookupDir, 'agents');
|
|
72
|
+
const defs = await loadDefinitionsFromDir(dir);
|
|
73
|
+
for (const [name, def] of defs) {
|
|
74
|
+
projectDefs.set(name, def);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// Project definitions override global
|
|
79
|
+
const merged = new Map(globalDefs);
|
|
80
|
+
for (const [name, def] of projectDefs) {
|
|
81
|
+
merged.set(name, def);
|
|
82
|
+
}
|
|
83
|
+
return merged;
|
|
84
|
+
}
|
|
85
|
+
let _cachedDefinitions = null;
|
|
86
|
+
let _cacheProjectDir;
|
|
87
|
+
export async function getAgentDefinitions(projectDir) {
|
|
88
|
+
if (_cachedDefinitions && _cacheProjectDir === projectDir)
|
|
89
|
+
return _cachedDefinitions;
|
|
90
|
+
_cachedDefinitions = await loadAgentDefinitions(projectDir);
|
|
91
|
+
_cacheProjectDir = projectDir;
|
|
92
|
+
return _cachedDefinitions;
|
|
93
|
+
}
|
|
94
|
+
export function invalidateAgentDefinitionCache() {
|
|
95
|
+
_cachedDefinitions = null;
|
|
96
|
+
_cacheProjectDir = undefined;
|
|
97
|
+
}
|
|
98
|
+
export function isBuiltInAgentType(name) {
|
|
99
|
+
return BUILT_IN_TYPES.has(name);
|
|
100
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { EventEmitter } from 'eventemitter3';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { logger } from './logger.js';
|
|
4
|
+
import { MULTI_AGENT_DEFAULTS } from './constants.js';
|
|
5
|
+
export class AgentRegistry extends EventEmitter {
|
|
6
|
+
static instance = null;
|
|
7
|
+
agents = new Map();
|
|
8
|
+
agentRefs = new Map();
|
|
9
|
+
messages = [];
|
|
10
|
+
limits;
|
|
11
|
+
constructor(limits) {
|
|
12
|
+
super();
|
|
13
|
+
this.limits = {
|
|
14
|
+
maxDepth: limits?.maxDepth ?? MULTI_AGENT_DEFAULTS.maxDepth,
|
|
15
|
+
maxConcurrentAgents: limits?.maxConcurrentAgents ?? MULTI_AGENT_DEFAULTS.maxConcurrentAgents,
|
|
16
|
+
globalTokenBudget: limits?.globalTokenBudget ?? MULTI_AGENT_DEFAULTS.globalTokenBudget,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
static getInstance(limits) {
|
|
20
|
+
if (!AgentRegistry.instance) {
|
|
21
|
+
AgentRegistry.instance = new AgentRegistry(limits);
|
|
22
|
+
}
|
|
23
|
+
return AgentRegistry.instance;
|
|
24
|
+
}
|
|
25
|
+
static resetInstance() {
|
|
26
|
+
if (AgentRegistry.instance) {
|
|
27
|
+
AgentRegistry.instance.removeAllListeners();
|
|
28
|
+
AgentRegistry.instance.agents.clear();
|
|
29
|
+
AgentRegistry.instance.agentRefs.clear();
|
|
30
|
+
AgentRegistry.instance.messages = [];
|
|
31
|
+
}
|
|
32
|
+
AgentRegistry.instance = null;
|
|
33
|
+
}
|
|
34
|
+
register(id, agentRef, parentId, metadata) {
|
|
35
|
+
const depth = parentId ? this.getDepth(parentId) + 1 : 0;
|
|
36
|
+
const entry = {
|
|
37
|
+
id,
|
|
38
|
+
parentId,
|
|
39
|
+
depth,
|
|
40
|
+
description: metadata.description,
|
|
41
|
+
model: metadata.model,
|
|
42
|
+
status: 'running',
|
|
43
|
+
tokenUsage: 0,
|
|
44
|
+
stepCount: 0,
|
|
45
|
+
spawnedAt: Date.now(),
|
|
46
|
+
completedAt: null,
|
|
47
|
+
};
|
|
48
|
+
this.agents.set(id, entry);
|
|
49
|
+
this.agentRefs.set(id, agentRef);
|
|
50
|
+
this.emit('agent:spawned', entry);
|
|
51
|
+
logger.info('Agent registered', { id, parentId, depth, description: metadata.description });
|
|
52
|
+
}
|
|
53
|
+
deregister(id, status) {
|
|
54
|
+
const entry = this.agents.get(id);
|
|
55
|
+
if (!entry)
|
|
56
|
+
return;
|
|
57
|
+
entry.status = status;
|
|
58
|
+
entry.completedAt = Date.now();
|
|
59
|
+
this.agentRefs.delete(id);
|
|
60
|
+
this.emit('agent:completed', entry);
|
|
61
|
+
logger.info('Agent deregistered', { id, status, tokens: entry.tokenUsage, steps: entry.stepCount });
|
|
62
|
+
}
|
|
63
|
+
getAgent(id) {
|
|
64
|
+
return this.agentRefs.get(id);
|
|
65
|
+
}
|
|
66
|
+
getMeta(id) {
|
|
67
|
+
return this.agents.get(id);
|
|
68
|
+
}
|
|
69
|
+
listActive() {
|
|
70
|
+
return [...this.agents.values()].filter(a => a.status === 'running');
|
|
71
|
+
}
|
|
72
|
+
listAll() {
|
|
73
|
+
return [...this.agents.values()];
|
|
74
|
+
}
|
|
75
|
+
getChildren(parentId) {
|
|
76
|
+
return [...this.agents.values()].filter(a => a.parentId === parentId);
|
|
77
|
+
}
|
|
78
|
+
getDepth(id) {
|
|
79
|
+
const entry = this.agents.get(id);
|
|
80
|
+
if (!entry)
|
|
81
|
+
return 0;
|
|
82
|
+
if (!entry.parentId)
|
|
83
|
+
return 0;
|
|
84
|
+
return 1 + this.getDepth(entry.parentId);
|
|
85
|
+
}
|
|
86
|
+
canSpawn(parentId) {
|
|
87
|
+
const activeCount = this.listActive().length;
|
|
88
|
+
if (activeCount >= this.limits.maxConcurrentAgents) {
|
|
89
|
+
return { allowed: false, reason: `Concurrent agent limit reached (${activeCount}/${this.limits.maxConcurrentAgents})` };
|
|
90
|
+
}
|
|
91
|
+
if (parentId) {
|
|
92
|
+
const depth = this.getDepth(parentId) + 1;
|
|
93
|
+
if (depth >= this.limits.maxDepth) {
|
|
94
|
+
return { allowed: false, reason: `Maximum agent depth reached (${depth}/${this.limits.maxDepth})` };
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const totalTokens = this.getTotalTokens();
|
|
98
|
+
if (totalTokens >= this.limits.globalTokenBudget) {
|
|
99
|
+
return { allowed: false, reason: `Global token budget exhausted (${totalTokens.toLocaleString()}/${this.limits.globalTokenBudget.toLocaleString()})` };
|
|
100
|
+
}
|
|
101
|
+
return { allowed: true };
|
|
102
|
+
}
|
|
103
|
+
// --- Messaging ---
|
|
104
|
+
sendMessage(from, to, content) {
|
|
105
|
+
if (from === to) {
|
|
106
|
+
return { error: 'Cannot send a message to self' };
|
|
107
|
+
}
|
|
108
|
+
const recipient = this.agents.get(to);
|
|
109
|
+
if (!recipient) {
|
|
110
|
+
return { error: `Agent "${to}" not found` };
|
|
111
|
+
}
|
|
112
|
+
if (recipient.status !== 'running') {
|
|
113
|
+
return { error: `Agent "${to}" is not running (status: ${recipient.status})` };
|
|
114
|
+
}
|
|
115
|
+
const msg = {
|
|
116
|
+
id: randomUUID(),
|
|
117
|
+
from,
|
|
118
|
+
to,
|
|
119
|
+
content,
|
|
120
|
+
timestamp: Date.now(),
|
|
121
|
+
read: false,
|
|
122
|
+
};
|
|
123
|
+
this.messages.push(msg);
|
|
124
|
+
this.emit('agent:message', msg);
|
|
125
|
+
return msg;
|
|
126
|
+
}
|
|
127
|
+
broadcast(from, content) {
|
|
128
|
+
const sent = [];
|
|
129
|
+
for (const agent of this.listActive()) {
|
|
130
|
+
if (agent.id === from)
|
|
131
|
+
continue;
|
|
132
|
+
const result = this.sendMessage(from, agent.id, content);
|
|
133
|
+
if ('id' in result)
|
|
134
|
+
sent.push(result);
|
|
135
|
+
}
|
|
136
|
+
return sent;
|
|
137
|
+
}
|
|
138
|
+
getUnread(agentId) {
|
|
139
|
+
return this.messages.filter(m => m.to === agentId && !m.read);
|
|
140
|
+
}
|
|
141
|
+
markRead(agentId) {
|
|
142
|
+
for (const msg of this.messages) {
|
|
143
|
+
if (msg.to === agentId && !msg.read) {
|
|
144
|
+
msg.read = true;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
// --- Resource Tracking ---
|
|
149
|
+
recordTokens(id, tokens) {
|
|
150
|
+
const entry = this.agents.get(id);
|
|
151
|
+
if (entry) {
|
|
152
|
+
entry.tokenUsage += tokens;
|
|
153
|
+
}
|
|
154
|
+
const totalTokens = this.getTotalTokens();
|
|
155
|
+
const budgetPct = totalTokens / this.limits.globalTokenBudget;
|
|
156
|
+
if (budgetPct >= 0.9) {
|
|
157
|
+
this.emit('budget:warning', {
|
|
158
|
+
totalTokens,
|
|
159
|
+
budget: this.limits.globalTokenBudget,
|
|
160
|
+
percentage: Math.round(budgetPct * 100),
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
recordStep(id) {
|
|
165
|
+
const entry = this.agents.get(id);
|
|
166
|
+
if (entry) {
|
|
167
|
+
entry.stepCount++;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
getTotalTokens() {
|
|
171
|
+
let total = 0;
|
|
172
|
+
for (const entry of this.agents.values()) {
|
|
173
|
+
total += entry.tokenUsage;
|
|
174
|
+
}
|
|
175
|
+
return total;
|
|
176
|
+
}
|
|
177
|
+
getUsageSummary() {
|
|
178
|
+
const all = [...this.agents.values()];
|
|
179
|
+
return {
|
|
180
|
+
totalTokens: this.getTotalTokens(),
|
|
181
|
+
activeCount: all.filter(a => a.status === 'running').length,
|
|
182
|
+
completedCount: all.filter(a => a.status === 'completed').length,
|
|
183
|
+
failedCount: all.filter(a => a.status === 'failed' || a.status === 'aborted').length,
|
|
184
|
+
byAgent: all.map(a => ({
|
|
185
|
+
id: a.id,
|
|
186
|
+
description: a.description,
|
|
187
|
+
model: a.model,
|
|
188
|
+
tokens: a.tokenUsage,
|
|
189
|
+
steps: a.stepCount,
|
|
190
|
+
status: a.status,
|
|
191
|
+
})),
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
getAgentTree(rootId) {
|
|
195
|
+
const lines = [];
|
|
196
|
+
const roots = rootId
|
|
197
|
+
? [this.agents.get(rootId)].filter(Boolean)
|
|
198
|
+
: [...this.agents.values()].filter(a => !a.parentId);
|
|
199
|
+
const render = (entry, indent) => {
|
|
200
|
+
const statusIcon = entry.status === 'running' ? '*' : entry.status === 'completed' ? '+' : 'x';
|
|
201
|
+
lines.push(`${indent}[${statusIcon}] ${entry.id.slice(0, 8)} ${entry.description} (${entry.model}, ${entry.tokenUsage.toLocaleString()} tok)`);
|
|
202
|
+
const children = this.getChildren(entry.id);
|
|
203
|
+
for (const child of children) {
|
|
204
|
+
render(child, indent + ' ');
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
for (const root of roots) {
|
|
208
|
+
render(root, '');
|
|
209
|
+
}
|
|
210
|
+
return lines.join('\n') || '(no agents)';
|
|
211
|
+
}
|
|
212
|
+
getLimits() {
|
|
213
|
+
return { ...this.limits };
|
|
214
|
+
}
|
|
215
|
+
setLimits(limits) {
|
|
216
|
+
if (limits.maxDepth !== undefined)
|
|
217
|
+
this.limits.maxDepth = limits.maxDepth;
|
|
218
|
+
if (limits.maxConcurrentAgents !== undefined)
|
|
219
|
+
this.limits.maxConcurrentAgents = limits.maxConcurrentAgents;
|
|
220
|
+
if (limits.globalTokenBudget !== undefined)
|
|
221
|
+
this.limits.globalTokenBudget = limits.globalTokenBudget;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
export function generateAgentId() {
|
|
225
|
+
return randomUUID();
|
|
226
|
+
}
|