@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.
Files changed (62) hide show
  1. package/README.md +53 -0
  2. package/dist/checkpoints.d.ts +155 -0
  3. package/dist/checkpoints.d.ts.map +1 -0
  4. package/dist/checkpoints.js +394 -0
  5. package/dist/config.d.ts +57 -0
  6. package/dist/config.d.ts.map +1 -0
  7. package/dist/config.js +399 -0
  8. package/dist/context.d.ts +21 -0
  9. package/dist/context.d.ts.map +1 -0
  10. package/dist/context.js +64 -0
  11. package/dist/dev-api.d.ts +85 -0
  12. package/dist/dev-api.d.ts.map +1 -0
  13. package/dist/dev-api.js +942 -0
  14. package/dist/index.d.ts +25 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +23 -0
  17. package/dist/loop.d.ts +156 -0
  18. package/dist/loop.d.ts.map +1 -0
  19. package/dist/loop.js +178 -0
  20. package/dist/permissions.d.ts +14 -0
  21. package/dist/permissions.d.ts.map +1 -0
  22. package/dist/permissions.js +74 -0
  23. package/dist/plugin.d.ts +38 -0
  24. package/dist/plugin.d.ts.map +1 -0
  25. package/dist/plugin.js +28 -0
  26. package/dist/providers/anthropic.d.ts +13 -0
  27. package/dist/providers/anthropic.d.ts.map +1 -0
  28. package/dist/providers/anthropic.js +100 -0
  29. package/dist/providers/google.d.ts +12 -0
  30. package/dist/providers/google.d.ts.map +1 -0
  31. package/dist/providers/google.js +87 -0
  32. package/dist/providers/ollama.d.ts +11 -0
  33. package/dist/providers/ollama.d.ts.map +1 -0
  34. package/dist/providers/ollama.js +61 -0
  35. package/dist/providers/openai.d.ts +12 -0
  36. package/dist/providers/openai.d.ts.map +1 -0
  37. package/dist/providers/openai.js +193 -0
  38. package/dist/providers/registry.d.ts +7 -0
  39. package/dist/providers/registry.d.ts.map +1 -0
  40. package/dist/providers/registry.js +33 -0
  41. package/dist/providers/types.d.ts +28 -0
  42. package/dist/providers/types.d.ts.map +1 -0
  43. package/dist/providers/types.js +15 -0
  44. package/dist/slash.d.ts +18 -0
  45. package/dist/slash.d.ts.map +1 -0
  46. package/dist/slash.js +77 -0
  47. package/dist/tools/browser.d.ts +3 -0
  48. package/dist/tools/browser.d.ts.map +1 -0
  49. package/dist/tools/browser.js +289 -0
  50. package/dist/tools/command.d.ts +14 -0
  51. package/dist/tools/command.d.ts.map +1 -0
  52. package/dist/tools/command.js +55 -0
  53. package/dist/tools/fs.d.ts +10 -0
  54. package/dist/tools/fs.d.ts.map +1 -0
  55. package/dist/tools/fs.js +142 -0
  56. package/dist/tools/vesk.d.ts +22 -0
  57. package/dist/tools/vesk.d.ts.map +1 -0
  58. package/dist/tools/vesk.js +828 -0
  59. package/dist/tools/web.d.ts +3 -0
  60. package/dist/tools/web.d.ts.map +1 -0
  61. package/dist/tools/web.js +111 -0
  62. 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
+ }