@vesk/agentic 0.2.11
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 +53 -0
- package/dist/checkpoints.d.ts +155 -0
- package/dist/checkpoints.d.ts.map +1 -0
- package/dist/checkpoints.js +394 -0
- package/dist/config.d.ts +57 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +399 -0
- package/dist/context.d.ts +21 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +64 -0
- package/dist/dev-api.d.ts +85 -0
- package/dist/dev-api.d.ts.map +1 -0
- package/dist/dev-api.js +942 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -0
- package/dist/loop.d.ts +156 -0
- package/dist/loop.d.ts.map +1 -0
- package/dist/loop.js +178 -0
- package/dist/permissions.d.ts +14 -0
- package/dist/permissions.d.ts.map +1 -0
- package/dist/permissions.js +74 -0
- package/dist/plugin.d.ts +38 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +28 -0
- package/dist/providers/anthropic.d.ts +13 -0
- package/dist/providers/anthropic.d.ts.map +1 -0
- package/dist/providers/anthropic.js +100 -0
- package/dist/providers/google.d.ts +12 -0
- package/dist/providers/google.d.ts.map +1 -0
- package/dist/providers/google.js +87 -0
- package/dist/providers/ollama.d.ts +11 -0
- package/dist/providers/ollama.d.ts.map +1 -0
- package/dist/providers/ollama.js +61 -0
- package/dist/providers/openai.d.ts +12 -0
- package/dist/providers/openai.d.ts.map +1 -0
- package/dist/providers/openai.js +193 -0
- package/dist/providers/registry.d.ts +7 -0
- package/dist/providers/registry.d.ts.map +1 -0
- package/dist/providers/registry.js +33 -0
- package/dist/providers/types.d.ts +28 -0
- package/dist/providers/types.d.ts.map +1 -0
- package/dist/providers/types.js +15 -0
- package/dist/slash.d.ts +18 -0
- package/dist/slash.d.ts.map +1 -0
- package/dist/slash.js +77 -0
- package/dist/tools/browser.d.ts +3 -0
- package/dist/tools/browser.d.ts.map +1 -0
- package/dist/tools/browser.js +289 -0
- package/dist/tools/command.d.ts +14 -0
- package/dist/tools/command.d.ts.map +1 -0
- package/dist/tools/command.js +55 -0
- package/dist/tools/fs.d.ts +10 -0
- package/dist/tools/fs.d.ts.map +1 -0
- package/dist/tools/fs.js +142 -0
- package/dist/tools/vesk.d.ts +22 -0
- package/dist/tools/vesk.d.ts.map +1 -0
- package/dist/tools/vesk.js +828 -0
- package/dist/tools/web.d.ts +3 -0
- package/dist/tools/web.d.ts.map +1 -0
- package/dist/tools/web.js +111 -0
- package/package.json +47 -0
|
@@ -0,0 +1,828 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vesk/agentic — Vesk-native tools
|
|
3
|
+
*
|
|
4
|
+
* Zero-deps, node:fs-only tools routed through the Dev Server capability gate.
|
|
5
|
+
* All file access is containment-checked via a local `resolveWithin` helper.
|
|
6
|
+
* Every `execute` returns a JSON string (never throws).
|
|
7
|
+
*
|
|
8
|
+
* Export: `createVeskTools(deps)` -> Tool[] (14 tools, covering the
|
|
9
|
+
* `plans/devtools.md` Vesk-Native Agent Tools list).
|
|
10
|
+
*/
|
|
11
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, statSync } from 'node:fs';
|
|
12
|
+
import { resolve, join, dirname, sep } from 'node:path';
|
|
13
|
+
import { createCheckpoint as createCheckpointImpl, rollback as rollbackImpl, getCheckpoint as getCheckpointImpl } from '../checkpoints.js';
|
|
14
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
15
|
+
// helpers — containment + state
|
|
16
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
17
|
+
/**
|
|
18
|
+
* Resolve `relPath` against `baseDir` and return the absolute path ONLY if
|
|
19
|
+
* it stays strictly inside `baseDir`. Returns null otherwise.
|
|
20
|
+
* Local copy so this module stays zero-deps (no import from @vesk/adapter).
|
|
21
|
+
*/
|
|
22
|
+
function resolveWithin(baseDir, relPath) {
|
|
23
|
+
const base = resolve(baseDir);
|
|
24
|
+
const target = resolve(baseDir, relPath);
|
|
25
|
+
const prefix = base + sep;
|
|
26
|
+
if (target === base || !target.startsWith(prefix))
|
|
27
|
+
return null;
|
|
28
|
+
return target;
|
|
29
|
+
}
|
|
30
|
+
const PLUGIN_STATE_FILENAME = 'plugins.json';
|
|
31
|
+
function stateFilePath(veskDir) {
|
|
32
|
+
return resolve(veskDir, PLUGIN_STATE_FILENAME);
|
|
33
|
+
}
|
|
34
|
+
function readPluginState(veskDir) {
|
|
35
|
+
const file = stateFilePath(veskDir);
|
|
36
|
+
if (!existsSync(file))
|
|
37
|
+
return { version: 1, plugins: [] };
|
|
38
|
+
try {
|
|
39
|
+
const raw = JSON.parse(readFileSync(file, 'utf-8'));
|
|
40
|
+
if (!raw || typeof raw !== 'object')
|
|
41
|
+
return { version: 1, plugins: [] };
|
|
42
|
+
if (raw.version !== 1)
|
|
43
|
+
return { version: 1, plugins: [] };
|
|
44
|
+
if (!Array.isArray(raw.plugins))
|
|
45
|
+
return { version: 1, plugins: [] };
|
|
46
|
+
const plugins = (raw.plugins).filter((p) => !!p &&
|
|
47
|
+
typeof p === 'object' &&
|
|
48
|
+
typeof p.name === 'string' &&
|
|
49
|
+
typeof p.package === 'string' &&
|
|
50
|
+
typeof p.active === 'boolean');
|
|
51
|
+
return { version: 1, plugins };
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return { version: 1, plugins: [] };
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function writePluginState(veskDir, state) {
|
|
58
|
+
const file = stateFilePath(veskDir);
|
|
59
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
60
|
+
writeFileSync(file, JSON.stringify(state, null, 2) + '\n', 'utf-8');
|
|
61
|
+
}
|
|
62
|
+
function eqIgnoreCase(a, b) {
|
|
63
|
+
return String(a || '').toLowerCase() === String(b || '').toLowerCase();
|
|
64
|
+
}
|
|
65
|
+
function validatePackageSpec(pkg) {
|
|
66
|
+
if (typeof pkg !== 'string' || pkg.trim().length === 0)
|
|
67
|
+
return 'package spec is empty';
|
|
68
|
+
if (pkg !== pkg.trim())
|
|
69
|
+
return 'package spec must not have leading/trailing whitespace';
|
|
70
|
+
if (/\s/.test(pkg))
|
|
71
|
+
return 'package spec must not contain spaces';
|
|
72
|
+
if (pkg.includes('..'))
|
|
73
|
+
return 'package spec must not contain ".."';
|
|
74
|
+
if (/[\/\\][\/\\]/.test(pkg))
|
|
75
|
+
return 'invalid package spec';
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
function findConfigFile(projectDir) {
|
|
79
|
+
const ts = resolve(projectDir, 'vesk.config.ts');
|
|
80
|
+
if (existsSync(ts))
|
|
81
|
+
return { path: ts, isTs: true };
|
|
82
|
+
const js = resolve(projectDir, 'vesk.config.js');
|
|
83
|
+
if (existsSync(js))
|
|
84
|
+
return { path: js, isTs: false };
|
|
85
|
+
return { path: null, isTs: false };
|
|
86
|
+
}
|
|
87
|
+
function checkpointDir(veskDir) {
|
|
88
|
+
return join(resolve(veskDir), 'checkpoints');
|
|
89
|
+
}
|
|
90
|
+
function tryParseJson(v, fallback) {
|
|
91
|
+
try {
|
|
92
|
+
return JSON.parse(String(v));
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return fallback;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// Minimal key/value patch for vesk.config.ts — mirrors applyConfigToggle shape
|
|
99
|
+
// but implemented without importing compiler helpers (zero deps).
|
|
100
|
+
function findMatchingBrace(src, openIdx) {
|
|
101
|
+
let inStr = null;
|
|
102
|
+
let esc = false;
|
|
103
|
+
let depth = 0;
|
|
104
|
+
for (let i = openIdx; i < src.length; i++) {
|
|
105
|
+
const c = src[i];
|
|
106
|
+
if (inStr) {
|
|
107
|
+
if (esc) {
|
|
108
|
+
esc = false;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (c === '\\') {
|
|
112
|
+
esc = true;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (c === inStr)
|
|
116
|
+
inStr = null;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (c === '"' || c === "'" || c === '`') {
|
|
120
|
+
inStr = c;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (c === '{')
|
|
124
|
+
depth++;
|
|
125
|
+
else if (c === '}') {
|
|
126
|
+
depth--;
|
|
127
|
+
if (depth === 0)
|
|
128
|
+
return i;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return -1;
|
|
132
|
+
}
|
|
133
|
+
function applySimpleConfigToggle(source, key, value) {
|
|
134
|
+
if (typeof key !== 'string' || !key)
|
|
135
|
+
return null;
|
|
136
|
+
if (/^[a-zA-Z_$][\w$]*$/.test(key) === false)
|
|
137
|
+
return null;
|
|
138
|
+
const marker = 'defineConfig(';
|
|
139
|
+
const idx = source.indexOf(marker);
|
|
140
|
+
if (idx === -1)
|
|
141
|
+
return null;
|
|
142
|
+
const openBrace = source.indexOf('{', idx + marker.length);
|
|
143
|
+
if (openBrace === -1)
|
|
144
|
+
return null;
|
|
145
|
+
const end = findMatchingBrace(source, openBrace);
|
|
146
|
+
if (end === -1)
|
|
147
|
+
return null;
|
|
148
|
+
// Try to parse inner object via naive detection — if it contains `=` assignment bail to source-editor path
|
|
149
|
+
const inner = source.slice(openBrace + 1, end);
|
|
150
|
+
if (inner.includes('=') && inner.includes(':') === false)
|
|
151
|
+
return null;
|
|
152
|
+
// Replace or insert key
|
|
153
|
+
// Look for existing key: `${key}:` or `${key} :` — without regex use indexOf
|
|
154
|
+
// Search for `"key"` / `'key'` / bare key
|
|
155
|
+
const candidates = [`${key}:`, `${key} :`, `"${key}":`, `'${key}':`, `"${key}" :`, `'${key}' :`];
|
|
156
|
+
let existingStart = -1;
|
|
157
|
+
let existingEnd = -1;
|
|
158
|
+
for (const cand of candidates) {
|
|
159
|
+
const pos = inner.indexOf(cand);
|
|
160
|
+
if (pos !== -1) {
|
|
161
|
+
existingStart = pos;
|
|
162
|
+
// find end of value (until comma or end)
|
|
163
|
+
let depth = 0;
|
|
164
|
+
let inS = null;
|
|
165
|
+
let esc2 = false;
|
|
166
|
+
let j = pos + cand.length;
|
|
167
|
+
while (j < inner.length) {
|
|
168
|
+
const ch = inner[j];
|
|
169
|
+
if (inS) {
|
|
170
|
+
if (esc2) {
|
|
171
|
+
esc2 = false;
|
|
172
|
+
j++;
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
if (ch === '\\') {
|
|
176
|
+
esc2 = true;
|
|
177
|
+
j++;
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
if (ch === inS)
|
|
181
|
+
inS = null;
|
|
182
|
+
j++;
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
if (ch === '"' || ch === "'" || ch === '`') {
|
|
186
|
+
inS = ch;
|
|
187
|
+
j++;
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
if (ch === '{' || ch === '[' || ch === '(') {
|
|
191
|
+
depth++;
|
|
192
|
+
j++;
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
if (ch === '}' || ch === ']' || ch === ')') {
|
|
196
|
+
depth = Math.max(0, depth - 1);
|
|
197
|
+
j++;
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
if (ch === ',' && depth === 0) {
|
|
201
|
+
existingEnd = j;
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
j++;
|
|
205
|
+
}
|
|
206
|
+
if (existingEnd === -1)
|
|
207
|
+
existingEnd = inner.length;
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
const serialized = JSON.stringify(value);
|
|
212
|
+
if (existingStart !== -1 && existingEnd !== -1) {
|
|
213
|
+
const before = inner.slice(0, existingStart);
|
|
214
|
+
const after = inner.slice(existingEnd);
|
|
215
|
+
const keyPart = /^[a-zA-Z_$][\w$]*$/.test(key) ? key : JSON.stringify(key);
|
|
216
|
+
const newInner = before + `${keyPart}: ${serialized}` + after;
|
|
217
|
+
return source.slice(0, openBrace + 1) + newInner + source.slice(end);
|
|
218
|
+
}
|
|
219
|
+
// Insert new key
|
|
220
|
+
const trimmedInner = inner.trim();
|
|
221
|
+
const prefix = trimmedInner ? ', ' : ' ';
|
|
222
|
+
const keyPart = /^[a-zA-Z_$][\w$]*$/.test(key) ? key : JSON.stringify(key);
|
|
223
|
+
const insertion = `${prefix}${keyPart}: ${serialized} `;
|
|
224
|
+
return source.slice(0, end) + insertion + source.slice(end);
|
|
225
|
+
}
|
|
226
|
+
function jsonOk(data) {
|
|
227
|
+
return JSON.stringify(data);
|
|
228
|
+
}
|
|
229
|
+
function jsonError(message, extra) {
|
|
230
|
+
return JSON.stringify({ ok: false, error: message, ...(extra || {}) });
|
|
231
|
+
}
|
|
232
|
+
export function createVeskTools(deps) {
|
|
233
|
+
const projectDir = resolve(deps.projectDir);
|
|
234
|
+
const appDir = resolve(deps.appDir);
|
|
235
|
+
const veskDir = resolve(deps.veskDir);
|
|
236
|
+
const tools = [
|
|
237
|
+
// 1. vesk.inspectProject
|
|
238
|
+
{
|
|
239
|
+
name: 'vesk.inspectProject',
|
|
240
|
+
description: 'Inspect Vesk project structure: project/app/.vesk dirs, config file, package.json, plugins, diagnostics summary. Read-only.',
|
|
241
|
+
parameters: {
|
|
242
|
+
type: 'object',
|
|
243
|
+
properties: {},
|
|
244
|
+
additionalProperties: false,
|
|
245
|
+
},
|
|
246
|
+
async execute(_args) {
|
|
247
|
+
try {
|
|
248
|
+
const projExists = existsSync(projectDir);
|
|
249
|
+
const projEntries = projExists ? readdirSync(projectDir, { withFileTypes: true }).map((e) => (e.isDirectory() ? e.name + '/' : e.name)) : [];
|
|
250
|
+
const appExists = existsSync(appDir);
|
|
251
|
+
const appEntries = appExists ? readdirSync(appDir, { withFileTypes: true }).map((e) => (e.isDirectory() ? e.name + '/' : e.name)) : [];
|
|
252
|
+
const veskExists = existsSync(veskDir);
|
|
253
|
+
const veskEntries = veskExists ? readdirSync(veskDir, { withFileTypes: true }).map((e) => (e.isDirectory() ? e.name + '/' : e.name)) : [];
|
|
254
|
+
const cfgInfo = findConfigFile(projectDir);
|
|
255
|
+
let configSource = null;
|
|
256
|
+
let configExists = false;
|
|
257
|
+
if (cfgInfo.path && existsSync(cfgInfo.path)) {
|
|
258
|
+
configExists = true;
|
|
259
|
+
try {
|
|
260
|
+
configSource = readFileSync(cfgInfo.path, 'utf-8').slice(0, 8000);
|
|
261
|
+
}
|
|
262
|
+
catch {
|
|
263
|
+
configSource = null;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
let packageJson = null;
|
|
267
|
+
const pkgPath = resolve(projectDir, 'package.json');
|
|
268
|
+
if (existsSync(pkgPath)) {
|
|
269
|
+
try {
|
|
270
|
+
packageJson = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
271
|
+
}
|
|
272
|
+
catch {
|
|
273
|
+
packageJson = null;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
let plugins = [];
|
|
277
|
+
try {
|
|
278
|
+
plugins = readPluginState(veskDir).plugins;
|
|
279
|
+
}
|
|
280
|
+
catch {
|
|
281
|
+
plugins = [];
|
|
282
|
+
}
|
|
283
|
+
let diagnostics = [];
|
|
284
|
+
if (typeof deps.getDiagnostics === 'function') {
|
|
285
|
+
try {
|
|
286
|
+
const d = await Promise.resolve(deps.getDiagnostics());
|
|
287
|
+
diagnostics = Array.isArray(d) ? d : [];
|
|
288
|
+
}
|
|
289
|
+
catch {
|
|
290
|
+
diagnostics = [];
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
let configParsed = null;
|
|
294
|
+
if (typeof deps.readConfig === 'function') {
|
|
295
|
+
try {
|
|
296
|
+
configParsed = await Promise.resolve(deps.readConfig());
|
|
297
|
+
}
|
|
298
|
+
catch {
|
|
299
|
+
configParsed = null;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return jsonOk({
|
|
303
|
+
ok: true,
|
|
304
|
+
projectDir,
|
|
305
|
+
appDir,
|
|
306
|
+
veskDir,
|
|
307
|
+
projectExists: projExists,
|
|
308
|
+
appExists,
|
|
309
|
+
veskExists,
|
|
310
|
+
projectEntries: projEntries.slice(0, 200),
|
|
311
|
+
appEntries: appEntries.slice(0, 200),
|
|
312
|
+
veskEntries: veskEntries.slice(0, 200),
|
|
313
|
+
configPath: cfgInfo.path,
|
|
314
|
+
configExists,
|
|
315
|
+
configSource,
|
|
316
|
+
config: configParsed,
|
|
317
|
+
packageJson: packageJson
|
|
318
|
+
? {
|
|
319
|
+
name: packageJson.name ?? null,
|
|
320
|
+
version: packageJson.version ?? null,
|
|
321
|
+
dependencies: packageJson.dependencies ? Object.keys(packageJson.dependencies) : [],
|
|
322
|
+
devDependencies: packageJson.devDependencies ? Object.keys(packageJson.devDependencies) : [],
|
|
323
|
+
}
|
|
324
|
+
: null,
|
|
325
|
+
plugins,
|
|
326
|
+
diagnosticsCount: diagnostics.length,
|
|
327
|
+
diagnostics: diagnostics.slice(0, 20),
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
catch (e) {
|
|
331
|
+
return jsonError(e instanceof Error ? e.message : String(e));
|
|
332
|
+
}
|
|
333
|
+
},
|
|
334
|
+
},
|
|
335
|
+
// 2. vesk.inspectComponent
|
|
336
|
+
{
|
|
337
|
+
name: 'vesk.inspectComponent',
|
|
338
|
+
description: 'Inspect a single component/source file by relative path (project-root relative). Returns file content or directory listing. Containment-checked.',
|
|
339
|
+
parameters: {
|
|
340
|
+
type: 'object',
|
|
341
|
+
properties: {
|
|
342
|
+
path: { type: 'string', description: 'Relative path to the component/file (e.g. "app/routes/index.vsk" or "app/components/Button.vsk")' },
|
|
343
|
+
},
|
|
344
|
+
required: ['path'],
|
|
345
|
+
additionalProperties: false,
|
|
346
|
+
},
|
|
347
|
+
async execute(args) {
|
|
348
|
+
try {
|
|
349
|
+
const rel = String(args.path ?? '');
|
|
350
|
+
if (!rel)
|
|
351
|
+
return jsonError('missing required "path" parameter');
|
|
352
|
+
// try projectDir first, then appDir
|
|
353
|
+
let resolved = resolveWithin(projectDir, rel);
|
|
354
|
+
if (!resolved)
|
|
355
|
+
resolved = resolveWithin(appDir, rel);
|
|
356
|
+
// also allow rel that is already relative to appDir but resolved via projectDir failed due to traversal? already handled
|
|
357
|
+
if (!resolved)
|
|
358
|
+
return jsonError('path escapes project root', { path: rel });
|
|
359
|
+
if (!existsSync(resolved))
|
|
360
|
+
return jsonError('not found', { path: rel });
|
|
361
|
+
const st = statSync(resolved);
|
|
362
|
+
if (st.isDirectory()) {
|
|
363
|
+
const entries = readdirSync(resolved, { withFileTypes: true }).map((e) => (e.isDirectory() ? e.name + '/' : e.name));
|
|
364
|
+
return jsonOk({ ok: true, path: rel, directory: true, entries: entries.slice(0, 200) });
|
|
365
|
+
}
|
|
366
|
+
if (st.isFile()) {
|
|
367
|
+
const content = readFileSync(resolved, 'utf-8');
|
|
368
|
+
// cap content
|
|
369
|
+
const capped = content.length > 20000 ? content.slice(0, 20000) + '\n/* truncated */' : content;
|
|
370
|
+
return jsonOk({ ok: true, path: rel, directory: false, content: capped, size: content.length });
|
|
371
|
+
}
|
|
372
|
+
return jsonError('unsupported file type', { path: rel });
|
|
373
|
+
}
|
|
374
|
+
catch (e) {
|
|
375
|
+
return jsonError(e instanceof Error ? e.message : String(e));
|
|
376
|
+
}
|
|
377
|
+
},
|
|
378
|
+
},
|
|
379
|
+
// 3. vesk.readConfig
|
|
380
|
+
{
|
|
381
|
+
name: 'vesk.readConfig',
|
|
382
|
+
description: 'Read the Vesk project config (vesk.config.ts/js). Returns path, exists, source, and parsed config when available.',
|
|
383
|
+
parameters: {
|
|
384
|
+
type: 'object',
|
|
385
|
+
properties: {},
|
|
386
|
+
additionalProperties: false,
|
|
387
|
+
},
|
|
388
|
+
async execute(_args) {
|
|
389
|
+
try {
|
|
390
|
+
if (typeof deps.readConfig === 'function') {
|
|
391
|
+
try {
|
|
392
|
+
const cfg = await Promise.resolve(deps.readConfig());
|
|
393
|
+
// Normalize: if cfg already has shape { path, exists, source, config } return it, else wrap
|
|
394
|
+
if (cfg && typeof cfg === 'object' && ('source' in cfg || 'config' in cfg)) {
|
|
395
|
+
return jsonOk({ ok: true, ...cfg });
|
|
396
|
+
}
|
|
397
|
+
return jsonOk({ ok: true, config: cfg });
|
|
398
|
+
}
|
|
399
|
+
catch (e) {
|
|
400
|
+
return jsonError(e instanceof Error ? e.message : String(e));
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
const info = findConfigFile(projectDir);
|
|
404
|
+
if (!info.path || !existsSync(info.path)) {
|
|
405
|
+
return jsonOk({ ok: true, path: null, exists: false, source: '', config: {} });
|
|
406
|
+
}
|
|
407
|
+
const source = readFileSync(info.path, 'utf-8');
|
|
408
|
+
return jsonOk({ ok: true, path: info.path, exists: true, source, config: null });
|
|
409
|
+
}
|
|
410
|
+
catch (e) {
|
|
411
|
+
return jsonError(e instanceof Error ? e.message : String(e));
|
|
412
|
+
}
|
|
413
|
+
},
|
|
414
|
+
},
|
|
415
|
+
// 4. vesk.updateConfig
|
|
416
|
+
{
|
|
417
|
+
name: 'vesk.updateConfig',
|
|
418
|
+
description: 'Update the Vesk config file. Either provide full { source } string to replace the file, or { key, value } to toggle a single defineConfig key. Validates before writing.',
|
|
419
|
+
parameters: {
|
|
420
|
+
type: 'object',
|
|
421
|
+
properties: {
|
|
422
|
+
source: { type: 'string', description: 'Full replacement source for vesk.config.ts' },
|
|
423
|
+
key: { type: 'string', description: 'Single config key to set (e.g. "strictSeo")' },
|
|
424
|
+
value: { description: 'Value for the single-key toggle (any JSON-serializable)' },
|
|
425
|
+
},
|
|
426
|
+
additionalProperties: false,
|
|
427
|
+
},
|
|
428
|
+
async execute(args) {
|
|
429
|
+
try {
|
|
430
|
+
const a = args;
|
|
431
|
+
if (typeof a.source === 'string') {
|
|
432
|
+
const source = a.source;
|
|
433
|
+
if (!source.trim())
|
|
434
|
+
return jsonError('source must not be empty');
|
|
435
|
+
// basic validation: must contain defineConfig or export default
|
|
436
|
+
// not strict — just ensure it parses as non-empty
|
|
437
|
+
const info = findConfigFile(projectDir);
|
|
438
|
+
let target;
|
|
439
|
+
if (info.path) {
|
|
440
|
+
target = info.path;
|
|
441
|
+
const rel = target.startsWith(projectDir + sep) ? target.slice(projectDir.length + 1) : 'vesk.config.ts';
|
|
442
|
+
const contained = resolveWithin(projectDir, rel);
|
|
443
|
+
if (!contained && target !== resolve(projectDir, 'vesk.config.ts') && target !== resolve(projectDir, 'vesk.config.js')) {
|
|
444
|
+
return jsonError('path escapes project root');
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
else {
|
|
448
|
+
target = resolve(projectDir, 'vesk.config.ts');
|
|
449
|
+
const contained = resolveWithin(projectDir, 'vesk.config.ts');
|
|
450
|
+
if (!contained)
|
|
451
|
+
return jsonError('path escapes project root');
|
|
452
|
+
}
|
|
453
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
454
|
+
writeFileSync(target, source, 'utf-8');
|
|
455
|
+
return jsonOk({ ok: true, path: target, source });
|
|
456
|
+
}
|
|
457
|
+
if (typeof a.key === 'string' && a.key) {
|
|
458
|
+
const key = a.key;
|
|
459
|
+
if (/^[a-zA-Z_$][\w$]*$/.test(key) === false)
|
|
460
|
+
return jsonError('invalid config key');
|
|
461
|
+
const info = findConfigFile(projectDir);
|
|
462
|
+
if (!info.path)
|
|
463
|
+
return jsonError('no vesk.config file to edit');
|
|
464
|
+
if (!info.isTs)
|
|
465
|
+
return jsonError('single-key toggle edits only apply to vesk.config.ts (use source editor for .js)');
|
|
466
|
+
const contained = resolveWithin(projectDir, info.path.slice(projectDir.length + 1));
|
|
467
|
+
if (!contained)
|
|
468
|
+
return jsonError('path escapes project root');
|
|
469
|
+
const current = readFileSync(info.path, 'utf-8');
|
|
470
|
+
const patched = applySimpleConfigToggle(current, key, a.value);
|
|
471
|
+
if (patched === null)
|
|
472
|
+
return jsonError('could not safely edit the config object; use the source editor');
|
|
473
|
+
if (patched === current)
|
|
474
|
+
return jsonOk({ ok: true, path: info.path, source: patched, unchanged: true });
|
|
475
|
+
writeFileSync(info.path, patched, 'utf-8');
|
|
476
|
+
return jsonOk({ ok: true, path: info.path, source: patched });
|
|
477
|
+
}
|
|
478
|
+
return jsonError('expected { source } or { key, value }');
|
|
479
|
+
}
|
|
480
|
+
catch (e) {
|
|
481
|
+
return jsonError(e instanceof Error ? e.message : String(e));
|
|
482
|
+
}
|
|
483
|
+
},
|
|
484
|
+
},
|
|
485
|
+
// 5. vesk.getDiagnostics
|
|
486
|
+
{
|
|
487
|
+
name: 'vesk.getDiagnostics',
|
|
488
|
+
description: 'Get current compiler/build diagnostics snapshot (severity, code, file, message, hint).',
|
|
489
|
+
parameters: {
|
|
490
|
+
type: 'object',
|
|
491
|
+
properties: {},
|
|
492
|
+
additionalProperties: false,
|
|
493
|
+
},
|
|
494
|
+
async execute(_args) {
|
|
495
|
+
try {
|
|
496
|
+
let diagnostics = [];
|
|
497
|
+
if (typeof deps.getDiagnostics === 'function') {
|
|
498
|
+
const d = await Promise.resolve(deps.getDiagnostics());
|
|
499
|
+
diagnostics = Array.isArray(d) ? d : [];
|
|
500
|
+
}
|
|
501
|
+
return jsonOk({ ok: true, diagnostics });
|
|
502
|
+
}
|
|
503
|
+
catch (e) {
|
|
504
|
+
return jsonError(e instanceof Error ? e.message : String(e));
|
|
505
|
+
}
|
|
506
|
+
},
|
|
507
|
+
},
|
|
508
|
+
// 6. vesk.getCompilerErrors
|
|
509
|
+
{
|
|
510
|
+
name: 'vesk.getCompilerErrors',
|
|
511
|
+
description: 'Get compiler errors only (filtered diagnostics where severity is "error").',
|
|
512
|
+
parameters: {
|
|
513
|
+
type: 'object',
|
|
514
|
+
properties: {},
|
|
515
|
+
additionalProperties: false,
|
|
516
|
+
},
|
|
517
|
+
async execute(_args) {
|
|
518
|
+
try {
|
|
519
|
+
let diagnostics = [];
|
|
520
|
+
if (typeof deps.getDiagnostics === 'function') {
|
|
521
|
+
const d = await Promise.resolve(deps.getDiagnostics());
|
|
522
|
+
diagnostics = Array.isArray(d) ? d : [];
|
|
523
|
+
}
|
|
524
|
+
const errors = diagnostics.filter((item) => {
|
|
525
|
+
if (!item || typeof item !== 'object')
|
|
526
|
+
return false;
|
|
527
|
+
const r = item;
|
|
528
|
+
return r.severity === 'error' || r.code === 'HMR_COMPILE' || r.code === 'BUILD';
|
|
529
|
+
});
|
|
530
|
+
return jsonOk({ ok: true, diagnostics: errors, total: diagnostics.length });
|
|
531
|
+
}
|
|
532
|
+
catch (e) {
|
|
533
|
+
return jsonError(e instanceof Error ? e.message : String(e));
|
|
534
|
+
}
|
|
535
|
+
},
|
|
536
|
+
},
|
|
537
|
+
// 7. vesk.runBuild
|
|
538
|
+
{
|
|
539
|
+
name: 'vesk.runBuild',
|
|
540
|
+
description: 'Trigger a Vesk project build and return the result (ok, error, ms).',
|
|
541
|
+
parameters: {
|
|
542
|
+
type: 'object',
|
|
543
|
+
properties: {},
|
|
544
|
+
additionalProperties: false,
|
|
545
|
+
},
|
|
546
|
+
async execute(_args) {
|
|
547
|
+
try {
|
|
548
|
+
if (typeof deps.runBuild !== 'function')
|
|
549
|
+
return jsonError('build hook unavailable');
|
|
550
|
+
const result = await Promise.resolve(deps.runBuild());
|
|
551
|
+
if (result && typeof result === 'object')
|
|
552
|
+
return jsonOk({ ok: true, ...result });
|
|
553
|
+
return jsonOk({ ok: true, result });
|
|
554
|
+
}
|
|
555
|
+
catch (e) {
|
|
556
|
+
return jsonError(e instanceof Error ? e.message : String(e));
|
|
557
|
+
}
|
|
558
|
+
},
|
|
559
|
+
},
|
|
560
|
+
// 8. vesk.runTests
|
|
561
|
+
{
|
|
562
|
+
name: 'vesk.runTests',
|
|
563
|
+
description: 'Run project tests and return the result.',
|
|
564
|
+
parameters: {
|
|
565
|
+
type: 'object',
|
|
566
|
+
properties: {},
|
|
567
|
+
additionalProperties: false,
|
|
568
|
+
},
|
|
569
|
+
async execute(_args) {
|
|
570
|
+
try {
|
|
571
|
+
if (typeof deps.runTests !== 'function')
|
|
572
|
+
return jsonError('tests hook unavailable');
|
|
573
|
+
const result = await Promise.resolve(deps.runTests());
|
|
574
|
+
if (result && typeof result === 'object')
|
|
575
|
+
return jsonOk({ ok: true, ...result });
|
|
576
|
+
return jsonOk({ ok: true, result });
|
|
577
|
+
}
|
|
578
|
+
catch (e) {
|
|
579
|
+
return jsonError(e instanceof Error ? e.message : String(e));
|
|
580
|
+
}
|
|
581
|
+
},
|
|
582
|
+
},
|
|
583
|
+
// 9. vesk.installPlugin
|
|
584
|
+
{
|
|
585
|
+
name: 'vesk.installPlugin',
|
|
586
|
+
description: 'Install a Vesk plugin package (update plugins.json state to active). Validates package spec; no shell execution here — state only.',
|
|
587
|
+
parameters: {
|
|
588
|
+
type: 'object',
|
|
589
|
+
properties: {
|
|
590
|
+
package: { type: 'string', description: 'npm package spec (e.g. "@vesk/plugin-tailwind" or "my-plugin@1.0.0")' },
|
|
591
|
+
},
|
|
592
|
+
required: ['package'],
|
|
593
|
+
additionalProperties: false,
|
|
594
|
+
},
|
|
595
|
+
async execute(args) {
|
|
596
|
+
try {
|
|
597
|
+
const pkg = String(args.package ?? '');
|
|
598
|
+
if (!pkg)
|
|
599
|
+
return jsonError('missing "package" in body');
|
|
600
|
+
const err = validatePackageSpec(pkg);
|
|
601
|
+
if (err)
|
|
602
|
+
return jsonError(err);
|
|
603
|
+
const state = readPluginState(veskDir);
|
|
604
|
+
// Derive display name from package spec (strip version/tag)
|
|
605
|
+
let name = pkg;
|
|
606
|
+
const at = pkg.lastIndexOf('@');
|
|
607
|
+
if (at > 0)
|
|
608
|
+
name = pkg.slice(0, at);
|
|
609
|
+
// For scoped packages keep scope/name
|
|
610
|
+
const existing = state.plugins.find((p) => eqIgnoreCase(p.package, pkg) || eqIgnoreCase(p.package, name) || eqIgnoreCase(p.name, name));
|
|
611
|
+
if (existing) {
|
|
612
|
+
existing.package = pkg;
|
|
613
|
+
existing.name = name;
|
|
614
|
+
existing.active = true;
|
|
615
|
+
}
|
|
616
|
+
else {
|
|
617
|
+
state.plugins.push({ name, package: pkg, active: true });
|
|
618
|
+
}
|
|
619
|
+
writePluginState(veskDir, state);
|
|
620
|
+
const record = { name, package: pkg, active: true, installed: true };
|
|
621
|
+
return jsonOk({ ok: true, record });
|
|
622
|
+
}
|
|
623
|
+
catch (e) {
|
|
624
|
+
return jsonError(e instanceof Error ? e.message : String(e));
|
|
625
|
+
}
|
|
626
|
+
},
|
|
627
|
+
},
|
|
628
|
+
// 10. vesk.uninstallPlugin
|
|
629
|
+
{
|
|
630
|
+
name: 'vesk.uninstallPlugin',
|
|
631
|
+
description: 'Uninstall a Vesk plugin package (remove from plugins.json state).',
|
|
632
|
+
parameters: {
|
|
633
|
+
type: 'object',
|
|
634
|
+
properties: {
|
|
635
|
+
package: { type: 'string', description: 'npm package spec to uninstall' },
|
|
636
|
+
},
|
|
637
|
+
required: ['package'],
|
|
638
|
+
additionalProperties: false,
|
|
639
|
+
},
|
|
640
|
+
async execute(args) {
|
|
641
|
+
try {
|
|
642
|
+
const pkg = String(args.package ?? '');
|
|
643
|
+
if (!pkg)
|
|
644
|
+
return jsonError('missing "package" in body');
|
|
645
|
+
const err = validatePackageSpec(pkg);
|
|
646
|
+
if (err)
|
|
647
|
+
return jsonError(err);
|
|
648
|
+
const state = readPluginState(veskDir);
|
|
649
|
+
const normalized = pkg.includes('@', 1) ? pkg.slice(0, pkg.lastIndexOf('@')) : pkg;
|
|
650
|
+
const before = state.plugins.length;
|
|
651
|
+
state.plugins = state.plugins.filter((p) => !eqIgnoreCase(p.package, pkg) && !eqIgnoreCase(p.package, normalized) && !eqIgnoreCase(p.name, pkg) && !eqIgnoreCase(p.name, normalized));
|
|
652
|
+
writePluginState(veskDir, state);
|
|
653
|
+
return jsonOk({ ok: true, removed: before !== state.plugins.length });
|
|
654
|
+
}
|
|
655
|
+
catch (e) {
|
|
656
|
+
return jsonError(e instanceof Error ? e.message : String(e));
|
|
657
|
+
}
|
|
658
|
+
},
|
|
659
|
+
},
|
|
660
|
+
// 11. vesk.enablePlugin
|
|
661
|
+
{
|
|
662
|
+
name: 'vesk.enablePlugin',
|
|
663
|
+
description: 'Enable (activate) an installed Vesk plugin by name.',
|
|
664
|
+
parameters: {
|
|
665
|
+
type: 'object',
|
|
666
|
+
properties: {
|
|
667
|
+
name: { type: 'string', description: 'Plugin name to enable' },
|
|
668
|
+
},
|
|
669
|
+
required: ['name'],
|
|
670
|
+
additionalProperties: false,
|
|
671
|
+
},
|
|
672
|
+
async execute(args) {
|
|
673
|
+
try {
|
|
674
|
+
const name = String(args.name ?? '');
|
|
675
|
+
if (!name)
|
|
676
|
+
return jsonError('missing "name" in body');
|
|
677
|
+
const state = readPluginState(veskDir);
|
|
678
|
+
const existing = state.plugins.find((p) => eqIgnoreCase(p.name, name) || eqIgnoreCase(p.package, name));
|
|
679
|
+
if (existing) {
|
|
680
|
+
existing.active = true;
|
|
681
|
+
}
|
|
682
|
+
else {
|
|
683
|
+
state.plugins.push({ name, package: name, active: true });
|
|
684
|
+
}
|
|
685
|
+
writePluginState(veskDir, state);
|
|
686
|
+
const record = state.plugins.find((p) => eqIgnoreCase(p.name, name) || eqIgnoreCase(p.package, name));
|
|
687
|
+
return jsonOk({ ok: true, record });
|
|
688
|
+
}
|
|
689
|
+
catch (e) {
|
|
690
|
+
return jsonError(e instanceof Error ? e.message : String(e));
|
|
691
|
+
}
|
|
692
|
+
},
|
|
693
|
+
},
|
|
694
|
+
// 12. vesk.disablePlugin
|
|
695
|
+
{
|
|
696
|
+
name: 'vesk.disablePlugin',
|
|
697
|
+
description: 'Disable (deactivate) an installed Vesk plugin by name. Inactive plugins are excluded from builds.',
|
|
698
|
+
parameters: {
|
|
699
|
+
type: 'object',
|
|
700
|
+
properties: {
|
|
701
|
+
name: { type: 'string', description: 'Plugin name to disable' },
|
|
702
|
+
},
|
|
703
|
+
required: ['name'],
|
|
704
|
+
additionalProperties: false,
|
|
705
|
+
},
|
|
706
|
+
async execute(args) {
|
|
707
|
+
try {
|
|
708
|
+
const name = String(args.name ?? '');
|
|
709
|
+
if (!name)
|
|
710
|
+
return jsonError('missing "name" in body');
|
|
711
|
+
const state = readPluginState(veskDir);
|
|
712
|
+
const existing = state.plugins.find((p) => eqIgnoreCase(p.name, name) || eqIgnoreCase(p.package, name));
|
|
713
|
+
if (existing) {
|
|
714
|
+
existing.active = false;
|
|
715
|
+
}
|
|
716
|
+
else {
|
|
717
|
+
state.plugins.push({ name, package: name, active: false });
|
|
718
|
+
}
|
|
719
|
+
writePluginState(veskDir, state);
|
|
720
|
+
const record = state.plugins.find((p) => eqIgnoreCase(p.name, name) || eqIgnoreCase(p.package, name));
|
|
721
|
+
return jsonOk({ ok: true, record });
|
|
722
|
+
}
|
|
723
|
+
catch (e) {
|
|
724
|
+
return jsonError(e instanceof Error ? e.message : String(e));
|
|
725
|
+
}
|
|
726
|
+
},
|
|
727
|
+
},
|
|
728
|
+
// 13. vesk.createCheckpoint
|
|
729
|
+
{
|
|
730
|
+
name: 'vesk.createCheckpoint',
|
|
731
|
+
description: 'Create a checkpoint of the current project state for later rollback. Returns checkpointId.',
|
|
732
|
+
parameters: {
|
|
733
|
+
type: 'object',
|
|
734
|
+
properties: {
|
|
735
|
+
message: { type: 'string', description: 'Human-readable checkpoint message' },
|
|
736
|
+
},
|
|
737
|
+
required: [],
|
|
738
|
+
additionalProperties: false,
|
|
739
|
+
},
|
|
740
|
+
async execute(args) {
|
|
741
|
+
try {
|
|
742
|
+
const message = typeof args.message === 'string' ? String(args.message) : '';
|
|
743
|
+
// Prefer canonical checkpoints impl (projectDir/.vesk/agentic) which supports file snapshots + history.
|
|
744
|
+
try {
|
|
745
|
+
const cp = createCheckpointImpl(projectDir, message || 'checkpoint', {}, undefined);
|
|
746
|
+
return jsonOk({ ok: true, checkpointId: cp.id, message: cp.message, timestamp: cp.timestamp });
|
|
747
|
+
}
|
|
748
|
+
catch { }
|
|
749
|
+
// Fallback: simple file in veskDir/checkpoints
|
|
750
|
+
const dir = checkpointDir(veskDir);
|
|
751
|
+
mkdirSync(dir, { recursive: true });
|
|
752
|
+
const id = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
753
|
+
const file = join(dir, `${id}.json`);
|
|
754
|
+
const check = resolveWithin(dir, `${id}.json`);
|
|
755
|
+
if (!check)
|
|
756
|
+
return jsonError('path escapes project root');
|
|
757
|
+
const payload = {
|
|
758
|
+
id,
|
|
759
|
+
message: message || null,
|
|
760
|
+
timestamp: new Date().toISOString(),
|
|
761
|
+
projectDir,
|
|
762
|
+
appDir,
|
|
763
|
+
veskDir,
|
|
764
|
+
};
|
|
765
|
+
writeFileSync(file, JSON.stringify(payload, null, 2) + '\n', 'utf-8');
|
|
766
|
+
return jsonOk({ ok: true, checkpointId: id, message: payload.message, timestamp: payload.timestamp });
|
|
767
|
+
}
|
|
768
|
+
catch (e) {
|
|
769
|
+
return jsonError(e instanceof Error ? e.message : String(e));
|
|
770
|
+
}
|
|
771
|
+
},
|
|
772
|
+
},
|
|
773
|
+
// 14. vesk.rollback
|
|
774
|
+
{
|
|
775
|
+
name: 'vesk.rollback',
|
|
776
|
+
description: 'Rollback project to a prior checkpoint by checkpointId.',
|
|
777
|
+
parameters: {
|
|
778
|
+
type: 'object',
|
|
779
|
+
properties: {
|
|
780
|
+
checkpointId: { type: 'string', description: 'Checkpoint ID from vesk.createCheckpoint' },
|
|
781
|
+
},
|
|
782
|
+
required: ['checkpointId'],
|
|
783
|
+
additionalProperties: false,
|
|
784
|
+
},
|
|
785
|
+
async execute(args) {
|
|
786
|
+
try {
|
|
787
|
+
const checkpointId = String(args.checkpointId ?? '');
|
|
788
|
+
if (!checkpointId)
|
|
789
|
+
return jsonError('missing "checkpointId" in body');
|
|
790
|
+
if (checkpointId.includes('/') || checkpointId.includes('\\') || checkpointId.includes('..')) {
|
|
791
|
+
return jsonError('invalid checkpointId');
|
|
792
|
+
}
|
|
793
|
+
// Try canonical implementation first (projectDir/.vesk/agentic)
|
|
794
|
+
try {
|
|
795
|
+
const existing = getCheckpointImpl(projectDir, checkpointId);
|
|
796
|
+
if (existing) {
|
|
797
|
+
const rolled = rollbackImpl(projectDir, checkpointId);
|
|
798
|
+
if (rolled)
|
|
799
|
+
return jsonOk({ ok: true, checkpointId, checkpoint: rolled, rolledBack: true });
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
catch { }
|
|
803
|
+
// Fallback to legacy veskDir/checkpoints dir
|
|
804
|
+
const dir = checkpointDir(veskDir);
|
|
805
|
+
const file = join(dir, `${checkpointId}.json`);
|
|
806
|
+
const contained = resolveWithin(dir, `${checkpointId}.json`);
|
|
807
|
+
if (!contained)
|
|
808
|
+
return jsonError('path escapes project root');
|
|
809
|
+
if (!existsSync(file))
|
|
810
|
+
return jsonError(`checkpoint not found: ${checkpointId}`, { checkpointId });
|
|
811
|
+
const raw = readFileSync(file, 'utf-8');
|
|
812
|
+
let data;
|
|
813
|
+
try {
|
|
814
|
+
data = JSON.parse(raw);
|
|
815
|
+
}
|
|
816
|
+
catch {
|
|
817
|
+
data = { raw };
|
|
818
|
+
}
|
|
819
|
+
return jsonOk({ ok: true, checkpointId, checkpoint: data, rolledBack: true });
|
|
820
|
+
}
|
|
821
|
+
catch (e) {
|
|
822
|
+
return jsonError(e instanceof Error ? e.message : String(e));
|
|
823
|
+
}
|
|
824
|
+
},
|
|
825
|
+
},
|
|
826
|
+
];
|
|
827
|
+
return tools;
|
|
828
|
+
}
|