@vierratale/ai 0.1.0-beta.14 → 0.1.0-beta.16
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/README.md +19 -132
- package/bin/vierrataleai.js +1 -76
- package/build.sh +17 -0
- package/package.json +16 -22
- package/src/catalog.js +49 -78
- package/src/cli.js +121 -1007
- package/src/config.js +10 -26
- package/src/index.js +1 -2
- package/src/installer.js +105 -0
- package/src/prompts/system.json +2 -2
- package/src/prompts/system.md +0 -42
- package/src/providers/anthropic.js +34 -65
- package/src/providers/base.js +0 -10
- package/src/providers/cortex.js +40 -223
- package/src/providers/gemini.js +31 -62
- package/src/providers/openai.js +33 -64
- package/src/session.js +15 -195
- package/src/ui/branding.js +1 -1
- package/src/utils/intents.js +1 -61
- package/src/utils/webfetch.js +24 -576
- package/src/utils/websearch.js +44 -213
- package/LICENSE +0 -21
- package/src/cmd/agent.js +0 -1825
- package/src/cmd/executor.js +0 -271
- package/src/cmd/todos.js +0 -64
- package/src/cmd/tools.js +0 -204
- package/src/engine.js +0 -150
- package/src/ui/chatbox.js +0 -280
- package/src/ui/input.js +0 -501
- package/src/utils/downloader.js +0 -364
- package/src/utils/filetree.js +0 -44
- package/src/utils/filewriter.js +0 -97
- package/src/utils/logger.js +0 -67
- package/src/utils/math.js +0 -142
package/src/cmd/executor.js
DELETED
|
@@ -1,271 +0,0 @@
|
|
|
1
|
-
import { spawn } from 'child_process';
|
|
2
|
-
import { dirname, resolve, sep } from 'path';
|
|
3
|
-
|
|
4
|
-
const ALLOWED_BINS = new Set([
|
|
5
|
-
'mkdir', 'touch', 'ls', 'pwd', 'cat', 'cp', 'mv', 'rm',
|
|
6
|
-
'npm', 'npx', 'node', 'python', 'python3', 'git', 'echo', 'printf',
|
|
7
|
-
'apt', 'apt-get', 'pkg',
|
|
8
|
-
]);
|
|
9
|
-
|
|
10
|
-
// Package-manager commands never run interactively (no TTY), so auto-append a
|
|
11
|
-
// confirm flag to mutating subcommands instead of letting apt/pkg block on a
|
|
12
|
-
// prompt. pkg is the Termux/Android wrapper, apt/apt-get the Linux ones.
|
|
13
|
-
const INSTALL_MANAGERS = new Set(['apt', 'apt-get', 'pkg']);
|
|
14
|
-
const INSTALL_SUBCOMMANDS = new Set(['install', 'upgrade', 'remove', 'autoremove', 'purge']);
|
|
15
|
-
const INSTALL_CONFIRM_FLAGS = /^-y$|^--yes$|^-dy$|^--assume-yes$/i;
|
|
16
|
-
|
|
17
|
-
const SHELL_META = /[;&|<>`$]|\$\{|\(\)|\|\||&&/;
|
|
18
|
-
|
|
19
|
-
// Bins whose *path arguments* must stay inside the workspace. Reads like `ls`
|
|
20
|
-
// are unrestricted on purpose; writes/deletes are contained.
|
|
21
|
-
const PATH_CONTAINED_BINS = new Set(['mkdir', 'touch', 'cat', 'cp', 'mv', 'rm']);
|
|
22
|
-
|
|
23
|
-
const MAX_CAPTURE = 200 * 1024;
|
|
24
|
-
|
|
25
|
-
export function toolResult(overrides = {}) {
|
|
26
|
-
return {
|
|
27
|
-
success: false,
|
|
28
|
-
command: '',
|
|
29
|
-
stdout: '',
|
|
30
|
-
stderr: '',
|
|
31
|
-
exitCode: 0,
|
|
32
|
-
cwd: '',
|
|
33
|
-
timedOut: false,
|
|
34
|
-
...overrides,
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// Tokenizer for a single command line. Supports single/double quotes and
|
|
39
|
-
// backslash escaping so arguments with spaces are handled without a shell.
|
|
40
|
-
export function parseCommandLine(line) {
|
|
41
|
-
const argv = [];
|
|
42
|
-
let buf = null;
|
|
43
|
-
let quote = null;
|
|
44
|
-
const flush = () => {
|
|
45
|
-
if (buf !== null) {
|
|
46
|
-
argv.push(buf);
|
|
47
|
-
buf = null;
|
|
48
|
-
}
|
|
49
|
-
};
|
|
50
|
-
for (let i = 0; i < line.length; i++) {
|
|
51
|
-
const c = line[i];
|
|
52
|
-
if (quote) {
|
|
53
|
-
if (c === quote) {
|
|
54
|
-
quote = null;
|
|
55
|
-
} else if (c === '\\' && quote === '"') {
|
|
56
|
-
if (i + 1 < line.length) buf += line[++i];
|
|
57
|
-
else buf += c;
|
|
58
|
-
} else {
|
|
59
|
-
buf += c;
|
|
60
|
-
}
|
|
61
|
-
continue;
|
|
62
|
-
}
|
|
63
|
-
if (c === "'" || c === '"') {
|
|
64
|
-
if (buf === null) buf = '';
|
|
65
|
-
quote = c;
|
|
66
|
-
continue;
|
|
67
|
-
}
|
|
68
|
-
if (c === ' ' || c === '\t' || c === '\n') {
|
|
69
|
-
flush();
|
|
70
|
-
continue;
|
|
71
|
-
}
|
|
72
|
-
if (c === '\\') {
|
|
73
|
-
if (buf === null) buf = '';
|
|
74
|
-
if (i + 1 < line.length) buf += line[++i];
|
|
75
|
-
else buf += c;
|
|
76
|
-
continue;
|
|
77
|
-
}
|
|
78
|
-
if (buf === null) buf = '';
|
|
79
|
-
buf += c;
|
|
80
|
-
}
|
|
81
|
-
flush();
|
|
82
|
-
return argv;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export function isAllowedBin(bin) {
|
|
86
|
-
if (!/^[A-Za-z0-9._-]+$/.test(bin || '')) return false;
|
|
87
|
-
return ALLOWED_BINS.has(bin);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export class SafeCommandExecutor {
|
|
91
|
-
constructor({ cwd, timeoutMs } = {}) {
|
|
92
|
-
this.cwd = resolve(cwd || process.cwd());
|
|
93
|
-
this.timeoutMs = timeoutMs || 60000;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Validate a command line and normalize it to an argv array, or throw with a
|
|
97
|
-
// human-readable reason.
|
|
98
|
-
validate(argv) {
|
|
99
|
-
if (!argv.length) {
|
|
100
|
-
throw new Error('Empty command — nothing to run.');
|
|
101
|
-
}
|
|
102
|
-
const bin = argv[0];
|
|
103
|
-
if (!isAllowedBin(bin)) {
|
|
104
|
-
throw new Error(`Command "${bin}" is not on the allowed list. Use one of: ${[...ALLOWED_BINS].join(', ')}.`);
|
|
105
|
-
}
|
|
106
|
-
const args = argv.slice(1);
|
|
107
|
-
|
|
108
|
-
for (const a of args) {
|
|
109
|
-
if (SHELL_META.test(a)) {
|
|
110
|
-
throw new Error(`Shell operators and variable syntax are not allowed in arguments: ${JSON.stringify(a)}`);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
if ((bin === 'node' || bin === 'python' || bin === 'python3') && args.includes('-e') || args.includes('-c')) {
|
|
115
|
-
throw new Error(`Inline code (-e/-c) is disabled for ${bin}. Run a file inside the workspace instead (e.g. ${bin} script.js).`);
|
|
116
|
-
}
|
|
117
|
-
if (bin === 'git' && args.includes('clean')) {
|
|
118
|
-
throw new Error('`git clean` is disabled by the safety policy.');
|
|
119
|
-
}
|
|
120
|
-
if (bin === 'rm') {
|
|
121
|
-
this._checkRm(args);
|
|
122
|
-
}
|
|
123
|
-
if (PATH_CONTAINED_BINS.has(bin)) {
|
|
124
|
-
this._checkPaths(bin, args);
|
|
125
|
-
}
|
|
126
|
-
if (INSTALL_MANAGERS.has(bin)) {
|
|
127
|
-
const subIdx = argv.findIndex((a) => INSTALL_SUBCOMMANDS.has(a));
|
|
128
|
-
if (subIdx !== -1 && !argv.some((a) => INSTALL_CONFIRM_FLAGS.test(a))) {
|
|
129
|
-
argv.splice(subIdx + 1, 0, '-y');
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
return argv;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
_resolve(argvIdxArg) {
|
|
136
|
-
const base = resolve(this.cwd);
|
|
137
|
-
const target = resolve(base, argvIdxArg);
|
|
138
|
-
const outside = target !== base && !target.startsWith(base + sep);
|
|
139
|
-
return { base, target, outside };
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
_isHomeish(p) {
|
|
143
|
-
return p === '~' || p.startsWith('~/');
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
_checkRm(args) {
|
|
147
|
-
const targets = args.filter((a) => !a.startsWith('-'));
|
|
148
|
-
if (!targets.length) {
|
|
149
|
-
throw new Error('`rm` needs a target path.');
|
|
150
|
-
}
|
|
151
|
-
for (const t of targets) {
|
|
152
|
-
if (this._isHomeish(t)) {
|
|
153
|
-
throw new Error(`Refusing to remove ${t} — that path is outside the workspace.`);
|
|
154
|
-
}
|
|
155
|
-
if (t === '*' || t.includes('*')) {
|
|
156
|
-
throw new Error('Wildcards are not expanded (commands run without a shell). Give an explicit path for `rm`.');
|
|
157
|
-
}
|
|
158
|
-
const { base, target, outside } = this._resolve(t);
|
|
159
|
-
if (outside || target === base) {
|
|
160
|
-
throw new Error(`Refusing to remove ${t} — it resolves outside the workspace (${base}).`);
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
_checkPaths(bin, args) {
|
|
166
|
-
for (const a of args) {
|
|
167
|
-
if (a.startsWith('-') || a === '--') continue;
|
|
168
|
-
if (this._isHomeish(a)) {
|
|
169
|
-
throw new Error(`Refusing to use ${a} — home-directory paths are outside the workspace.`);
|
|
170
|
-
}
|
|
171
|
-
if (!this._looksLikePath(a)) continue;
|
|
172
|
-
const { base, target, outside } = this._resolve(a);
|
|
173
|
-
if (outside) {
|
|
174
|
-
throw new Error(`${bin}: refusing to touch ${a} — it resolves outside the workspace (${base}).`);
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// Avoid treating plain words (e.g. a file NAME in `npm install foo`) as
|
|
180
|
-
// paths. Only flags, or tokens that contain a path separator or a known
|
|
181
|
-
// relative prefix, are checked.
|
|
182
|
-
_looksLikePath(a) {
|
|
183
|
-
if (a.includes('/') || a.includes('\\') || a.startsWith('.') || a === '..') return true;
|
|
184
|
-
if (/\.[A-Za-z0-9]{1,8}$/.test(a)) return true;
|
|
185
|
-
return false;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
runCommand(commandLine) {
|
|
189
|
-
if (typeof commandLine !== 'string' || !commandLine.trim()) {
|
|
190
|
-
return Promise.resolve(toolResult({
|
|
191
|
-
success: false,
|
|
192
|
-
command: String(commandLine || ''),
|
|
193
|
-
stderr: 'Empty or invalid command.',
|
|
194
|
-
cwd: this.cwd,
|
|
195
|
-
}));
|
|
196
|
-
}
|
|
197
|
-
let argv;
|
|
198
|
-
try {
|
|
199
|
-
argv = this.validate(parseCommandLine(commandLine));
|
|
200
|
-
} catch (err) {
|
|
201
|
-
return Promise.resolve(toolResult({
|
|
202
|
-
success: false,
|
|
203
|
-
command: commandLine,
|
|
204
|
-
stderr: err.message,
|
|
205
|
-
cwd: this.cwd,
|
|
206
|
-
}));
|
|
207
|
-
}
|
|
208
|
-
return this.run(argv, commandLine);
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
run(argv, display = argv.join(' ')) {
|
|
212
|
-
return new Promise((resolvePromise) => {
|
|
213
|
-
let stdout = '';
|
|
214
|
-
let stderr = '';
|
|
215
|
-
let settled = false;
|
|
216
|
-
let timedOut = false;
|
|
217
|
-
let childExitCode = 1;
|
|
218
|
-
|
|
219
|
-
let child;
|
|
220
|
-
try {
|
|
221
|
-
child = spawn(argv[0], argv.slice(1), { cwd: this.cwd, shell: false });
|
|
222
|
-
} catch (err) {
|
|
223
|
-
return resolvePromise(toolResult({
|
|
224
|
-
success: false,
|
|
225
|
-
command: display,
|
|
226
|
-
stderr: err.message,
|
|
227
|
-
cwd: this.cwd,
|
|
228
|
-
}));
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
const finish = (code, dueToTimeout) => {
|
|
232
|
-
if (settled) return;
|
|
233
|
-
settled = true;
|
|
234
|
-
let out = stdout;
|
|
235
|
-
let errOut = stderr;
|
|
236
|
-
if (out.length > MAX_CAPTURE) out = out.slice(-MAX_CAPTURE) + '\n…[truncated]';
|
|
237
|
-
if (errOut.length > MAX_CAPTURE) errOut = errOut.slice(-MAX_CAPTURE) + '\n…[truncated]';
|
|
238
|
-
resolvePromise(toolResult({
|
|
239
|
-
success: code === 0 && !dueToTimeout,
|
|
240
|
-
command: display,
|
|
241
|
-
stdout: out,
|
|
242
|
-
stderr: errOut,
|
|
243
|
-
exitCode: dueToTimeout ? 124 : code,
|
|
244
|
-
cwd: this.cwd,
|
|
245
|
-
timedOut: !!dueToTimeout,
|
|
246
|
-
}));
|
|
247
|
-
};
|
|
248
|
-
|
|
249
|
-
const timer = setTimeout(() => {
|
|
250
|
-
timedOut = true;
|
|
251
|
-
try { child.kill('SIGKILL'); } catch {}
|
|
252
|
-
}, this.timeoutMs);
|
|
253
|
-
|
|
254
|
-
child.stdout.on('data', (d) => {
|
|
255
|
-
if (stdout.length < MAX_CAPTURE * 2) stdout += d.toString();
|
|
256
|
-
});
|
|
257
|
-
child.stderr.on('data', (d) => {
|
|
258
|
-
if (stderr.length < MAX_CAPTURE * 2) stderr += d.toString();
|
|
259
|
-
});
|
|
260
|
-
child.on('error', (err) => {
|
|
261
|
-
clearTimeout(timer);
|
|
262
|
-
stderr = err.message;
|
|
263
|
-
finish(127, false);
|
|
264
|
-
});
|
|
265
|
-
child.on('close', (code) => {
|
|
266
|
-
clearTimeout(timer);
|
|
267
|
-
finish(timedOut ? 124 : (code == null ? 1 : code), timedOut);
|
|
268
|
-
});
|
|
269
|
-
});
|
|
270
|
-
}
|
|
271
|
-
}
|
package/src/cmd/todos.js
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync } from 'fs';
|
|
2
|
-
import { join } from 'path';
|
|
3
|
-
|
|
4
|
-
const FILE_NAME = '.todos.json';
|
|
5
|
-
|
|
6
|
-
export function todosFile(cwd) {
|
|
7
|
-
return join(cwd || '.', FILE_NAME);
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export function loadTodos(cwd) {
|
|
11
|
-
try {
|
|
12
|
-
const data = JSON.parse(readFileSync(todosFile(cwd), 'utf-8'));
|
|
13
|
-
if (!Array.isArray(data)) return [];
|
|
14
|
-
return data.map((t, i) => {
|
|
15
|
-
if (t && typeof t === 'object') {
|
|
16
|
-
return {
|
|
17
|
-
text: String(t.text ?? '').trim(),
|
|
18
|
-
done: Boolean(t.done),
|
|
19
|
-
createdAt: t.createdAt ?? null,
|
|
20
|
-
updatedAt: t.updatedAt ?? null,
|
|
21
|
-
};
|
|
22
|
-
}
|
|
23
|
-
return { text: String(t ?? '').trim(), done: false, createdAt: null, updatedAt: null };
|
|
24
|
-
}).filter((t) => t.text.length > 0);
|
|
25
|
-
} catch {
|
|
26
|
-
return [];
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function saveTodos(cwd, todos) {
|
|
31
|
-
writeFileSync(todosFile(cwd), JSON.stringify(todos, null, 2) + '\n', 'utf-8');
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export function formatTodos(todos) {
|
|
35
|
-
if (!todos || !todos.length) return '(no todos yet)';
|
|
36
|
-
return todos.map((t, i) => `${i + 1}. ${t.done ? '[x]' : '[ ]'} ${t.text}`).join('\n');
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function addTodo(cwd, text) {
|
|
40
|
-
const clean = String(text ?? '').trim();
|
|
41
|
-
if (!clean) return { error: 'Missing todo text.' };
|
|
42
|
-
const todos = loadTodos(cwd);
|
|
43
|
-
todos.push({ text: clean, done: false, createdAt: new Date().toISOString(), updatedAt: null });
|
|
44
|
-
saveTodos(cwd, todos);
|
|
45
|
-
return { okay: true };
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export function updateTodo(cwd, index, { done, text } = {}) {
|
|
49
|
-
const todos = loadTodos(cwd);
|
|
50
|
-
const i = Number(index) - 1;
|
|
51
|
-
if (!Number.isInteger(i) || i < 0 || i >= todos.length) {
|
|
52
|
-
return { error: `No todo #${index}.` };
|
|
53
|
-
}
|
|
54
|
-
if (typeof done === 'boolean') todos[i].done = done;
|
|
55
|
-
if (typeof text === 'string' && text.trim()) todos[i].text = text.trim();
|
|
56
|
-
todos[i].updatedAt = new Date().toISOString();
|
|
57
|
-
saveTodos(cwd, todos);
|
|
58
|
-
return { okay: true };
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export function clearTodos(cwd) {
|
|
62
|
-
saveTodos(cwd, []);
|
|
63
|
-
return { okay: true };
|
|
64
|
-
}
|
package/src/cmd/tools.js
DELETED
|
@@ -1,204 +0,0 @@
|
|
|
1
|
-
import { accessSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync, X_OK } from 'fs';
|
|
2
|
-
import { delimiter, join, resolve, sep } from 'path';
|
|
3
|
-
import { toolResult, SafeCommandExecutor } from './executor.js';
|
|
4
|
-
import { Downloader } from '../utils/downloader.js';
|
|
5
|
-
import { Config } from '../config.js';
|
|
6
|
-
import { loadTodos, addTodo, updateTodo, clearTodos, formatTodos } from './todos.js';
|
|
7
|
-
|
|
8
|
-
// Resolve a path relative to the workspace and require it to stay inside.
|
|
9
|
-
// Throws if the path escapes the workspace (or hits the workspace root for
|
|
10
|
-
// destructive operations).
|
|
11
|
-
export function resolveWithin(cwd, p, { allowRoot = false } = {}) {
|
|
12
|
-
const base = resolve(cwd);
|
|
13
|
-
const target = resolve(base, String(p || ''));
|
|
14
|
-
const rootOrEscape = target === base || !target.startsWith(base + sep);
|
|
15
|
-
if (target !== base && !target.startsWith(base + sep)) {
|
|
16
|
-
throw new Error(`Refusing to access a path outside the workspace: ${p}`);
|
|
17
|
-
}
|
|
18
|
-
if (!allowRoot && target === base && String(p || '') !== '.' && String(p || '') !== '') {
|
|
19
|
-
throw new Error('Refusing to use the workspace root as a file path.');
|
|
20
|
-
}
|
|
21
|
-
return target;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function ok({ command, stdout = '', cwd }) {
|
|
25
|
-
return toolResult({ success: true, command, stdout, stderr: '', exitCode: 0, cwd });
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function fail({ command, stderr, exitCode = 1, cwd }) {
|
|
29
|
-
return toolResult({ success: false, command, stdout: '', stderr, exitCode, cwd });
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export function createFile(cwd, path, content = '') {
|
|
33
|
-
if (!path || typeof path !== 'string' || path.trim() === '') {
|
|
34
|
-
return fail({ command: 'create_file', stderr: 'Missing file path.', cwd });
|
|
35
|
-
}
|
|
36
|
-
try {
|
|
37
|
-
const target = resolveWithin(cwd, path);
|
|
38
|
-
const dir = resolve(target, '..');
|
|
39
|
-
mkdirSync(dir, { recursive: true });
|
|
40
|
-
writeFileSync(target, String(content ?? ''));
|
|
41
|
-
return ok({ command: `create_file("${path}")`, stdout: `created ${path} (${String(content ?? '').length} bytes)`, cwd });
|
|
42
|
-
} catch (err) {
|
|
43
|
-
return fail({ command: `create_file("${path}")`, stderr: err.message, cwd });
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export function createDirectory(cwd, path) {
|
|
48
|
-
if (!path || typeof path !== 'string' || path.trim() === '') {
|
|
49
|
-
return fail({ command: 'create_directory', stderr: 'Missing directory path.', cwd });
|
|
50
|
-
}
|
|
51
|
-
try {
|
|
52
|
-
const target = resolveWithin(cwd, path, { allowRoot: true });
|
|
53
|
-
if (target === resolve(cwd)) {
|
|
54
|
-
return ok({ command: `create_directory("${path}")`, stdout: `already at workspace root`, cwd });
|
|
55
|
-
}
|
|
56
|
-
mkdirSync(target, { recursive: true });
|
|
57
|
-
return ok({ command: `create_directory("${path}")`, stdout: `created directory ${path}`, cwd });
|
|
58
|
-
} catch (err) {
|
|
59
|
-
return fail({ command: `create_directory("${path}")`, stderr: err.message, cwd });
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export function readFile(cwd, path) {
|
|
64
|
-
if (!path || typeof path !== 'string' || path.trim() === '') {
|
|
65
|
-
return fail({ command: 'read_file', stderr: 'Missing file path.', cwd });
|
|
66
|
-
}
|
|
67
|
-
try {
|
|
68
|
-
const target = resolveWithin(cwd, path);
|
|
69
|
-
const content = readFileSync(target, 'utf-8');
|
|
70
|
-
return ok({ command: `read_file("${path}")`, stdout: content, cwd });
|
|
71
|
-
} catch (err) {
|
|
72
|
-
return fail({ command: `read_file("${path}")`, stderr: err.message, cwd });
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export function listDirectory(cwd, path = '.') {
|
|
77
|
-
try {
|
|
78
|
-
const target = resolveWithin(cwd, path, { allowRoot: true });
|
|
79
|
-
const entries = readdirSync(target, { withFileTypes: true });
|
|
80
|
-
const list = entries
|
|
81
|
-
.map((e) => (e.isDirectory() ? `${e.name}/` : e.name))
|
|
82
|
-
.sort()
|
|
83
|
-
.join('\n');
|
|
84
|
-
return ok({ command: `list_directory("${path}")`, stdout: list || '(empty)', cwd });
|
|
85
|
-
} catch (err) {
|
|
86
|
-
return fail({ command: `list_directory("${path}")`, stderr: err.message, cwd });
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export function deleteFile(cwd, path) {
|
|
91
|
-
if (!path || typeof path !== 'string' || path.trim() === '') {
|
|
92
|
-
return fail({ command: 'delete_file', stderr: 'Missing path.', cwd });
|
|
93
|
-
}
|
|
94
|
-
try {
|
|
95
|
-
const target = resolveWithin(cwd, path);
|
|
96
|
-
if (resolve(cwd) === target) {
|
|
97
|
-
return fail({ command: `delete_file("${path}")`, stderr: 'Refusing to delete the workspace root.', cwd });
|
|
98
|
-
}
|
|
99
|
-
if (!existsSync(target)) {
|
|
100
|
-
return fail({ command: `delete_file("${path}")`, stderr: `No such file or directory: ${path}`, exitCode: 1, cwd });
|
|
101
|
-
}
|
|
102
|
-
rmSync(target, { recursive: true, force: true });
|
|
103
|
-
return ok({ command: `delete_file("${path}")`, stdout: `removed ${path}`, cwd });
|
|
104
|
-
} catch (err) {
|
|
105
|
-
return fail({ command: `delete_file("${path}")`, stderr: err.message, cwd });
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// Pick the package manager to use for installing system packages: pkg is the
|
|
110
|
-
// Termux/Android wrapper, everywhere else apt/apt-get.
|
|
111
|
-
export function pickPackageManager() {
|
|
112
|
-
const dirs = (process.env.PATH || '').split(delimiter).filter(Boolean);
|
|
113
|
-
const available = (bin) => dirs.some((dir) => {
|
|
114
|
-
try {
|
|
115
|
-
accessSync(join(dir, bin), X_OK);
|
|
116
|
-
return true;
|
|
117
|
-
} catch {
|
|
118
|
-
return false;
|
|
119
|
-
}
|
|
120
|
-
});
|
|
121
|
-
if (available('pkg')) return 'pkg';
|
|
122
|
-
if (available('apt-get')) return 'apt-get';
|
|
123
|
-
return 'apt';
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
export async function downloadUrl(cwd, url, dir) {
|
|
127
|
-
if (!url || typeof url !== 'string' || !/^https?:\/\//i.test(url.trim())) {
|
|
128
|
-
return fail({ command: 'download_url', stderr: 'Invalid URL. Provide a valid http(s) address.', cwd });
|
|
129
|
-
}
|
|
130
|
-
const targetDir = dir ? resolveWithin(cwd, dir, { allowRoot: true }) : Config.get('downloadDir');
|
|
131
|
-
try {
|
|
132
|
-
const res = await Downloader.download(url.trim(), { dir: targetDir });
|
|
133
|
-
const kind = res.kind || 'file';
|
|
134
|
-
const note = res.extracted ? '\n(contents extracted into that folder)' : '';
|
|
135
|
-
return ok({
|
|
136
|
-
command: `download_url("${url}")`,
|
|
137
|
-
stdout: `downloaded ${kind} → ${res.path}${note}`,
|
|
138
|
-
cwd,
|
|
139
|
-
});
|
|
140
|
-
} catch (err) {
|
|
141
|
-
return fail({ command: 'download_url', stderr: err.message, cwd });
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
export function todoAdd(cwd, text) {
|
|
146
|
-
const err = addTodo(cwd, text);
|
|
147
|
-
if (err && err.error) return fail({ command: `todo_add("${text}")`, stderr: err.error, cwd });
|
|
148
|
-
return ok({ command: `todo_add("${text}")`, stdout: formatTodos(loadTodos(cwd)), cwd });
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
export function todoList(cwd) {
|
|
152
|
-
try {
|
|
153
|
-
return ok({ command: 'todo_list', stdout: formatTodos(loadTodos(cwd)), cwd });
|
|
154
|
-
} catch (err) {
|
|
155
|
-
return fail({ command: 'todo_list', stderr: err.message, cwd });
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
export function todoUpdate(cwd, index, done, text) {
|
|
160
|
-
const err = updateTodo(cwd, index, { done, text });
|
|
161
|
-
if (err && err.error) return fail({ command: `todo_update(${index})`, stderr: err.error, cwd });
|
|
162
|
-
return ok({ command: `todo_update(${index})`, stdout: formatTodos(loadTodos(cwd)), cwd });
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
export function todoClear(cwd) {
|
|
166
|
-
try {
|
|
167
|
-
clearTodos(cwd);
|
|
168
|
-
return ok({ command: 'todo_clear', stdout: '(no todos yet)', cwd });
|
|
169
|
-
} catch (err) {
|
|
170
|
-
return fail({ command: 'todo_clear', stderr: err.message, cwd });
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// Structured tool registry shared with the agent loop.
|
|
175
|
-
export const TOOL_EXECUTORS = {
|
|
176
|
-
run_command: (executor, params) => executor.runCommand(params.command),
|
|
177
|
-
create_file: (executor, params) => createFile(executor.cwd, params.path, params.content),
|
|
178
|
-
create_directory: (executor, params) => createDirectory(executor.cwd, params.path),
|
|
179
|
-
read_file: (executor, params) => readFile(executor.cwd, params.path),
|
|
180
|
-
list_directory: (executor, params) => listDirectory(executor.cwd, params.path || '.'),
|
|
181
|
-
delete_file: (executor, params) => deleteFile(executor.cwd, params.path),
|
|
182
|
-
download_url: (executor, params) => downloadUrl(executor.cwd, params.url, params.dir),
|
|
183
|
-
todo_add: (executor, params) => todoAdd(executor.cwd, params ? params.text : undefined),
|
|
184
|
-
todo_list: (executor, params) => todoList(executor.cwd),
|
|
185
|
-
todo_update: (executor, params) => todoUpdate(executor.cwd, params ? params.index : undefined, params ? params.done : undefined, params ? params.text : undefined),
|
|
186
|
-
todo_clear: (executor, params) => todoClear(executor.cwd),
|
|
187
|
-
};
|
|
188
|
-
|
|
189
|
-
export function makeExecutor({ cwd, timeoutMs } = {}) {
|
|
190
|
-
return new SafeCommandExecutor({ cwd, timeoutMs });
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
export function dispatchTool(executor, call) {
|
|
194
|
-
const fn = TOOL_EXECUTORS[call && call.name];
|
|
195
|
-
if (!fn) {
|
|
196
|
-
return toolResult({
|
|
197
|
-
success: false,
|
|
198
|
-
command: `${call && call.name ? call.name : '(unknown tool)'}`,
|
|
199
|
-
stderr: `Unknown tool ${call && call.name}.`,
|
|
200
|
-
cwd: executor.cwd,
|
|
201
|
-
});
|
|
202
|
-
}
|
|
203
|
-
return fn(executor, call.params || {});
|
|
204
|
-
}
|
package/src/engine.js
DELETED
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
import { execSync, exec } from 'child_process';
|
|
2
|
-
import { existsSync } from 'fs';
|
|
3
|
-
import { Catalog } from './catalog.js';
|
|
4
|
-
import { Config } from './config.js';
|
|
5
|
-
|
|
6
|
-
function runSilent(cmd) {
|
|
7
|
-
return new Promise((resolve) => {
|
|
8
|
-
exec(cmd, { stdio: 'ignore' }, (err) => resolve(!err));
|
|
9
|
-
});
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function isEngineRunning(host) {
|
|
13
|
-
return fetch(`${host}/api/tags`, { signal: AbortSignal.timeout(3000) })
|
|
14
|
-
.then((r) => r.ok)
|
|
15
|
-
.catch(() => false);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function isEngineInstalled() {
|
|
19
|
-
try {
|
|
20
|
-
execSync('which ollama 2>/dev/null', { stdio: 'ignore' });
|
|
21
|
-
return true;
|
|
22
|
-
} catch {}
|
|
23
|
-
const paths = ['/usr/local/bin/ollama', '/usr/bin/ollama'];
|
|
24
|
-
return paths.some((p) => existsSync(p));
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
async function installEngine() {
|
|
28
|
-
await runSilent('curl -fsSL https://ollama.com/install.sh | sh');
|
|
29
|
-
return isEngineInstalled();
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
async function getInstalledModels(host) {
|
|
33
|
-
try {
|
|
34
|
-
const resp = await fetch(`${host}/api/tags`);
|
|
35
|
-
if (!resp.ok) return [];
|
|
36
|
-
const data = await resp.json();
|
|
37
|
-
return (data.models || []).map((m) => m.name);
|
|
38
|
-
} catch {
|
|
39
|
-
return [];
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
async function pullModel(host, model) {
|
|
44
|
-
try {
|
|
45
|
-
const resp = await fetch(`${host}/api/pull`, {
|
|
46
|
-
method: 'POST',
|
|
47
|
-
headers: { 'Content-Type': 'application/json' },
|
|
48
|
-
body: JSON.stringify({ name: model, stream: false }),
|
|
49
|
-
signal: AbortSignal.timeout(600000),
|
|
50
|
-
});
|
|
51
|
-
return resp.ok;
|
|
52
|
-
} catch {
|
|
53
|
-
return false;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function familyOf(model) {
|
|
58
|
-
return model
|
|
59
|
-
.split(':')[0]
|
|
60
|
-
.toLowerCase()
|
|
61
|
-
.replace(/-(coder|instruct|reasoning|chat|base|tiny|nano)$/, '');
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function sizeOf(model) {
|
|
65
|
-
const size = model.split(':')[1] || '';
|
|
66
|
-
const match = size.match(/(\d+(?:\.\d+)?)(m|b)/i);
|
|
67
|
-
if (!match) return Number.POSITIVE_INFINITY;
|
|
68
|
-
const value = parseFloat(match[1]);
|
|
69
|
-
return match[2].toLowerCase() === 'b' ? value * 1000 : value;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function chooseModel(installed, realModel) {
|
|
73
|
-
if (installed.includes(realModel)) {
|
|
74
|
-
return { model: realModel, substituted: false, reason: null };
|
|
75
|
-
}
|
|
76
|
-
const fam = familyOf(realModel);
|
|
77
|
-
let best = null;
|
|
78
|
-
let bestDist = Number.POSITIVE_INFINITY;
|
|
79
|
-
for (const m of installed) {
|
|
80
|
-
if (familyOf(m) !== fam) continue;
|
|
81
|
-
const dist = Math.abs(sizeOf(m) - sizeOf(realModel)) + 0.5;
|
|
82
|
-
if (dist < bestDist) {
|
|
83
|
-
bestDist = dist;
|
|
84
|
-
best = m;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
if (best) {
|
|
88
|
-
return {
|
|
89
|
-
model: best,
|
|
90
|
-
substituted: true,
|
|
91
|
-
reason: `"${realModel}" is not installed; using ${best} (same family)`,
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
return { model: null, substituted: true, reason: `"${realModel}" is not installed` };
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
async function resolveModel(host, realModel) {
|
|
98
|
-
const installed = await getInstalledModels(host);
|
|
99
|
-
return chooseModel(installed, realModel);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
async function ensure() {
|
|
103
|
-
const host = Config.get('engineHost');
|
|
104
|
-
|
|
105
|
-
if (!isEngineInstalled()) {
|
|
106
|
-
await installEngine();
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
const running = await isEngineRunning(host);
|
|
110
|
-
if (!running) {
|
|
111
|
-
try {
|
|
112
|
-
const child = exec('ollama serve', {
|
|
113
|
-
stdio: 'ignore',
|
|
114
|
-
detached: true,
|
|
115
|
-
});
|
|
116
|
-
if (child.unref) child.unref();
|
|
117
|
-
for (let i = 0; i < 30; i++) {
|
|
118
|
-
await new Promise((r) => setTimeout(r, 1000));
|
|
119
|
-
if (await isEngineRunning(host)) break;
|
|
120
|
-
}
|
|
121
|
-
} catch {}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const realModel = Catalog.getRealModel(Config.get('model'));
|
|
125
|
-
const installed = await getInstalledModels(host);
|
|
126
|
-
const resolved = chooseModel(installed, realModel);
|
|
127
|
-
if (resolved.model == null) {
|
|
128
|
-
await pullModel(host, realModel);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
return true;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
async function isReady() {
|
|
135
|
-
const host = Config.get('engineHost');
|
|
136
|
-
return isEngineRunning(host);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
export const Engine = {
|
|
140
|
-
ensure,
|
|
141
|
-
isReady,
|
|
142
|
-
pullModel,
|
|
143
|
-
getInstalledModels,
|
|
144
|
-
resolveModel,
|
|
145
|
-
chooseModel,
|
|
146
|
-
firstInstalled: async (host) => {
|
|
147
|
-
const installed = await getInstalledModels(host);
|
|
148
|
-
return installed[0] || null;
|
|
149
|
-
},
|
|
150
|
-
};
|