@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
package/dist/dev-api.js
ADDED
|
@@ -0,0 +1,942 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dev API router — `createAgentRouter` (B6-plug of plans/devtools.md).
|
|
3
|
+
*
|
|
4
|
+
* A self-contained, dependency-injectable router for the agentic
|
|
5
|
+
* dev-panel HTTP endpoints under `/__vesk/agent/*`. Mirrors the shape of
|
|
6
|
+
* `createDevApiRouter` from `@vesk/adapter/src/dev-api.ts`:
|
|
7
|
+
*
|
|
8
|
+
* POST /__vesk/agent/run → run the agent (prompt, mode, providerConfig)
|
|
9
|
+
* GET /__vesk/agent/history → list checkpoints (history.json)
|
|
10
|
+
* POST /__vesk/agent/checkpoint → create a checkpoint
|
|
11
|
+
* POST /__vesk/agent/rollback → rollback to a checkpoint
|
|
12
|
+
*
|
|
13
|
+
* ARCHITECTURE: the Dev Server is the ONLY path from browser → project files /
|
|
14
|
+
* build system. Every endpoint is gated by an `AgentCapability` in
|
|
15
|
+
* `AgentCapabilityTable` (server-enforced — the browser cannot bypass it).
|
|
16
|
+
* There is NO raw `child_process` reach: the agent's `command.execute` tool
|
|
17
|
+
* routes through the dev server's allowlisted `runCommand` hook, and file
|
|
18
|
+
* access is containment-checked.
|
|
19
|
+
*
|
|
20
|
+
* Pure (fake injectable inputs, no socket/listener), mirroring
|
|
21
|
+
* `createPluginStateRouter` / `createDevApiRouter`: returns
|
|
22
|
+
* `{ route(method, pathname, body, search) }`, yielding `null` for
|
|
23
|
+
* non-`/ __vesk/agent/*` paths so the dev server can fall through to the
|
|
24
|
+
* next router (e.g. the adapter's `createDevApiRouter`).
|
|
25
|
+
*
|
|
26
|
+
* Zero deps — only local `@vesk/agentic` modules; no npm dependencies
|
|
27
|
+
* beyond `@vesk/types`.
|
|
28
|
+
*/
|
|
29
|
+
import { CheckpointManager } from './checkpoints.js';
|
|
30
|
+
import { loadAgenticConfig, saveAgenticConfig, getApiKey, saveApiKey, providerDotenvVar as defaultVkVar, readDotenvValue } from './config.js';
|
|
31
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
32
|
+
import { resolve } from 'node:path';
|
|
33
|
+
import { openAiProvider } from './providers/openai.js';
|
|
34
|
+
import { anthropicProvider } from './providers/anthropic.js';
|
|
35
|
+
import { googleProvider } from './providers/google.js';
|
|
36
|
+
import { ollamaProvider } from './providers/ollama.js';
|
|
37
|
+
import { SLASH_COMMANDS } from './slash.js';
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
// helpers
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
function jsonStatus(status, data) {
|
|
42
|
+
return {
|
|
43
|
+
status,
|
|
44
|
+
headers: { 'Content-Type': 'application/json' },
|
|
45
|
+
body: JSON.stringify(data),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function sseEvent(type, data) {
|
|
49
|
+
return 'event: ' + type + '\n' + 'data: ' + JSON.stringify(data) + '\n\n';
|
|
50
|
+
}
|
|
51
|
+
function badRequest(message) {
|
|
52
|
+
return jsonStatus(400, { error: message });
|
|
53
|
+
}
|
|
54
|
+
function denied(cap) {
|
|
55
|
+
return jsonStatus(403, { error: `capability denied: ${cap}` });
|
|
56
|
+
}
|
|
57
|
+
// per-provider helpers — shared with config.ts but duplicated here to avoid circular
|
|
58
|
+
// Strongly avoid leaking raw keys — only masked previews leave this module.
|
|
59
|
+
const SUPPORTED_PROVIDERS = [
|
|
60
|
+
'openai', 'anthropic', 'google', 'ollama',
|
|
61
|
+
'opencode', 'opencode-go', 'openrouter', 'loopers', 'custom',
|
|
62
|
+
];
|
|
63
|
+
function maskPreview(key) {
|
|
64
|
+
if (!key)
|
|
65
|
+
return null;
|
|
66
|
+
const trimmed = String(key).trim();
|
|
67
|
+
if (!trimmed)
|
|
68
|
+
return null;
|
|
69
|
+
if (trimmed.length <= 8)
|
|
70
|
+
return '***';
|
|
71
|
+
return trimmed.slice(0, 7) + '***' + trimmed.slice(-4);
|
|
72
|
+
}
|
|
73
|
+
function isValidProvider(provider) {
|
|
74
|
+
const normalized = provider.trim().toLowerCase();
|
|
75
|
+
return SUPPORTED_PROVIDERS.includes(normalized);
|
|
76
|
+
}
|
|
77
|
+
function normalizeProvider(provider) {
|
|
78
|
+
return provider.trim().toLowerCase();
|
|
79
|
+
}
|
|
80
|
+
function sanitizeProviderName(provider) {
|
|
81
|
+
const p = provider.trim().toLowerCase();
|
|
82
|
+
if (!p)
|
|
83
|
+
return 'default';
|
|
84
|
+
return p.replace(/[\/\\]+/g, '_').replace(/[^a-z0-9_.-]/g, '_') || 'default';
|
|
85
|
+
}
|
|
86
|
+
// Precise per-provider key lookup from project .env.local VK_{PROVIDER}_KEY.
|
|
87
|
+
// The CLI loads .env.local into process.env at startup, so process.env is the
|
|
88
|
+
// primary read; a direct file read is a defensive fallback for edits made
|
|
89
|
+
// after startup.
|
|
90
|
+
function getPerProviderKeyRaw(projectDir, provider) {
|
|
91
|
+
const prov = normalizeProvider(provider);
|
|
92
|
+
const dotenvName = defaultVkVar(prov);
|
|
93
|
+
// 1. .env.local VK_*_KEY (mirrored in process.env)
|
|
94
|
+
try {
|
|
95
|
+
const v = process.env[dotenvName];
|
|
96
|
+
if (v && v.trim())
|
|
97
|
+
return v.trim();
|
|
98
|
+
}
|
|
99
|
+
catch { }
|
|
100
|
+
try {
|
|
101
|
+
const df = readDotenvValue(projectDir, dotenvName);
|
|
102
|
+
if (df && df.trim())
|
|
103
|
+
return df.trim();
|
|
104
|
+
}
|
|
105
|
+
catch { }
|
|
106
|
+
// 2. legacy per-provider file .vesk/agentic/keys/{provider}.key (compat)
|
|
107
|
+
try {
|
|
108
|
+
const p = resolve(projectDir, '.vesk', 'agentic', 'keys', `${sanitizeProviderName(prov)}.key`);
|
|
109
|
+
if (existsSync(p)) {
|
|
110
|
+
const v = readFileSync(p, 'utf-8').trim();
|
|
111
|
+
if (v)
|
|
112
|
+
return v;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
catch { }
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
function buildKeysMap(projectDir) {
|
|
119
|
+
const out = {};
|
|
120
|
+
for (const p of SUPPORTED_PROVIDERS) {
|
|
121
|
+
try {
|
|
122
|
+
const k = getPerProviderKeyRaw(projectDir, p);
|
|
123
|
+
out[p] = !!k && String(k).trim().length > 0;
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
out[p] = false;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return out;
|
|
130
|
+
}
|
|
131
|
+
function buildPreviewsMap(projectDir) {
|
|
132
|
+
const out = {};
|
|
133
|
+
for (const p of SUPPORTED_PROVIDERS) {
|
|
134
|
+
try {
|
|
135
|
+
const k = getPerProviderKeyRaw(projectDir, p);
|
|
136
|
+
out[p] = maskPreview(k);
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
out[p] = null;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return out;
|
|
143
|
+
}
|
|
144
|
+
function allowsAny(table, caps) {
|
|
145
|
+
for (const cap of caps) {
|
|
146
|
+
try {
|
|
147
|
+
if (table.allows(cap))
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
// ignore
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
// ---------------------------------------------------------------------------
|
|
157
|
+
// createAgentRouter
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
export function createAgentRouter(opts) {
|
|
160
|
+
const projectDir = opts.projectDir;
|
|
161
|
+
const veskDir = opts.veskDir;
|
|
162
|
+
const appDir = opts.appDir;
|
|
163
|
+
void veskDir;
|
|
164
|
+
void appDir;
|
|
165
|
+
// Fallback in-memory store when injections are absent. Each router gets
|
|
166
|
+
// its own manager so concurrent routers do not share state; callers that
|
|
167
|
+
// need persistence should inject `listCheckpoints`/`rollback` closures
|
|
168
|
+
// backed by the filesystem (e.g. old `checkpoints.ts` fs helpers) or a
|
|
169
|
+
// shared CheckpointManager instance.
|
|
170
|
+
const fallbackManager = new CheckpointManager();
|
|
171
|
+
async function route(method, pathname, body, _search) {
|
|
172
|
+
// Only handle /__vesk/agent/* — return null so the dev server can
|
|
173
|
+
// fall through to the adapter router or normal page handling.
|
|
174
|
+
if (!pathname.startsWith('/__vesk/agent/') && pathname !== '/__vesk/agent') {
|
|
175
|
+
if (pathname.startsWith('/__vesk/')) {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
let table = null;
|
|
181
|
+
try {
|
|
182
|
+
const maybe = opts.getPermissions();
|
|
183
|
+
table = maybe instanceof Promise ? await maybe : maybe;
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
return jsonStatus(403, { error: 'capability denied: unknown' });
|
|
187
|
+
}
|
|
188
|
+
// ── POST /__vesk/agent/run ──────────────────────────────────────────
|
|
189
|
+
if (pathname === '/__vesk/agent/run') {
|
|
190
|
+
if (method !== 'POST')
|
|
191
|
+
return jsonStatus(405, { error: 'method not allowed' });
|
|
192
|
+
if (!table || typeof table.allows !== 'function') {
|
|
193
|
+
return denied('readFiles');
|
|
194
|
+
}
|
|
195
|
+
try {
|
|
196
|
+
if (!table.allows('readFiles')) {
|
|
197
|
+
return denied('readFiles');
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
return denied('readFiles');
|
|
202
|
+
}
|
|
203
|
+
const b = (body ?? {});
|
|
204
|
+
const promptRaw = b.prompt ?? b.input ?? b.message ?? b.query;
|
|
205
|
+
const prompt = typeof promptRaw === 'string' ? promptRaw : '';
|
|
206
|
+
if (!prompt || !prompt.trim())
|
|
207
|
+
return badRequest('missing "prompt" in body');
|
|
208
|
+
const rawMode = typeof b.mode === 'string' ? b.mode : table.mode;
|
|
209
|
+
const validModes = ['explore', 'debug', 'agent'];
|
|
210
|
+
const mode = validModes.includes(rawMode)
|
|
211
|
+
? rawMode
|
|
212
|
+
: table.mode;
|
|
213
|
+
const providerConfig = (b.providerConfig ??
|
|
214
|
+
b.provider ??
|
|
215
|
+
b.config ??
|
|
216
|
+
b.provider_config ??
|
|
217
|
+
undefined);
|
|
218
|
+
// Thread the requested step budget into the run path so a per-request
|
|
219
|
+
// setting (e.g. from the dev-panel UI max-steps control) actually applies.
|
|
220
|
+
const pc = (providerConfig && typeof providerConfig === 'object' ? providerConfig : {});
|
|
221
|
+
if (typeof b.maxSteps === 'number' && Number.isFinite(b.maxSteps))
|
|
222
|
+
pc.maxSteps = b.maxSteps;
|
|
223
|
+
if (typeof opts.runAgent !== 'function') {
|
|
224
|
+
return jsonStatus(503, { error: 'agent runner unavailable' });
|
|
225
|
+
}
|
|
226
|
+
if (b.stream === true && typeof opts.runAgentStream === 'function') {
|
|
227
|
+
const headers = {
|
|
228
|
+
'Content-Type': 'text/event-stream',
|
|
229
|
+
'Cache-Control': 'no-cache',
|
|
230
|
+
Connection: 'keep-alive',
|
|
231
|
+
'X-Accel-Buffering': 'no',
|
|
232
|
+
};
|
|
233
|
+
const generator = (async function* () {
|
|
234
|
+
let events;
|
|
235
|
+
try {
|
|
236
|
+
events = await opts.runAgentStream(prompt, mode, pc);
|
|
237
|
+
}
|
|
238
|
+
catch (e) {
|
|
239
|
+
yield sseEvent('error', { message: e instanceof Error ? e.message : String(e) });
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
try {
|
|
243
|
+
for await (const ev of events) {
|
|
244
|
+
yield sseEvent(ev.type, ev);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
catch (e) {
|
|
248
|
+
yield sseEvent('error', { message: e instanceof Error ? e.message : String(e) });
|
|
249
|
+
}
|
|
250
|
+
})();
|
|
251
|
+
return {
|
|
252
|
+
status: 200,
|
|
253
|
+
headers,
|
|
254
|
+
body: '',
|
|
255
|
+
stream: generator,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
try {
|
|
259
|
+
const result = await opts.runAgent(prompt, mode, pc);
|
|
260
|
+
return jsonStatus(200, { ok: true, result });
|
|
261
|
+
}
|
|
262
|
+
catch (e) {
|
|
263
|
+
return jsonStatus(500, { error: e instanceof Error ? e.message : String(e) });
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
// ── GET /__vesk/agent/history ───────────────────────────────────────
|
|
267
|
+
if (pathname === '/__vesk/agent/history' || pathname === '/__vesk/agent/checkpoints') {
|
|
268
|
+
if (method !== 'GET' && method !== 'HEAD')
|
|
269
|
+
return jsonStatus(405, { error: 'method not allowed' });
|
|
270
|
+
if (!table || typeof table.allows !== 'function') {
|
|
271
|
+
return denied('readFiles');
|
|
272
|
+
}
|
|
273
|
+
try {
|
|
274
|
+
if (!table.allows('readFiles')) {
|
|
275
|
+
return denied('readFiles');
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
catch {
|
|
279
|
+
return denied('readFiles');
|
|
280
|
+
}
|
|
281
|
+
try {
|
|
282
|
+
let checkpoints = null;
|
|
283
|
+
if (typeof opts.listCheckpoints === 'function') {
|
|
284
|
+
const injected = opts.listCheckpoints;
|
|
285
|
+
let res;
|
|
286
|
+
try {
|
|
287
|
+
if (injected.length === 0) {
|
|
288
|
+
res = await injected();
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
try {
|
|
292
|
+
res = await injected(projectDir);
|
|
293
|
+
if (!Array.isArray(res)) {
|
|
294
|
+
const alt = await injected();
|
|
295
|
+
if (Array.isArray(alt))
|
|
296
|
+
res = alt;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
catch {
|
|
300
|
+
res = await injected();
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
catch {
|
|
305
|
+
res = fallbackManager.listNewestFirst();
|
|
306
|
+
}
|
|
307
|
+
if (Array.isArray(res))
|
|
308
|
+
checkpoints = res;
|
|
309
|
+
else if (res == null)
|
|
310
|
+
checkpoints = [];
|
|
311
|
+
else
|
|
312
|
+
checkpoints = fallbackManager.listNewestFirst();
|
|
313
|
+
}
|
|
314
|
+
else {
|
|
315
|
+
checkpoints = fallbackManager.listNewestFirst();
|
|
316
|
+
}
|
|
317
|
+
if (!Array.isArray(checkpoints))
|
|
318
|
+
checkpoints = [];
|
|
319
|
+
return jsonStatus(200, { checkpoints, history: checkpoints });
|
|
320
|
+
}
|
|
321
|
+
catch (e) {
|
|
322
|
+
return jsonStatus(500, { error: e instanceof Error ? e.message : String(e) });
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
// ── POST /__vesk/agent/checkpoint ───────────────────────────────────
|
|
326
|
+
if (pathname === '/__vesk/agent/checkpoint') {
|
|
327
|
+
if (method !== 'POST')
|
|
328
|
+
return jsonStatus(405, { error: 'method not allowed' });
|
|
329
|
+
if (!table || typeof table.allows !== 'function') {
|
|
330
|
+
return denied('createCheckpoint');
|
|
331
|
+
}
|
|
332
|
+
try {
|
|
333
|
+
if (!table.allows('createCheckpoint')) {
|
|
334
|
+
return denied('createCheckpoint');
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
catch {
|
|
338
|
+
return denied('createCheckpoint');
|
|
339
|
+
}
|
|
340
|
+
const b = (body ?? {});
|
|
341
|
+
const message = typeof b.message === 'string'
|
|
342
|
+
? b.message
|
|
343
|
+
: typeof b.msg === 'string'
|
|
344
|
+
? b.msg
|
|
345
|
+
: typeof b.prompt === 'string'
|
|
346
|
+
? b.prompt.slice(0, 200)
|
|
347
|
+
: 'checkpoint';
|
|
348
|
+
const filesRaw = Array.isArray(b.files) ? b.files : undefined;
|
|
349
|
+
const commandsRaw = Array.isArray(b.commands) ? b.commands : undefined;
|
|
350
|
+
const depsRaw = b.deps;
|
|
351
|
+
const prompt = typeof b.prompt === 'string' ? b.prompt : undefined;
|
|
352
|
+
// Normalize to CheckpointManager shapes: files = CheckpointFile[], commands = string[][], deps = {installed,removed}
|
|
353
|
+
const files = filesRaw;
|
|
354
|
+
let commands;
|
|
355
|
+
if (commandsRaw) {
|
|
356
|
+
// commands may be string[][] or array of {argv:string[]} objects
|
|
357
|
+
if (commandsRaw.length > 0 && typeof commandsRaw[0] === 'object' && !Array.isArray(commandsRaw[0])) {
|
|
358
|
+
const cmdObjs = commandsRaw;
|
|
359
|
+
commands = cmdObjs.map((c) => (Array.isArray(c.argv) ? c.argv : []));
|
|
360
|
+
}
|
|
361
|
+
else {
|
|
362
|
+
commands = commandsRaw;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
let deps;
|
|
366
|
+
if (Array.isArray(depsRaw)) {
|
|
367
|
+
deps = { installed: depsRaw, removed: [] };
|
|
368
|
+
}
|
|
369
|
+
else if (depsRaw && typeof depsRaw === 'object') {
|
|
370
|
+
const d = depsRaw;
|
|
371
|
+
if (Array.isArray(d.installed) || Array.isArray(d.removed)) {
|
|
372
|
+
deps = {
|
|
373
|
+
installed: Array.isArray(d.installed) ? d.installed : [],
|
|
374
|
+
removed: Array.isArray(d.removed) ? d.removed : [],
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
const buildRaw = b.build ?? b.buildResult;
|
|
379
|
+
const testRaw = b.test ?? b.testResult;
|
|
380
|
+
let build = null;
|
|
381
|
+
if (buildRaw && typeof buildRaw === 'object') {
|
|
382
|
+
const br = buildRaw;
|
|
383
|
+
if (typeof br.ok === 'boolean')
|
|
384
|
+
build = { ok: br.ok, error: typeof br.error === 'string' ? br.error : undefined, ms: typeof br.ms === 'number' ? br.ms : undefined };
|
|
385
|
+
else
|
|
386
|
+
build = null;
|
|
387
|
+
}
|
|
388
|
+
let test = null;
|
|
389
|
+
if (testRaw && typeof testRaw === 'object') {
|
|
390
|
+
const tr = testRaw;
|
|
391
|
+
if (typeof tr.ok === 'boolean')
|
|
392
|
+
test = { ok: tr.ok, error: typeof tr.error === 'string' ? tr.error : undefined };
|
|
393
|
+
}
|
|
394
|
+
try {
|
|
395
|
+
let cp = null;
|
|
396
|
+
const injectedCreate = opts.createCheckpoint;
|
|
397
|
+
if (typeof injectedCreate === 'function') {
|
|
398
|
+
const fn = injectedCreate;
|
|
399
|
+
let res;
|
|
400
|
+
try {
|
|
401
|
+
if (fn.length <= 3) {
|
|
402
|
+
// Try (message, changes, buildResult) and (opts) shapes.
|
|
403
|
+
// First try CreateCheckpointOptions shape: single object
|
|
404
|
+
if (fn.length === 1) {
|
|
405
|
+
const createOpts = { label: message, prompt, files, commands, deps, build, test };
|
|
406
|
+
res = await fn(createOpts);
|
|
407
|
+
}
|
|
408
|
+
else {
|
|
409
|
+
const changes = {};
|
|
410
|
+
if (files)
|
|
411
|
+
changes.files = files;
|
|
412
|
+
if (commands)
|
|
413
|
+
changes.commands = commands;
|
|
414
|
+
if (deps)
|
|
415
|
+
changes.deps = deps;
|
|
416
|
+
if (prompt)
|
|
417
|
+
changes.prompt = prompt;
|
|
418
|
+
res = await fn(message, changes, buildRaw ?? testRaw);
|
|
419
|
+
}
|
|
420
|
+
if (res && typeof res === 'object' && 'id' in res) {
|
|
421
|
+
cp = res;
|
|
422
|
+
}
|
|
423
|
+
else {
|
|
424
|
+
// Try projectDir form
|
|
425
|
+
res = await fn(projectDir, message, { files, commands, deps, prompt }, buildRaw);
|
|
426
|
+
if (res && typeof res === 'object' && 'id' in res) {
|
|
427
|
+
cp = res;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
else {
|
|
432
|
+
res = await fn(projectDir, message, { files, commands, deps, prompt }, buildRaw);
|
|
433
|
+
if (res && typeof res === 'object' && 'id' in res) {
|
|
434
|
+
cp = res;
|
|
435
|
+
}
|
|
436
|
+
else {
|
|
437
|
+
const alt = await fn({ label: message, prompt, files, commands, deps, build, test });
|
|
438
|
+
if (alt && typeof alt === 'object' && 'id' in alt)
|
|
439
|
+
cp = alt;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
catch {
|
|
444
|
+
try {
|
|
445
|
+
const alt = await fn(projectDir, message, { files, commands, deps, prompt }, buildRaw);
|
|
446
|
+
if (alt && typeof alt === 'object' && 'id' in alt)
|
|
447
|
+
cp = alt;
|
|
448
|
+
}
|
|
449
|
+
catch {
|
|
450
|
+
cp = null;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
if (!cp || typeof cp.id !== 'string') {
|
|
454
|
+
cp = fallbackManager.create({ label: message, prompt, files: files, commands, deps, build, test });
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
else {
|
|
458
|
+
cp = fallbackManager.create({ label: message, prompt, files: files, commands, deps, build, test });
|
|
459
|
+
}
|
|
460
|
+
return jsonStatus(200, { ok: true, checkpoint: cp });
|
|
461
|
+
}
|
|
462
|
+
catch (e) {
|
|
463
|
+
return jsonStatus(500, { error: e instanceof Error ? e.message : String(e) });
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
// ── POST /__vesk/agent/rollback ─────────────────────────────────────
|
|
467
|
+
if (pathname === '/__vesk/agent/rollback') {
|
|
468
|
+
if (method !== 'POST')
|
|
469
|
+
return jsonStatus(405, { error: 'method not allowed' });
|
|
470
|
+
if (!table || typeof table.allows !== 'function') {
|
|
471
|
+
return denied('rollback');
|
|
472
|
+
}
|
|
473
|
+
try {
|
|
474
|
+
if (!table.allows('rollback')) {
|
|
475
|
+
return denied('rollback');
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
catch {
|
|
479
|
+
return denied('rollback');
|
|
480
|
+
}
|
|
481
|
+
const b = (body ?? {});
|
|
482
|
+
const id = typeof b.checkpointId === 'string'
|
|
483
|
+
? b.checkpointId
|
|
484
|
+
: typeof b.id === 'string'
|
|
485
|
+
? b.id
|
|
486
|
+
: typeof b.checkpoint_id === 'string'
|
|
487
|
+
? b.checkpoint_id
|
|
488
|
+
: '';
|
|
489
|
+
if (!id || !id.trim())
|
|
490
|
+
return badRequest('missing "checkpointId" in body');
|
|
491
|
+
if (id.includes('/') || id.includes('\\') || id.includes('..')) {
|
|
492
|
+
return badRequest('invalid checkpointId');
|
|
493
|
+
}
|
|
494
|
+
try {
|
|
495
|
+
let cp = null;
|
|
496
|
+
if (typeof opts.rollback === 'function') {
|
|
497
|
+
const fn = opts.rollback;
|
|
498
|
+
let res;
|
|
499
|
+
try {
|
|
500
|
+
if (fn.length <= 1) {
|
|
501
|
+
res = await fn(id);
|
|
502
|
+
if (res && typeof res === 'object' && 'id' in res) {
|
|
503
|
+
cp = res;
|
|
504
|
+
}
|
|
505
|
+
else if (res && typeof res === 'object' && 'checkpoint' in res) {
|
|
506
|
+
// manager.rollback returns {checkpoint, filesToRestore}
|
|
507
|
+
const r = res;
|
|
508
|
+
if (r.checkpoint && typeof r.checkpoint === 'object' && 'id' in r.checkpoint) {
|
|
509
|
+
cp = r.checkpoint;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
else if (res == null) {
|
|
513
|
+
try {
|
|
514
|
+
const alt = await fn(projectDir, id);
|
|
515
|
+
if (alt && typeof alt === 'object' && 'id' in alt)
|
|
516
|
+
cp = alt;
|
|
517
|
+
else if (alt && typeof alt === 'object' && 'checkpoint' in alt) {
|
|
518
|
+
const r = alt;
|
|
519
|
+
if (r.checkpoint && typeof r.checkpoint === 'object')
|
|
520
|
+
cp = r.checkpoint;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
catch {
|
|
524
|
+
cp = null;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
else {
|
|
529
|
+
res = await fn(projectDir, id);
|
|
530
|
+
if (res && typeof res === 'object' && 'id' in res) {
|
|
531
|
+
cp = res;
|
|
532
|
+
}
|
|
533
|
+
else if (res && typeof res === 'object' && 'checkpoint' in res) {
|
|
534
|
+
const r = res;
|
|
535
|
+
if (r.checkpoint && typeof r.checkpoint === 'object')
|
|
536
|
+
cp = r.checkpoint;
|
|
537
|
+
}
|
|
538
|
+
else if (res == null) {
|
|
539
|
+
try {
|
|
540
|
+
const alt = await fn(id);
|
|
541
|
+
if (alt && typeof alt === 'object' && 'id' in alt)
|
|
542
|
+
cp = alt;
|
|
543
|
+
}
|
|
544
|
+
catch { }
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
catch {
|
|
549
|
+
try {
|
|
550
|
+
const alt = await fn(projectDir, id);
|
|
551
|
+
if (alt && typeof alt === 'object' && 'id' in alt)
|
|
552
|
+
cp = alt;
|
|
553
|
+
else if (alt && typeof alt === 'object' && 'checkpoint' in alt) {
|
|
554
|
+
const r = alt;
|
|
555
|
+
if (r.checkpoint)
|
|
556
|
+
cp = r.checkpoint;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
catch { }
|
|
560
|
+
}
|
|
561
|
+
if (cp == null && res === undefined) {
|
|
562
|
+
const found = fallbackManager.get(id);
|
|
563
|
+
if (found)
|
|
564
|
+
cp = found;
|
|
565
|
+
else {
|
|
566
|
+
const rb = fallbackManager.rollback(id);
|
|
567
|
+
if (rb)
|
|
568
|
+
cp = rb.checkpoint;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
else {
|
|
573
|
+
const found = fallbackManager.get(id);
|
|
574
|
+
if (found)
|
|
575
|
+
cp = found;
|
|
576
|
+
else {
|
|
577
|
+
const rb = fallbackManager.rollback(id);
|
|
578
|
+
if (rb)
|
|
579
|
+
cp = rb.checkpoint;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
if (cp == null && typeof opts.rollback === 'function') {
|
|
583
|
+
const found = fallbackManager.get(id);
|
|
584
|
+
if (found)
|
|
585
|
+
cp = found;
|
|
586
|
+
else {
|
|
587
|
+
const rb = fallbackManager.rollback(id);
|
|
588
|
+
if (rb)
|
|
589
|
+
cp = rb.checkpoint;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
if (!cp)
|
|
593
|
+
return jsonStatus(404, { error: `checkpoint not found: ${id}` });
|
|
594
|
+
return jsonStatus(200, { ok: true, checkpoint: cp, rolledBack: true });
|
|
595
|
+
}
|
|
596
|
+
catch (e) {
|
|
597
|
+
return jsonStatus(500, { error: e instanceof Error ? e.message : String(e) });
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
// ── GET /__vesk/agent/models?provider= ———— list models for provider
|
|
601
|
+
if (pathname === '/__vesk/agent/models') {
|
|
602
|
+
if (method !== 'GET' && method !== 'HEAD')
|
|
603
|
+
return jsonStatus(405, { error: 'method not allowed' });
|
|
604
|
+
if (!table || typeof table.allows !== 'function')
|
|
605
|
+
return denied('readFiles');
|
|
606
|
+
try {
|
|
607
|
+
if (!table.allows('readFiles'))
|
|
608
|
+
return denied('readFiles');
|
|
609
|
+
}
|
|
610
|
+
catch {
|
|
611
|
+
return denied('readFiles');
|
|
612
|
+
}
|
|
613
|
+
const params = new URLSearchParams(_search ?? '');
|
|
614
|
+
const provider = (params.get('provider') || params.get('p') || loadAgenticConfig(projectDir).provider);
|
|
615
|
+
const cfg = loadAgenticConfig(projectDir);
|
|
616
|
+
// per-provider key: try provider-specific first, fallback generic
|
|
617
|
+
let apiKey = '';
|
|
618
|
+
try {
|
|
619
|
+
apiKey = getApiKey(projectDir, provider) || getApiKey(projectDir) || '';
|
|
620
|
+
}
|
|
621
|
+
catch {
|
|
622
|
+
apiKey = getApiKey(projectDir) || '';
|
|
623
|
+
}
|
|
624
|
+
const baseUrl = params.get('baseUrl') || cfg.baseUrl;
|
|
625
|
+
let models = [];
|
|
626
|
+
try {
|
|
627
|
+
if (provider === 'openai' || provider === 'opencode' || provider === 'opencode-go' || provider === 'openrouter' || provider === 'loopers' || provider === 'custom') {
|
|
628
|
+
let effectiveBase = baseUrl;
|
|
629
|
+
if (!effectiveBase) {
|
|
630
|
+
if (provider === 'opencode')
|
|
631
|
+
effectiveBase = 'https://opencode.ai/zen/v1';
|
|
632
|
+
else if (provider === 'opencode-go')
|
|
633
|
+
effectiveBase = 'https://opencode.ai/zen/go/v1';
|
|
634
|
+
else if (provider === 'openrouter')
|
|
635
|
+
effectiveBase = 'https://openrouter.ai/api/v1';
|
|
636
|
+
else if (provider === 'loopers')
|
|
637
|
+
effectiveBase = 'http://localhost:8080';
|
|
638
|
+
}
|
|
639
|
+
const p = openAiProvider({ apiKey, baseUrl: effectiveBase });
|
|
640
|
+
models = p.listModels ? await p.listModels({ apiKey, baseUrl: effectiveBase }) : [];
|
|
641
|
+
}
|
|
642
|
+
else if (provider === 'anthropic') {
|
|
643
|
+
const p = anthropicProvider({ apiKey, baseUrl });
|
|
644
|
+
models = p.listModels ? await p.listModels({ apiKey, baseUrl }) : [];
|
|
645
|
+
}
|
|
646
|
+
else if (provider === 'google') {
|
|
647
|
+
const p = googleProvider({ apiKey, baseUrl });
|
|
648
|
+
models = p.listModels ? await p.listModels({ apiKey, baseUrl }) : [];
|
|
649
|
+
}
|
|
650
|
+
else if (provider === 'ollama') {
|
|
651
|
+
const p = ollamaProvider({ baseUrl });
|
|
652
|
+
models = p.listModels ? await p.listModels({ baseUrl }) : [];
|
|
653
|
+
}
|
|
654
|
+
else {
|
|
655
|
+
return badRequest(`unknown provider: ${provider}`);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
catch (e) {
|
|
659
|
+
return jsonStatus(500, { error: e instanceof Error ? e.message : String(e) });
|
|
660
|
+
}
|
|
661
|
+
return jsonStatus(200, { provider, models });
|
|
662
|
+
}
|
|
663
|
+
// ── GET /__vesk/agent/keys + POST /__vesk/agent/keys ───────────────
|
|
664
|
+
if (pathname === '/__vesk/agent/keys') {
|
|
665
|
+
if (method === 'GET' || method === 'HEAD') {
|
|
666
|
+
if (!table || typeof table.allows !== 'function')
|
|
667
|
+
return denied('readFiles');
|
|
668
|
+
try {
|
|
669
|
+
if (!table.allows('readFiles'))
|
|
670
|
+
return denied('readFiles');
|
|
671
|
+
}
|
|
672
|
+
catch {
|
|
673
|
+
return denied('readFiles');
|
|
674
|
+
}
|
|
675
|
+
const keys = buildKeysMap(projectDir);
|
|
676
|
+
const keyPreviews = buildPreviewsMap(projectDir);
|
|
677
|
+
// compatibility aliases: previews, hasKeys, keyPreview alias already handled
|
|
678
|
+
// never echo raw keys — only hasKey booleans + masked previews
|
|
679
|
+
return jsonStatus(200, {
|
|
680
|
+
keys,
|
|
681
|
+
keyPreviews,
|
|
682
|
+
previews: keyPreviews,
|
|
683
|
+
hasKeys: keys,
|
|
684
|
+
keyPreview: keyPreviews,
|
|
685
|
+
// also include top-level provider-agnostic hint matching old config shape
|
|
686
|
+
hasKey: Object.values(keys).some(Boolean),
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
if (method === 'POST') {
|
|
690
|
+
// Setting your own API key is allowed in any mode (readFiles) — it's not a project write
|
|
691
|
+
if (!table || typeof table.allows !== 'function')
|
|
692
|
+
return denied('readFiles');
|
|
693
|
+
try {
|
|
694
|
+
if (!table.allows('readFiles'))
|
|
695
|
+
return denied('readFiles');
|
|
696
|
+
}
|
|
697
|
+
catch {
|
|
698
|
+
return denied('readFiles');
|
|
699
|
+
}
|
|
700
|
+
const b = (body ?? {});
|
|
701
|
+
// accept {provider, apiKey} or {provider, key} or {provider, value}
|
|
702
|
+
const providerRaw = typeof b.provider === 'string' ? b.provider : typeof b.p === 'string' ? b.p : typeof b.name === 'string' ? b.name : '';
|
|
703
|
+
const provider = providerRaw ? normalizeProvider(providerRaw) : '';
|
|
704
|
+
if (!provider)
|
|
705
|
+
return badRequest('missing "provider" in body');
|
|
706
|
+
if (!isValidProvider(provider))
|
|
707
|
+
return badRequest(`unknown provider: ${providerRaw}`);
|
|
708
|
+
// apiKey may be under apiKey | key | value | token
|
|
709
|
+
const apiKeyRaw = typeof b.apiKey === 'string' ? b.apiKey : typeof b.key === 'string' ? b.key : typeof b.value === 'string' ? b.value : typeof b.token === 'string' ? b.token : typeof b.api_key === 'string' ? b.api_key : '';
|
|
710
|
+
if (!apiKeyRaw || !String(apiKeyRaw).trim())
|
|
711
|
+
return badRequest('missing "apiKey" in body');
|
|
712
|
+
const apiKey = String(apiKeyRaw).trim();
|
|
713
|
+
// write via saveApiKey — 3-arg per-provider form (projectDir, provider, apiKey)
|
|
714
|
+
try {
|
|
715
|
+
// config's saveApiKey supports (projectDir, provider, apiKey) when 3 args
|
|
716
|
+
// fallback to 2-arg generic if provider is 'openai' for backwards compat? Keep per-provider always.
|
|
717
|
+
// Detect arity: if saveApiKey length >=3, call 3-arg; else call 2-arg generic + per-file
|
|
718
|
+
// Our config now supports 3-arg, so use it.
|
|
719
|
+
const fn = saveApiKey;
|
|
720
|
+
if (fn.length >= 3) {
|
|
721
|
+
fn(projectDir, provider, apiKey);
|
|
722
|
+
}
|
|
723
|
+
else {
|
|
724
|
+
// old signature — still try per-provider file via direct call
|
|
725
|
+
saveApiKey(projectDir, apiKey);
|
|
726
|
+
// also try 3-arg for new config that may be loaded
|
|
727
|
+
try {
|
|
728
|
+
saveApiKey(projectDir, provider, apiKey);
|
|
729
|
+
}
|
|
730
|
+
catch { }
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
catch (e) {
|
|
734
|
+
return jsonStatus(500, { error: e instanceof Error ? e.message : String(e) });
|
|
735
|
+
}
|
|
736
|
+
const preview = maskPreview(apiKey);
|
|
737
|
+
// return ok + hasKey + preview — never raw key
|
|
738
|
+
return jsonStatus(200, {
|
|
739
|
+
ok: true,
|
|
740
|
+
provider,
|
|
741
|
+
hasKey: true,
|
|
742
|
+
preview,
|
|
743
|
+
keyPreview: preview,
|
|
744
|
+
masked: preview,
|
|
745
|
+
keys: buildKeysMap(projectDir),
|
|
746
|
+
keyPreviews: buildPreviewsMap(projectDir),
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
return jsonStatus(405, { error: 'method not allowed' });
|
|
750
|
+
}
|
|
751
|
+
// ── GET /__vesk/agent/config + POST /__vesk/agent/config ──
|
|
752
|
+
if (pathname === '/__vesk/agent/config') {
|
|
753
|
+
if (method === 'GET' || method === 'HEAD') {
|
|
754
|
+
if (!table || typeof table.allows !== 'function')
|
|
755
|
+
return denied('readFiles');
|
|
756
|
+
try {
|
|
757
|
+
if (!table.allows('readFiles'))
|
|
758
|
+
return denied('readFiles');
|
|
759
|
+
}
|
|
760
|
+
catch {
|
|
761
|
+
return denied('readFiles');
|
|
762
|
+
}
|
|
763
|
+
const cfg = loadAgenticConfig(projectDir);
|
|
764
|
+
// never leak full key — only masked
|
|
765
|
+
const key = getApiKey(projectDir);
|
|
766
|
+
const masked = maskPreview(key);
|
|
767
|
+
const keys = buildKeysMap(projectDir);
|
|
768
|
+
const keyPreviews = buildPreviewsMap(projectDir);
|
|
769
|
+
return jsonStatus(200, {
|
|
770
|
+
provider: cfg.provider,
|
|
771
|
+
model: cfg.model,
|
|
772
|
+
baseUrl: cfg.baseUrl ?? null,
|
|
773
|
+
mode: cfg.mode,
|
|
774
|
+
maxSteps: cfg.maxSteps,
|
|
775
|
+
hasKey: cfg.hasKey,
|
|
776
|
+
keyPreview: masked,
|
|
777
|
+
// per-provider extensions — never raw keys
|
|
778
|
+
keys,
|
|
779
|
+
hasKeys: keys,
|
|
780
|
+
keyPreviews,
|
|
781
|
+
previews: keyPreviews,
|
|
782
|
+
hasKeyMap: keys,
|
|
783
|
+
previewsMap: keyPreviews,
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
if (method === 'POST') {
|
|
787
|
+
const b = (body ?? {});
|
|
788
|
+
// If body is only apiKey(s), allow with readFiles (setting your own key shouldn't require modifyConfig)
|
|
789
|
+
const isOnlyKeys = (typeof b.apiKey === 'string' && Object.keys(b).every(k => ['apiKey', 'provider', 'apiKeys', 'keys', 'keyMap'].includes(k))) || (b.apiKeys && typeof b.apiKeys === 'object');
|
|
790
|
+
let neededCap = 'modifyConfig';
|
|
791
|
+
if (isOnlyKeys)
|
|
792
|
+
neededCap = 'readFiles';
|
|
793
|
+
if (!table || typeof table.allows !== 'function')
|
|
794
|
+
return denied(neededCap);
|
|
795
|
+
try {
|
|
796
|
+
if (!table.allows(neededCap))
|
|
797
|
+
return denied(neededCap);
|
|
798
|
+
}
|
|
799
|
+
catch {
|
|
800
|
+
return denied(neededCap);
|
|
801
|
+
}
|
|
802
|
+
const patch = {};
|
|
803
|
+
if (typeof b.provider === 'string')
|
|
804
|
+
patch.provider = b.provider;
|
|
805
|
+
if (typeof b.model === 'string')
|
|
806
|
+
patch.model = b.model;
|
|
807
|
+
if (typeof b.baseUrl === 'string')
|
|
808
|
+
patch.baseUrl = b.baseUrl;
|
|
809
|
+
if (typeof b.mode === 'string' && ['explore', 'debug', 'agent'].includes(b.mode))
|
|
810
|
+
patch.mode = b.mode;
|
|
811
|
+
if (typeof b.maxSteps === 'number')
|
|
812
|
+
patch.maxSteps = b.maxSteps;
|
|
813
|
+
// backwards compat: single apiKey
|
|
814
|
+
if (typeof b.apiKey === 'string' && b.apiKey.trim()) {
|
|
815
|
+
// For backwards compat, save as generic + also as per-current/new provider if known
|
|
816
|
+
const targetProvider = typeof b.provider === 'string' && b.provider.trim() ? normalizeProvider(b.provider) : loadAgenticConfig(projectDir).provider;
|
|
817
|
+
try {
|
|
818
|
+
// save generic legacy
|
|
819
|
+
saveApiKey(projectDir, b.apiKey.trim());
|
|
820
|
+
}
|
|
821
|
+
catch { }
|
|
822
|
+
// also save per-provider for future GET keys consistency
|
|
823
|
+
try {
|
|
824
|
+
const fn = saveApiKey;
|
|
825
|
+
if (fn.length >= 3 && isValidProvider(targetProvider)) {
|
|
826
|
+
fn(projectDir, targetProvider, b.apiKey.trim());
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
catch { }
|
|
830
|
+
}
|
|
831
|
+
// per-provider map: {apiKeys:{openai:"sk-...", anthropic:"..."}}
|
|
832
|
+
const apiKeysRaw = b.apiKeys ?? b.keys ?? b.keyMap;
|
|
833
|
+
if (apiKeysRaw && typeof apiKeysRaw === 'object' && !Array.isArray(apiKeysRaw)) {
|
|
834
|
+
const map = apiKeysRaw;
|
|
835
|
+
for (const [provRaw, val] of Object.entries(map)) {
|
|
836
|
+
if (typeof val !== 'string' || !val.trim())
|
|
837
|
+
continue;
|
|
838
|
+
const prov = normalizeProvider(provRaw);
|
|
839
|
+
if (!isValidProvider(prov))
|
|
840
|
+
continue;
|
|
841
|
+
const keyVal = String(val).trim();
|
|
842
|
+
if (!keyVal)
|
|
843
|
+
continue;
|
|
844
|
+
try {
|
|
845
|
+
const fn = saveApiKey;
|
|
846
|
+
if (fn.length >= 3)
|
|
847
|
+
fn(projectDir, prov, keyVal);
|
|
848
|
+
else {
|
|
849
|
+
// old 2-arg — at least save generic if provider is current, otherwise try 3-arg anyway
|
|
850
|
+
try {
|
|
851
|
+
saveApiKey(projectDir, prov, keyVal);
|
|
852
|
+
}
|
|
853
|
+
catch { }
|
|
854
|
+
if (prov === normalizeProvider(loadAgenticConfig(projectDir).provider)) {
|
|
855
|
+
saveApiKey(projectDir, keyVal);
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
catch { }
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
// also handle alternative shape: {openaiKey, anthropicKey, ...} or {keys:{...}} already handled
|
|
863
|
+
// also handle direct per-provider fields like b["openai_apiKey"]? Not needed, but be lenient
|
|
864
|
+
// Check for apiKey per provider via body[provider] keys? Skip
|
|
865
|
+
const next = saveAgenticConfig(projectDir, patch);
|
|
866
|
+
// build updated per-provider maps for response (never raw keys)
|
|
867
|
+
const keys = buildKeysMap(projectDir);
|
|
868
|
+
const keyPreviews = buildPreviewsMap(projectDir);
|
|
869
|
+
// masked preview for the current/single key for backward compat
|
|
870
|
+
const curKey = getApiKey(projectDir);
|
|
871
|
+
const curMasked = maskPreview(curKey);
|
|
872
|
+
return jsonStatus(200, {
|
|
873
|
+
ok: true,
|
|
874
|
+
provider: next.provider,
|
|
875
|
+
model: next.model,
|
|
876
|
+
mode: next.mode,
|
|
877
|
+
hasKey: Object.values(keys).some(Boolean) || !!curKey,
|
|
878
|
+
keyPreview: curMasked,
|
|
879
|
+
keys,
|
|
880
|
+
hasKeys: keys,
|
|
881
|
+
keyPreviews,
|
|
882
|
+
previews: keyPreviews,
|
|
883
|
+
});
|
|
884
|
+
}
|
|
885
|
+
return jsonStatus(405, { error: 'method not allowed' });
|
|
886
|
+
}
|
|
887
|
+
// ── GET /__vesk/agent/tools ──
|
|
888
|
+
if (pathname === '/__vesk/agent/tools') {
|
|
889
|
+
if (method !== 'GET' && method !== 'HEAD')
|
|
890
|
+
return jsonStatus(405, { error: 'method not allowed' });
|
|
891
|
+
if (!table || typeof table.allows !== 'function')
|
|
892
|
+
return denied('readFiles');
|
|
893
|
+
try {
|
|
894
|
+
if (!table.allows('readFiles'))
|
|
895
|
+
return denied('readFiles');
|
|
896
|
+
}
|
|
897
|
+
catch {
|
|
898
|
+
return denied('readFiles');
|
|
899
|
+
}
|
|
900
|
+
// Filter tools by permissions: expose what current mode allows
|
|
901
|
+
// We don't have full tool list here without deps, so return capability map + known tool names
|
|
902
|
+
const allTools = [
|
|
903
|
+
{ name: 'vesk.inspectProject', description: 'inspect project structure', capability: 'readFiles' },
|
|
904
|
+
{ name: 'vesk.inspectComponent', description: 'read component source', capability: 'readFiles' },
|
|
905
|
+
{ name: 'vesk.readConfig', description: 'read vesk.config.ts', capability: 'readFiles' },
|
|
906
|
+
{ name: 'vesk.updateConfig', description: 'update config', capability: 'modifyConfig' },
|
|
907
|
+
{ name: 'vesk.getDiagnostics', description: 'get diagnostics', capability: 'readFiles' },
|
|
908
|
+
{ name: 'vesk.runBuild', description: 'run build', capability: 'runBuild' },
|
|
909
|
+
{ name: 'vesk.runTests', description: 'run tests', capability: 'runTests' },
|
|
910
|
+
{ name: 'vesk.installPlugin', description: 'install plugin', capability: 'installPackages' },
|
|
911
|
+
{ name: 'vesk.enablePlugin', description: 'enable plugin', capability: 'managePlugins' },
|
|
912
|
+
{ name: 'filesystem.read', description: 'read file', capability: 'readFiles' },
|
|
913
|
+
{ name: 'filesystem.write', description: 'write file', capability: 'writeFiles' },
|
|
914
|
+
{ name: 'filesystem.delete', description: 'delete file', capability: 'deleteFiles' },
|
|
915
|
+
{ name: 'command.execute', description: 'execute allowlisted command', capability: 'executeCommands' },
|
|
916
|
+
{ name: 'vesk.createCheckpoint', description: 'create checkpoint', capability: 'createCheckpoint' },
|
|
917
|
+
{ name: 'vesk.rollback', description: 'rollback', capability: 'rollback' },
|
|
918
|
+
];
|
|
919
|
+
const allowed = allTools.filter(t => {
|
|
920
|
+
try {
|
|
921
|
+
return table.allows(t.capability);
|
|
922
|
+
}
|
|
923
|
+
catch {
|
|
924
|
+
return false;
|
|
925
|
+
}
|
|
926
|
+
});
|
|
927
|
+
return jsonStatus(200, { tools: allowed, allTools });
|
|
928
|
+
}
|
|
929
|
+
// ── GET /__vesk/agent/commands ──
|
|
930
|
+
if (pathname === '/__vesk/agent/commands') {
|
|
931
|
+
if (method !== 'GET' && method !== 'HEAD')
|
|
932
|
+
return jsonStatus(405, { error: 'method not allowed' });
|
|
933
|
+
return jsonStatus(200, { commands: SLASH_COMMANDS });
|
|
934
|
+
}
|
|
935
|
+
// ── Unknown /__vesk/agent/* subpath → 404 ────────────────────────────
|
|
936
|
+
if (pathname.startsWith('/__vesk/agent/')) {
|
|
937
|
+
return jsonStatus(404, { error: 'Not found' });
|
|
938
|
+
}
|
|
939
|
+
return null;
|
|
940
|
+
}
|
|
941
|
+
return { route };
|
|
942
|
+
}
|