scalix 1.1.0 → 1.2.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/PARITY-PLAN.md +116 -0
- package/bin/scalix.js +1 -0
- package/dist/agent.js +12 -1
- package/dist/cli.js +567 -44
- package/dist/commands.js +53 -2
- package/dist/constants.js +3 -3
- package/dist/headless.js +42 -2
- package/dist/hooks.js +141 -9
- package/dist/keychain.js +109 -0
- package/dist/markdown.js +46 -3
- package/dist/plugins.js +132 -0
- package/dist/project-index.js +20 -1
- package/dist/sandbox.js +66 -0
- package/dist/sessions.js +59 -0
- package/dist/settings.js +85 -4
- package/dist/skills.js +269 -0
- package/dist/terminal-notify.js +73 -0
- package/dist/text-utils.js +146 -0
- package/dist/tools-extra.js +2 -8
- package/package.json +3 -2
package/dist/skills.js
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { logger } from './logger.js';
|
|
5
|
+
import { PROJECT_LOOKUP_DIRS } from './constants.js';
|
|
6
|
+
const SKILL_DIRS = PROJECT_LOOKUP_DIRS.map(d => `${d}/skills`);
|
|
7
|
+
function parseSkillFrontmatter(content) {
|
|
8
|
+
const match = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/);
|
|
9
|
+
if (!match)
|
|
10
|
+
return null;
|
|
11
|
+
const meta = {};
|
|
12
|
+
let currentKey = '';
|
|
13
|
+
let isList = false;
|
|
14
|
+
for (const line of match[1].split('\n')) {
|
|
15
|
+
const trimmed = line.trim();
|
|
16
|
+
if (trimmed.startsWith('- ') && isList && currentKey) {
|
|
17
|
+
const arr = meta[currentKey];
|
|
18
|
+
if (Array.isArray(arr))
|
|
19
|
+
arr.push(trimmed.slice(2).trim());
|
|
20
|
+
else
|
|
21
|
+
meta[currentKey] = [trimmed.slice(2).trim()];
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
const idx = line.indexOf(':');
|
|
25
|
+
if (idx > 0) {
|
|
26
|
+
currentKey = line.slice(0, idx).trim();
|
|
27
|
+
const value = line.slice(idx + 1).trim();
|
|
28
|
+
if (value === '' || value === '[]') {
|
|
29
|
+
meta[currentKey] = [];
|
|
30
|
+
isList = true;
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
meta[currentKey] = value;
|
|
34
|
+
isList = false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const name = meta.name || '';
|
|
39
|
+
const description = meta.description || '';
|
|
40
|
+
if (!name)
|
|
41
|
+
return null;
|
|
42
|
+
return {
|
|
43
|
+
meta: {
|
|
44
|
+
name,
|
|
45
|
+
description,
|
|
46
|
+
version: meta.version || '1.0.0',
|
|
47
|
+
paths: Array.isArray(meta.paths) ? meta.paths : (meta.paths ? [meta.paths] : []),
|
|
48
|
+
triggers: Array.isArray(meta.triggers) ? meta.triggers : (meta.triggers ? [meta.triggers] : []),
|
|
49
|
+
},
|
|
50
|
+
body: match[2],
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
async function discoverResources(skillDir) {
|
|
54
|
+
const resources = { scripts: [], references: [], examples: [], assets: [] };
|
|
55
|
+
const dirs = ['scripts', 'references', 'examples', 'assets'];
|
|
56
|
+
for (const dir of dirs) {
|
|
57
|
+
const fullDir = path.join(skillDir, dir);
|
|
58
|
+
try {
|
|
59
|
+
const entries = await fs.readdir(fullDir, { withFileTypes: true });
|
|
60
|
+
resources[dir] = entries
|
|
61
|
+
.filter(e => e.isFile())
|
|
62
|
+
.map(e => path.join(fullDir, e.name));
|
|
63
|
+
}
|
|
64
|
+
catch { }
|
|
65
|
+
}
|
|
66
|
+
return resources;
|
|
67
|
+
}
|
|
68
|
+
export async function loadSkills(cwd = process.cwd()) {
|
|
69
|
+
const skills = [];
|
|
70
|
+
for (const dir of SKILL_DIRS) {
|
|
71
|
+
const fullDir = path.resolve(cwd, dir);
|
|
72
|
+
let entries;
|
|
73
|
+
try {
|
|
74
|
+
entries = await fs.readdir(fullDir, { withFileTypes: true });
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
for (const entry of entries) {
|
|
80
|
+
if (!entry.isDirectory()) {
|
|
81
|
+
if (entry.isFile() && entry.name === 'SKILL.md') {
|
|
82
|
+
const skill = await loadSkillFile(path.join(fullDir, entry.name), fullDir);
|
|
83
|
+
if (skill)
|
|
84
|
+
skills.push(skill);
|
|
85
|
+
}
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
const skillFile = path.join(fullDir, entry.name, 'SKILL.md');
|
|
89
|
+
if (existsSync(skillFile)) {
|
|
90
|
+
const skill = await loadSkillFile(skillFile, path.join(fullDir, entry.name));
|
|
91
|
+
if (skill)
|
|
92
|
+
skills.push(skill);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
skills.push(...getBuiltinSkills());
|
|
97
|
+
return skills;
|
|
98
|
+
}
|
|
99
|
+
async function loadSkillFile(filePath, skillDir) {
|
|
100
|
+
try {
|
|
101
|
+
const raw = await fs.readFile(filePath, 'utf-8');
|
|
102
|
+
const parsed = parseSkillFrontmatter(raw);
|
|
103
|
+
if (!parsed)
|
|
104
|
+
return null;
|
|
105
|
+
const resources = await discoverResources(skillDir);
|
|
106
|
+
return {
|
|
107
|
+
name: parsed.meta.name,
|
|
108
|
+
description: parsed.meta.description,
|
|
109
|
+
version: parsed.meta.version || '1.0.0',
|
|
110
|
+
paths: parsed.meta.paths || [],
|
|
111
|
+
triggers: parsed.meta.triggers || [],
|
|
112
|
+
content: parsed.body.trim(),
|
|
113
|
+
source: filePath,
|
|
114
|
+
resources,
|
|
115
|
+
builtin: false,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
catch (err) {
|
|
119
|
+
logger.warn('Failed to load skill', { path: filePath, error: err.message });
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
export function getSkillsForPath(skills, filePath) {
|
|
124
|
+
const normalized = filePath.replace(/\\/g, '/');
|
|
125
|
+
return skills.filter(skill => skill.paths.some(p => {
|
|
126
|
+
const pattern = p.replace(/\\/g, '/');
|
|
127
|
+
if (pattern.endsWith('/**')) {
|
|
128
|
+
return normalized.startsWith(pattern.slice(0, -3));
|
|
129
|
+
}
|
|
130
|
+
if (pattern.endsWith('/*')) {
|
|
131
|
+
const dir = pattern.slice(0, -2);
|
|
132
|
+
return normalized.startsWith(dir) && !normalized.slice(dir.length + 1).includes('/');
|
|
133
|
+
}
|
|
134
|
+
return normalized.startsWith(pattern);
|
|
135
|
+
}));
|
|
136
|
+
}
|
|
137
|
+
export function getSkillByTrigger(skills, input) {
|
|
138
|
+
const lower = input.toLowerCase();
|
|
139
|
+
return skills.find(skill => skill.triggers.some(t => lower.includes(t.toLowerCase()))) || null;
|
|
140
|
+
}
|
|
141
|
+
export function getSkillContent(skill, level = 'full') {
|
|
142
|
+
if (level === 'metadata') {
|
|
143
|
+
return `**${skill.name}** (v${skill.version}) — ${skill.description}`;
|
|
144
|
+
}
|
|
145
|
+
let content = skill.content;
|
|
146
|
+
if (level === 'resources') {
|
|
147
|
+
const resourceFiles = [
|
|
148
|
+
...skill.resources.scripts,
|
|
149
|
+
...skill.resources.references,
|
|
150
|
+
...skill.resources.examples,
|
|
151
|
+
];
|
|
152
|
+
for (const filePath of resourceFiles) {
|
|
153
|
+
if (existsSync(filePath)) {
|
|
154
|
+
try {
|
|
155
|
+
const fileContent = readFileSync(filePath, 'utf-8');
|
|
156
|
+
const relPath = path.basename(filePath);
|
|
157
|
+
content += `\n\n<resource path="${relPath}">\n${fileContent}\n</resource>`;
|
|
158
|
+
}
|
|
159
|
+
catch { }
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return content;
|
|
164
|
+
}
|
|
165
|
+
export function skillsSummary(skills) {
|
|
166
|
+
if (skills.length === 0)
|
|
167
|
+
return 'No skills available.';
|
|
168
|
+
const lines = ['Available skills:'];
|
|
169
|
+
for (const s of skills) {
|
|
170
|
+
const paths = s.paths.length > 0 ? ` [paths: ${s.paths.join(', ')}]` : '';
|
|
171
|
+
const builtin = s.builtin ? ' (built-in)' : '';
|
|
172
|
+
lines.push(` ${s.name} — ${s.description}${paths}${builtin}`);
|
|
173
|
+
}
|
|
174
|
+
return lines.join('\n');
|
|
175
|
+
}
|
|
176
|
+
function getBuiltinSkills() {
|
|
177
|
+
const emptyResources = { scripts: [], references: [], examples: [], assets: [] };
|
|
178
|
+
return [
|
|
179
|
+
{
|
|
180
|
+
name: 'feature-dev',
|
|
181
|
+
description: 'Guided 7-phase feature development workflow',
|
|
182
|
+
version: '1.0.0',
|
|
183
|
+
paths: [],
|
|
184
|
+
triggers: ['new feature', 'implement feature', 'build feature', 'add feature'],
|
|
185
|
+
content: `# Feature Development Workflow
|
|
186
|
+
|
|
187
|
+
Follow this 7-phase process for developing new features:
|
|
188
|
+
|
|
189
|
+
## Phase 1: Requirements
|
|
190
|
+
- Clarify the feature scope and acceptance criteria
|
|
191
|
+
- Identify affected files and components
|
|
192
|
+
- List edge cases and error scenarios
|
|
193
|
+
|
|
194
|
+
## Phase 2: Design
|
|
195
|
+
- Propose the technical approach
|
|
196
|
+
- Identify new files, modified files, and interfaces
|
|
197
|
+
- Consider backwards compatibility
|
|
198
|
+
|
|
199
|
+
## Phase 3: Implementation
|
|
200
|
+
- Write the core implementation
|
|
201
|
+
- Follow existing code conventions
|
|
202
|
+
- Keep changes minimal and focused
|
|
203
|
+
|
|
204
|
+
## Phase 4: Testing
|
|
205
|
+
- Write unit tests for new functions
|
|
206
|
+
- Test edge cases identified in Phase 1
|
|
207
|
+
- Run existing test suite to check for regressions
|
|
208
|
+
|
|
209
|
+
## Phase 5: Integration
|
|
210
|
+
- Wire up the feature with existing code
|
|
211
|
+
- Update configuration if needed
|
|
212
|
+
- Add feature flags if appropriate
|
|
213
|
+
|
|
214
|
+
## Phase 6: Documentation
|
|
215
|
+
- Update relevant documentation
|
|
216
|
+
- Add inline comments only where the "why" is non-obvious
|
|
217
|
+
- Update CHANGELOG if one exists
|
|
218
|
+
|
|
219
|
+
## Phase 7: Review
|
|
220
|
+
- Run linter and type checker
|
|
221
|
+
- Review diff for accidental changes
|
|
222
|
+
- Verify the feature works end-to-end`,
|
|
223
|
+
source: 'built-in',
|
|
224
|
+
resources: emptyResources,
|
|
225
|
+
builtin: true,
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
name: 'code-review',
|
|
229
|
+
description: 'Systematic code review of current changes',
|
|
230
|
+
version: '1.0.0',
|
|
231
|
+
paths: [],
|
|
232
|
+
triggers: ['review code', 'code review', 'review changes', 'review my changes'],
|
|
233
|
+
content: `# Code Review
|
|
234
|
+
|
|
235
|
+
Review the current changes systematically:
|
|
236
|
+
|
|
237
|
+
1. **Run \`git diff\`** to see all uncommitted changes
|
|
238
|
+
2. **Security**: Check for hardcoded secrets, SQL injection, XSS, path traversal
|
|
239
|
+
3. **Correctness**: Verify logic, edge cases, error handling
|
|
240
|
+
4. **Style**: Check naming, formatting, consistency with codebase conventions
|
|
241
|
+
5. **Performance**: Look for N+1 queries, unnecessary allocations, blocking I/O
|
|
242
|
+
6. **Testing**: Are new functions tested? Are existing tests still valid?
|
|
243
|
+
7. **Summary**: Provide a structured review with findings categorized as Critical / Important / Minor`,
|
|
244
|
+
source: 'built-in',
|
|
245
|
+
resources: emptyResources,
|
|
246
|
+
builtin: true,
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
name: 'commit-push-pr',
|
|
250
|
+
description: 'Commit changes, push, and create a pull request',
|
|
251
|
+
version: '1.0.0',
|
|
252
|
+
paths: [],
|
|
253
|
+
triggers: ['commit and push', 'create pr', 'open pr', 'make a pr'],
|
|
254
|
+
content: `# Commit, Push & PR
|
|
255
|
+
|
|
256
|
+
1. **Stage changes**: Review \`git status\` and stage relevant files
|
|
257
|
+
2. **Commit**: Write a concise commit message (imperative mood, <72 chars)
|
|
258
|
+
3. **Push**: Push to the remote branch
|
|
259
|
+
4. **PR**: Create a pull request with:
|
|
260
|
+
- Clear title (matches commit message style)
|
|
261
|
+
- Description summarizing what changed and why
|
|
262
|
+
- Link to related issues if applicable
|
|
263
|
+
- List any manual testing done`,
|
|
264
|
+
source: 'built-in',
|
|
265
|
+
resources: emptyResources,
|
|
266
|
+
builtin: true,
|
|
267
|
+
},
|
|
268
|
+
];
|
|
269
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Terminal notification passthrough for iTerm2, Kitty, Ghostty, and tmux
|
|
2
|
+
function detectTerminal() {
|
|
3
|
+
const term = process.env.TERM_PROGRAM?.toLowerCase() || '';
|
|
4
|
+
const tmux = !!process.env.TMUX;
|
|
5
|
+
if (tmux)
|
|
6
|
+
return 'tmux';
|
|
7
|
+
if (term === 'iterm.app' || term === 'iterm2')
|
|
8
|
+
return 'iterm2';
|
|
9
|
+
if (term === 'kitty')
|
|
10
|
+
return 'kitty';
|
|
11
|
+
if (term === 'ghostty')
|
|
12
|
+
return 'ghostty';
|
|
13
|
+
return 'generic';
|
|
14
|
+
}
|
|
15
|
+
function wrapForTmux(seq) {
|
|
16
|
+
return `\x1bPtmux;\x1b${seq}\x1b\\`;
|
|
17
|
+
}
|
|
18
|
+
function sendOSC(code, payload) {
|
|
19
|
+
const terminal = detectTerminal();
|
|
20
|
+
const seq = `\x1b]${code};${payload}\x07`;
|
|
21
|
+
if (terminal === 'tmux') {
|
|
22
|
+
process.stdout.write(wrapForTmux(seq));
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
process.stdout.write(seq);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export function sendNotification(title, body) {
|
|
29
|
+
const terminal = detectTerminal();
|
|
30
|
+
switch (terminal) {
|
|
31
|
+
case 'iterm2':
|
|
32
|
+
sendOSC(9, body ? `${title}: ${body}` : title);
|
|
33
|
+
break;
|
|
34
|
+
case 'kitty':
|
|
35
|
+
sendOSC(99, `i=1:d=0:p=title;${title}`);
|
|
36
|
+
if (body) {
|
|
37
|
+
sendOSC(99, `i=1:d=1:p=body;${body}`);
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
sendOSC(99, `i=1:d=1:p=body;`);
|
|
41
|
+
}
|
|
42
|
+
break;
|
|
43
|
+
case 'ghostty':
|
|
44
|
+
sendOSC(777, `notify;${title};${body || ''}`);
|
|
45
|
+
break;
|
|
46
|
+
case 'tmux': {
|
|
47
|
+
const tmuxPayload = body ? `${title}: ${body}` : title;
|
|
48
|
+
sendOSC(9, tmuxPayload);
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
case 'generic':
|
|
52
|
+
// OSC 9 is widely supported (ConEmu, Windows Terminal, etc.)
|
|
53
|
+
sendOSC(9, body ? `${title}: ${body}` : title);
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export function sendBell() {
|
|
58
|
+
process.stdout.write('\x07');
|
|
59
|
+
}
|
|
60
|
+
export function setTerminalTitle(title) {
|
|
61
|
+
sendOSC(0, title);
|
|
62
|
+
}
|
|
63
|
+
export function notifyTaskComplete(taskName) {
|
|
64
|
+
sendNotification('Scalix', `${taskName} complete`);
|
|
65
|
+
sendBell();
|
|
66
|
+
}
|
|
67
|
+
export function notifyError(message) {
|
|
68
|
+
sendNotification('Scalix Error', message);
|
|
69
|
+
sendBell();
|
|
70
|
+
}
|
|
71
|
+
export function getTerminalType() {
|
|
72
|
+
return detectTerminal();
|
|
73
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// CJK/RTL text support utilities
|
|
2
|
+
const CJK_RANGES = [
|
|
3
|
+
[0x2E80, 0x2EFF], // CJK Radicals Supplement
|
|
4
|
+
[0x2F00, 0x2FDF], // Kangxi Radicals
|
|
5
|
+
[0x3000, 0x303F], // CJK Symbols and Punctuation
|
|
6
|
+
[0x3040, 0x309F], // Hiragana
|
|
7
|
+
[0x30A0, 0x30FF], // Katakana
|
|
8
|
+
[0x3100, 0x312F], // Bopomofo
|
|
9
|
+
[0x3130, 0x318F], // Hangul Compatibility Jamo
|
|
10
|
+
[0x3200, 0x32FF], // Enclosed CJK Letters and Months
|
|
11
|
+
[0x3400, 0x4DBF], // CJK Unified Ideographs Extension A
|
|
12
|
+
[0x4E00, 0x9FFF], // CJK Unified Ideographs
|
|
13
|
+
[0xAC00, 0xD7AF], // Hangul Syllables
|
|
14
|
+
[0xF900, 0xFAFF], // CJK Compatibility Ideographs
|
|
15
|
+
[0xFE30, 0xFE4F], // CJK Compatibility Forms
|
|
16
|
+
[0xFF00, 0xFFEF], // Halfwidth and Fullwidth Forms
|
|
17
|
+
[0x20000, 0x2A6DF], // CJK Unified Ideographs Extension B
|
|
18
|
+
[0x2A700, 0x2B73F], // CJK Unified Ideographs Extension C
|
|
19
|
+
[0x2B740, 0x2B81F], // CJK Unified Ideographs Extension D
|
|
20
|
+
];
|
|
21
|
+
const RTL_RANGES = [
|
|
22
|
+
[0x0590, 0x05FF], // Hebrew
|
|
23
|
+
[0x0600, 0x06FF], // Arabic
|
|
24
|
+
[0x0700, 0x074F], // Syriac
|
|
25
|
+
[0x0750, 0x077F], // Arabic Supplement
|
|
26
|
+
[0x0780, 0x07BF], // Thaana
|
|
27
|
+
[0x07C0, 0x07FF], // NKo
|
|
28
|
+
[0x0800, 0x083F], // Samaritan
|
|
29
|
+
[0x0840, 0x085F], // Mandaic
|
|
30
|
+
[0x08A0, 0x08FF], // Arabic Extended-A
|
|
31
|
+
[0xFB50, 0xFDFF], // Arabic Presentation Forms-A
|
|
32
|
+
[0xFE70, 0xFEFF], // Arabic Presentation Forms-B
|
|
33
|
+
[0x10800, 0x1083F], // Cypriot Syllabary
|
|
34
|
+
];
|
|
35
|
+
function inRange(code, ranges) {
|
|
36
|
+
for (const [lo, hi] of ranges) {
|
|
37
|
+
if (code >= lo && code <= hi)
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
export function isCJK(char) {
|
|
43
|
+
const code = char.codePointAt(0) ?? 0;
|
|
44
|
+
return inRange(code, CJK_RANGES);
|
|
45
|
+
}
|
|
46
|
+
export function isRTL(char) {
|
|
47
|
+
const code = char.codePointAt(0) ?? 0;
|
|
48
|
+
return inRange(code, RTL_RANGES);
|
|
49
|
+
}
|
|
50
|
+
export function getCharWidth(char) {
|
|
51
|
+
const code = char.codePointAt(0) ?? 0;
|
|
52
|
+
if (inRange(code, CJK_RANGES))
|
|
53
|
+
return 2;
|
|
54
|
+
if (code >= 0xD800 && code <= 0xDFFF)
|
|
55
|
+
return 0; // surrogate pair half
|
|
56
|
+
if (code >= 0x0300 && code <= 0x036F)
|
|
57
|
+
return 0; // combining diacritical marks
|
|
58
|
+
if (code >= 0x0900 && code <= 0x097F)
|
|
59
|
+
return 1; // Devanagari
|
|
60
|
+
if (code >= 0xFE00 && code <= 0xFE0F)
|
|
61
|
+
return 0; // variation selectors
|
|
62
|
+
if (code >= 0x200B && code <= 0x200F)
|
|
63
|
+
return 0; // zero-width chars
|
|
64
|
+
if (code >= 0x202A && code <= 0x202E)
|
|
65
|
+
return 0; // bidi marks
|
|
66
|
+
if (code >= 0x2060 && code <= 0x206F)
|
|
67
|
+
return 0; // invisible operators
|
|
68
|
+
return 1;
|
|
69
|
+
}
|
|
70
|
+
export function stringWidth(str) {
|
|
71
|
+
let width = 0;
|
|
72
|
+
for (const char of str) {
|
|
73
|
+
width += getCharWidth(char);
|
|
74
|
+
}
|
|
75
|
+
return width;
|
|
76
|
+
}
|
|
77
|
+
export function detectTextDirection(text) {
|
|
78
|
+
let rtlCount = 0;
|
|
79
|
+
let ltrCount = 0;
|
|
80
|
+
for (const char of text) {
|
|
81
|
+
if (isRTL(char))
|
|
82
|
+
rtlCount++;
|
|
83
|
+
else if (/[a-zA-Z]/.test(char))
|
|
84
|
+
ltrCount++;
|
|
85
|
+
}
|
|
86
|
+
if (rtlCount === 0 && ltrCount === 0)
|
|
87
|
+
return 'ltr';
|
|
88
|
+
if (rtlCount > 0 && ltrCount === 0)
|
|
89
|
+
return 'rtl';
|
|
90
|
+
if (rtlCount === 0 && ltrCount > 0)
|
|
91
|
+
return 'ltr';
|
|
92
|
+
return 'mixed';
|
|
93
|
+
}
|
|
94
|
+
export function wrapText(text, maxWidth) {
|
|
95
|
+
const lines = [];
|
|
96
|
+
let currentLine = '';
|
|
97
|
+
let currentWidth = 0;
|
|
98
|
+
for (const char of text) {
|
|
99
|
+
if (char === '\n') {
|
|
100
|
+
lines.push(currentLine);
|
|
101
|
+
currentLine = '';
|
|
102
|
+
currentWidth = 0;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const charWidth = getCharWidth(char);
|
|
106
|
+
if (currentWidth + charWidth > maxWidth && currentLine.length > 0) {
|
|
107
|
+
lines.push(currentLine);
|
|
108
|
+
currentLine = char;
|
|
109
|
+
currentWidth = charWidth;
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
currentLine += char;
|
|
113
|
+
currentWidth += charWidth;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (currentLine.length > 0) {
|
|
117
|
+
lines.push(currentLine);
|
|
118
|
+
}
|
|
119
|
+
return lines;
|
|
120
|
+
}
|
|
121
|
+
export function truncateText(text, maxWidth, ellipsis = '…') {
|
|
122
|
+
const ellipsisWidth = stringWidth(ellipsis);
|
|
123
|
+
let width = 0;
|
|
124
|
+
let result = '';
|
|
125
|
+
for (const char of text) {
|
|
126
|
+
const charWidth = getCharWidth(char);
|
|
127
|
+
if (width + charWidth + ellipsisWidth > maxWidth) {
|
|
128
|
+
return result + ellipsis;
|
|
129
|
+
}
|
|
130
|
+
result += char;
|
|
131
|
+
width += charWidth;
|
|
132
|
+
}
|
|
133
|
+
return result;
|
|
134
|
+
}
|
|
135
|
+
export function applyRTLMarks(text) {
|
|
136
|
+
const dir = detectTextDirection(text);
|
|
137
|
+
if (dir === 'rtl') {
|
|
138
|
+
return `${text}`;
|
|
139
|
+
}
|
|
140
|
+
if (dir === 'mixed') {
|
|
141
|
+
return text.replace(/[--ۿ܀-ݏ]+/g, (match) => {
|
|
142
|
+
return `${match}`;
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
return text;
|
|
146
|
+
}
|
package/dist/tools-extra.js
CHANGED
|
@@ -353,13 +353,7 @@ export const gitCommitTool = tool({
|
|
|
353
353
|
files: z.array(z.string().max(500)).optional().describe('Files to stage (default: all changed files)'),
|
|
354
354
|
}),
|
|
355
355
|
execute: async ({ message, files }) => {
|
|
356
|
-
const
|
|
357
|
-
...process.env,
|
|
358
|
-
GIT_AUTHOR_NAME: GIT_BOT_IDENTITY.name,
|
|
359
|
-
GIT_AUTHOR_EMAIL: GIT_BOT_IDENTITY.email,
|
|
360
|
-
GIT_COMMITTER_NAME: GIT_BOT_IDENTITY.name,
|
|
361
|
-
GIT_COMMITTER_EMAIL: GIT_BOT_IDENTITY.email,
|
|
362
|
-
};
|
|
356
|
+
const fullMessage = message + `\n\nCo-Authored-By: ${GIT_BOT_IDENTITY.name} <${GIT_BOT_IDENTITY.email}>`;
|
|
363
357
|
try {
|
|
364
358
|
if (files && files.length > 0) {
|
|
365
359
|
const safePaths = files.map((f) => sandboxPath(f));
|
|
@@ -372,7 +366,7 @@ export const gitCommitTool = tool({
|
|
|
372
366
|
if (!staged.trim()) {
|
|
373
367
|
return { error: 'Nothing to commit. No changes are staged.' };
|
|
374
368
|
}
|
|
375
|
-
const { stdout } = await execFileAsync('git', ['commit', '-m',
|
|
369
|
+
const { stdout } = await execFileAsync('git', ['commit', '-m', fullMessage], { timeout: _limits.gitTimeoutMs });
|
|
376
370
|
return { success: true, output: stdout.trim() };
|
|
377
371
|
}
|
|
378
372
|
catch (error) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "scalix",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "AI coding agent for the terminal — edit code, run commands, and build with Scalix",
|
|
5
5
|
"main": "dist/headless.js",
|
|
6
6
|
"bin": {
|
|
@@ -61,12 +61,13 @@
|
|
|
61
61
|
"zod": "^3.23.8"
|
|
62
62
|
},
|
|
63
63
|
"devDependencies": {
|
|
64
|
-
"@types/node": "^25.
|
|
64
|
+
"@types/node": "^25.6.0",
|
|
65
65
|
"@types/react": "^19.2.14",
|
|
66
66
|
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
|
67
67
|
"@typescript-eslint/parser": "^8.0.0",
|
|
68
68
|
"@vitest/coverage-v8": "^3.2.4",
|
|
69
69
|
"eslint": "^9.0.0",
|
|
70
|
+
"playwright": "^1.59.1",
|
|
70
71
|
"tsx": "^4.21.0",
|
|
71
72
|
"typescript": "^6.0.2",
|
|
72
73
|
"vitest": "^3.0.0"
|