viagen 0.0.57 → 0.1.3
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/dist/cli.js +27 -9
- package/dist/index.d.ts +2 -0
- package/dist/index.js +577 -97
- package/package.json +3 -3
- package/LICENSE +0 -21
package/dist/cli.js
CHANGED
|
@@ -103,6 +103,11 @@ async function waitForServer(baseUrl, token, devServer) {
|
|
|
103
103
|
async function deploySandbox(opts) {
|
|
104
104
|
const token = randomUUID();
|
|
105
105
|
const useGit = !!opts.git;
|
|
106
|
+
const rootDir = opts.rootDir ?? null;
|
|
107
|
+
const rootPrefix = rootDir ? `${rootDir}/` : "";
|
|
108
|
+
if (rootDir) {
|
|
109
|
+
console.log(` Monorepo root dir: ${rootDir}`);
|
|
110
|
+
}
|
|
106
111
|
const timeoutMs = (opts.timeoutMinutes ?? 30) * 60 * 1e3;
|
|
107
112
|
const sourceOpts = opts.git ? {
|
|
108
113
|
source: {
|
|
@@ -219,21 +224,23 @@ async function deploySandbox(opts) {
|
|
|
219
224
|
const envLines = Object.entries(envMap).map(([k, v]) => `${k}=${v}`);
|
|
220
225
|
await sandbox2.writeFiles([
|
|
221
226
|
{
|
|
222
|
-
path:
|
|
227
|
+
path: `${rootPrefix}.env`,
|
|
223
228
|
content: Buffer.from(envLines.join("\n"))
|
|
224
229
|
}
|
|
225
230
|
]);
|
|
226
231
|
const dots = startDots(" Installing dependencies");
|
|
227
|
-
const
|
|
232
|
+
const installCmd = rootDir ? `cd ${rootDir} && npm install` : "npm install";
|
|
233
|
+
const install = await sandbox2.runCommand("bash", ["-c", installCmd]);
|
|
228
234
|
dots.stop();
|
|
229
235
|
if (install.exitCode !== 0) {
|
|
230
236
|
const stderr = await install.stderr();
|
|
231
237
|
console.error(stderr);
|
|
232
238
|
throw new Error(`npm install failed (exit ${install.exitCode})`);
|
|
233
239
|
}
|
|
240
|
+
const devCmd = rootDir ? `cd ${rootDir} && npm run dev` : "npm run dev";
|
|
234
241
|
const devServer = await sandbox2.runCommand({
|
|
235
|
-
cmd: "
|
|
236
|
-
args: ["
|
|
242
|
+
cmd: "bash",
|
|
243
|
+
args: ["-c", devCmd],
|
|
237
244
|
detached: true
|
|
238
245
|
});
|
|
239
246
|
const baseUrl = sandbox2.domain(5173);
|
|
@@ -252,7 +259,9 @@ async function deploySandbox(opts) {
|
|
|
252
259
|
sandboxId: sandbox2.sandboxId,
|
|
253
260
|
mode: useGit ? "git" : "upload",
|
|
254
261
|
streamLogs: (opts2) => devServer.logs(opts2),
|
|
255
|
-
stop: () =>
|
|
262
|
+
stop: async () => {
|
|
263
|
+
await sandbox2.stop();
|
|
264
|
+
}
|
|
256
265
|
};
|
|
257
266
|
} catch (err) {
|
|
258
267
|
await sandbox2.stop().catch(() => {
|
|
@@ -970,8 +979,13 @@ async function sandbox(args, options) {
|
|
|
970
979
|
branch: envBranch || "main",
|
|
971
980
|
userName: envUserName || "viagen",
|
|
972
981
|
userEmail: envUserEmail || "noreply@viagen.dev",
|
|
973
|
-
isDirty:
|
|
974
|
-
|
|
982
|
+
isDirty: (() => {
|
|
983
|
+
try {
|
|
984
|
+
return execSync2("git status --porcelain", { cwd, encoding: "utf-8" }).trim().length > 0;
|
|
985
|
+
} catch {
|
|
986
|
+
return false;
|
|
987
|
+
}
|
|
988
|
+
})()
|
|
975
989
|
} : getGitInfo(cwd);
|
|
976
990
|
if (envRemoteUrl) {
|
|
977
991
|
console.log("Using git info from .env");
|
|
@@ -1011,7 +1025,7 @@ async function sandbox(args, options) {
|
|
|
1011
1025
|
userEmail: gitInfo.userEmail,
|
|
1012
1026
|
token: githubToken
|
|
1013
1027
|
});
|
|
1014
|
-
if (
|
|
1028
|
+
if (gitInfo.isDirty && !branchOverride) {
|
|
1015
1029
|
console.log("");
|
|
1016
1030
|
console.log("Your working tree has uncommitted changes.");
|
|
1017
1031
|
console.log("");
|
|
@@ -1070,6 +1084,9 @@ async function sandbox(args, options) {
|
|
|
1070
1084
|
console.log(` Repo: ${deployGit.remoteUrl}`);
|
|
1071
1085
|
console.log(` Branch: ${deployGit.branch}`);
|
|
1072
1086
|
}
|
|
1087
|
+
if (env["SANDBOX_ROOT_DIR"]) {
|
|
1088
|
+
console.log(` Root: ${env["SANDBOX_ROOT_DIR"]}`);
|
|
1089
|
+
}
|
|
1073
1090
|
const frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
1074
1091
|
let frameIdx = 0;
|
|
1075
1092
|
const spinner = setInterval(() => {
|
|
@@ -1093,7 +1110,8 @@ async function sandbox(args, options) {
|
|
|
1093
1110
|
} : void 0,
|
|
1094
1111
|
fling: existsSync(join2(homedir(), ".fling", "token")) ? { token: readFileSync2(join2(homedir(), ".fling", "token"), "utf-8").trim() } : void 0,
|
|
1095
1112
|
timeoutMinutes,
|
|
1096
|
-
prompt
|
|
1113
|
+
prompt,
|
|
1114
|
+
rootDir: env["SANDBOX_ROOT_DIR"] || void 0
|
|
1097
1115
|
});
|
|
1098
1116
|
clearInterval(spinner);
|
|
1099
1117
|
process.stdout.write("\r \u2713 Sandbox ready! \n");
|
package/dist/index.d.ts
CHANGED
|
@@ -51,6 +51,8 @@ interface DeploySandboxOptions {
|
|
|
51
51
|
envVars?: Record<string, string>;
|
|
52
52
|
/** Initial prompt to auto-send in the chat UI on load. */
|
|
53
53
|
prompt?: string;
|
|
54
|
+
/** Subdirectory to use as the working directory (for monorepos). */
|
|
55
|
+
rootDir?: string;
|
|
54
56
|
}
|
|
55
57
|
interface DeploySandboxResult {
|
|
56
58
|
/** Full URL with auth token in query string. */
|
package/dist/index.js
CHANGED
|
@@ -6,8 +6,8 @@ var __export = (target, all) => {
|
|
|
6
6
|
|
|
7
7
|
// src/index.ts
|
|
8
8
|
import { execSync as execSync2 } from "child_process";
|
|
9
|
-
import { mkdirSync as
|
|
10
|
-
import { join as
|
|
9
|
+
import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync5 } from "fs";
|
|
10
|
+
import { join as join7 } from "path";
|
|
11
11
|
import { loadEnv } from "vite";
|
|
12
12
|
|
|
13
13
|
// src/logger.ts
|
|
@@ -101,16 +101,17 @@ function registerHealthRoutes(server, env, errorRef) {
|
|
|
101
101
|
const prompt = env["VIAGEN_PROMPT"] || null;
|
|
102
102
|
const taskId = env["VIAGEN_TASK_ID"] || null;
|
|
103
103
|
const environmentId = env["VIAGEN_ENVIRONMENT_ID"] || null;
|
|
104
|
+
const projectId = env["VIAGEN_PROJECT_ID"] || null;
|
|
104
105
|
res.setHeader("Content-Type", "application/json");
|
|
105
106
|
if (configured) {
|
|
106
|
-
res.end(JSON.stringify({ status: "ok", configured: true, git, vercel, branch, session, prompt, taskId, environmentId, missing }));
|
|
107
|
+
res.end(JSON.stringify({ status: "ok", configured: true, git, vercel, branch, session, prompt, taskId, environmentId, projectId, missing }));
|
|
107
108
|
} else {
|
|
108
109
|
res.end(
|
|
109
|
-
JSON.stringify({ status: "error", configured: false, git, vercel, branch, session, prompt, taskId, environmentId, missing })
|
|
110
|
+
JSON.stringify({ status: "error", configured: false, git, vercel, branch, session, prompt, taskId, environmentId, projectId, missing })
|
|
110
111
|
);
|
|
111
112
|
}
|
|
112
113
|
});
|
|
113
|
-
const currentVersion = true ? "0.
|
|
114
|
+
const currentVersion = true ? "0.1.3" : "0.0.0";
|
|
114
115
|
debug("health", `version resolved: ${currentVersion}`);
|
|
115
116
|
let versionCache = null;
|
|
116
117
|
server.middlewares.use("/via/version", (_req, res) => {
|
|
@@ -770,10 +771,10 @@ data: ${JSON.stringify(doneData)}
|
|
|
770
771
|
`);
|
|
771
772
|
res.end();
|
|
772
773
|
}
|
|
773
|
-
if (opts.viagenClient && opts.
|
|
774
|
+
if (opts.viagenClient && opts.projectId) {
|
|
774
775
|
const taskId = opts.env["VIAGEN_TASK_ID"];
|
|
775
776
|
if (taskId && (event.inputTokens || event.outputTokens)) {
|
|
776
|
-
opts.viagenClient.tasks.update(opts.
|
|
777
|
+
opts.viagenClient.tasks.update(opts.projectId, taskId, {
|
|
777
778
|
...event.inputTokens != null && { inputTokens: event.inputTokens },
|
|
778
779
|
...event.outputTokens != null && { outputTokens: event.outputTokens }
|
|
779
780
|
}).catch((err) => {
|
|
@@ -3798,7 +3799,7 @@ function buildUiHtml(opts) {
|
|
|
3798
3799
|
|
|
3799
3800
|
// Store task context for history rendering
|
|
3800
3801
|
healthTaskId = data.taskId || null;
|
|
3801
|
-
healthProjectId = data.environmentId || null;
|
|
3802
|
+
healthProjectId = data.projectId || data.environmentId || null;
|
|
3802
3803
|
|
|
3803
3804
|
// Load chat history from server (source of truth)
|
|
3804
3805
|
await loadHistory();
|
|
@@ -3892,7 +3893,7 @@ function buildUiHtml(opts) {
|
|
|
3892
3893
|
if (data.prompt && data.configured && chatLog.length === 0) {
|
|
3893
3894
|
if (data.taskId) {
|
|
3894
3895
|
// Task mode: show link instead of raw prompt
|
|
3895
|
-
var taskUrl = 'https://app.viagen.dev/
|
|
3896
|
+
var taskUrl = 'https://app.viagen.dev/projects/' + (data.projectId || data.environmentId) + '/tasks/' + data.taskId;
|
|
3896
3897
|
var div = document.createElement('div');
|
|
3897
3898
|
div.className = 'msg msg-user';
|
|
3898
3899
|
div.innerHTML = '<span class="label">Task</span><span class="text">Working on <a href="' + escapeHtml(taskUrl) + '" target="_blank" style="color:#2563eb;text-decoration:underline;">Via Task</a></span>';
|
|
@@ -4199,15 +4200,17 @@ function buildUiHtml(opts) {
|
|
|
4199
4200
|
// src/iframe.ts
|
|
4200
4201
|
function buildIframeHtml(opts) {
|
|
4201
4202
|
const pw = opts.panelWidth;
|
|
4203
|
+
const appSrc = opts.appUrl ?? "/?_viagen_embed=1";
|
|
4202
4204
|
return `<!DOCTYPE html>
|
|
4203
4205
|
<html lang="en">
|
|
4204
4206
|
<head>
|
|
4205
4207
|
<meta charset="UTF-8">
|
|
4206
4208
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
4209
|
+
<meta name="color-scheme" content="light dark">
|
|
4207
4210
|
<title>viagen</title>
|
|
4208
4211
|
<style>
|
|
4209
4212
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
4210
|
-
body { display: flex; height: 100vh; background: #
|
|
4213
|
+
body { display: flex; height: 100vh; background: #f5f5f5; overflow: hidden; }
|
|
4211
4214
|
#app-frame { flex: 1; border: none; height: 100%; min-width: 200px; }
|
|
4212
4215
|
#divider {
|
|
4213
4216
|
width: 1px;
|
|
@@ -4229,24 +4232,67 @@ function buildIframeHtml(opts) {
|
|
|
4229
4232
|
#divider:hover, #divider.active { background: #a3a3a3; }
|
|
4230
4233
|
#chat-frame { width: ${pw}px; border: none; height: 100%; min-width: 280px; background: #ffffff; }
|
|
4231
4234
|
.dragging iframe { pointer-events: none; }
|
|
4235
|
+
#loading {
|
|
4236
|
+
position: fixed; inset: 0; display: flex; align-items: center; justify-content: center;
|
|
4237
|
+
background: #f5f5f5; color: #666; font-family: system-ui; z-index: 100;
|
|
4238
|
+
transition: opacity 0.3s;
|
|
4239
|
+
}
|
|
4240
|
+
#loading.hidden { opacity: 0; pointer-events: none; }
|
|
4241
|
+
.spinner { width: 20px; height: 20px; border: 2px solid #ddd; border-top-color: #888;
|
|
4242
|
+
border-radius: 50%; animation: spin .6s linear infinite; margin-right: 12px; }
|
|
4243
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
4244
|
+
body.ready { background: #ffffff; }
|
|
4245
|
+
@media (prefers-color-scheme: dark) {
|
|
4246
|
+
body { background: #0a0a0a; }
|
|
4247
|
+
#loading { background: #0a0a0a; color: #888; }
|
|
4248
|
+
.spinner { border-color: #333; border-top-color: #999; }
|
|
4249
|
+
#divider { background: #333; }
|
|
4250
|
+
#divider:hover, #divider.active { background: #555; }
|
|
4251
|
+
#chat-frame { background: #0a0a0a; }
|
|
4252
|
+
body.ready { background: #0a0a0a; }
|
|
4253
|
+
}
|
|
4232
4254
|
</style>
|
|
4233
4255
|
</head>
|
|
4234
4256
|
<body>
|
|
4235
|
-
<
|
|
4257
|
+
<div id="loading"><div class="spinner"></div><span>Starting dev server\u2026</span></div>
|
|
4258
|
+
<iframe id="app-frame"></iframe>
|
|
4236
4259
|
<div id="divider"></div>
|
|
4237
|
-
<iframe id="chat-frame"
|
|
4260
|
+
<iframe id="chat-frame"></iframe>
|
|
4238
4261
|
<script>
|
|
4262
|
+
var appFrame = document.getElementById('app-frame');
|
|
4263
|
+
var chatFrame = document.getElementById('chat-frame');
|
|
4264
|
+
var loading = document.getElementById('loading');
|
|
4265
|
+
|
|
4266
|
+
// Wait for Vite to be fully initialized before loading iframes.
|
|
4267
|
+
// We probe /@vite/client which goes through Vite's transform pipeline
|
|
4268
|
+
// and only succeeds after the dev server has finished starting up.
|
|
4269
|
+
// Static middleware routes like /via/ui return 200 instantly (before
|
|
4270
|
+
// Vite is ready), so they can't be used as readiness probes.
|
|
4271
|
+
(async function() {
|
|
4272
|
+
for (var i = 0; i < 120; i++) {
|
|
4273
|
+
try {
|
|
4274
|
+
var r = await fetch('/@vite/client', { credentials: 'same-origin' });
|
|
4275
|
+
if (r.ok) break;
|
|
4276
|
+
} catch(e) {}
|
|
4277
|
+
await new Promise(function(resolve) { setTimeout(resolve, 500); });
|
|
4278
|
+
}
|
|
4279
|
+
// Set iframe srcs \u2014 now Vite should be ready
|
|
4280
|
+
chatFrame.src = '/via/ui';
|
|
4281
|
+
appFrame.src = ${JSON.stringify(appSrc)};
|
|
4282
|
+
// Hide loading overlay once chat frame loads
|
|
4283
|
+
chatFrame.addEventListener('load', function() {
|
|
4284
|
+
loading.classList.add('hidden');
|
|
4285
|
+
document.body.classList.add('ready');
|
|
4286
|
+
chatFrame.contentWindow.postMessage({ type: 'viagen:context', iframe: true }, '*');
|
|
4287
|
+
});
|
|
4288
|
+
})();
|
|
4289
|
+
|
|
4239
4290
|
// Relay postMessage from app iframe to chat iframe (e.g. "Fix This Error")
|
|
4240
4291
|
window.addEventListener('message', function(ev) {
|
|
4241
4292
|
if (ev.data && ev.data.type === 'viagen:send') {
|
|
4242
|
-
|
|
4293
|
+
chatFrame.contentWindow.postMessage(ev.data, '*');
|
|
4243
4294
|
}
|
|
4244
4295
|
});
|
|
4245
|
-
// Tell chat iframe it's in split-view mode (hides pop-out button)
|
|
4246
|
-
var chatFrame = document.getElementById('chat-frame');
|
|
4247
|
-
chatFrame.addEventListener('load', function() {
|
|
4248
|
-
chatFrame.contentWindow.postMessage({ type: 'viagen:context', iframe: true }, '*');
|
|
4249
|
-
});
|
|
4250
4296
|
|
|
4251
4297
|
// Drag-resizable divider
|
|
4252
4298
|
var divider = document.getElementById('divider');
|
|
@@ -4287,6 +4333,39 @@ function parseCookies(header) {
|
|
|
4287
4333
|
}
|
|
4288
4334
|
return cookies;
|
|
4289
4335
|
}
|
|
4336
|
+
function buildWaitPage(targetUrl) {
|
|
4337
|
+
return `<!DOCTYPE html>
|
|
4338
|
+
<html><head><meta charset="utf-8"><title>Loading\u2026</title>
|
|
4339
|
+
<style>
|
|
4340
|
+
body { margin: 0; display: flex; align-items: center; justify-content: center;
|
|
4341
|
+
height: 100vh; background: #f5f5f5; color: #666; font-family: system-ui; }
|
|
4342
|
+
.spinner { width: 20px; height: 20px; border: 2px solid #ddd; border-top-color: #888;
|
|
4343
|
+
border-radius: 50%; animation: spin .6s linear infinite; margin-right: 12px; }
|
|
4344
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
4345
|
+
@media (prefers-color-scheme: dark) {
|
|
4346
|
+
body { background: #0a0a0a; color: #888; }
|
|
4347
|
+
.spinner { border-color: #333; border-top-color: #999; }
|
|
4348
|
+
}
|
|
4349
|
+
</style></head>
|
|
4350
|
+
<body><div class="spinner"></div><span>Starting dev server\u2026</span>
|
|
4351
|
+
<script>
|
|
4352
|
+
(async () => {
|
|
4353
|
+
const target = ${JSON.stringify(targetUrl)};
|
|
4354
|
+
// Probe /@vite/client \u2014 it goes through Vite's transform pipeline and
|
|
4355
|
+
// only succeeds once the dev server is fully initialized. Static routes
|
|
4356
|
+
// like /via/iframe return 200 too early.
|
|
4357
|
+
for (let i = 0; i < 60; i++) {
|
|
4358
|
+
try {
|
|
4359
|
+
const r = await fetch('/@vite/client', { credentials: 'same-origin' });
|
|
4360
|
+
if (r.ok) { window.location.replace(target); return; }
|
|
4361
|
+
} catch {}
|
|
4362
|
+
await new Promise(r => setTimeout(r, 500));
|
|
4363
|
+
}
|
|
4364
|
+
// After 30s, navigate anyway \u2014 let the user see whatever error there is
|
|
4365
|
+
window.location.replace(target);
|
|
4366
|
+
})();
|
|
4367
|
+
</script></body></html>`;
|
|
4368
|
+
}
|
|
4290
4369
|
function createAuthMiddleware(token) {
|
|
4291
4370
|
return function authMiddleware(req, res, next) {
|
|
4292
4371
|
const url2 = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
@@ -4294,26 +4373,26 @@ function createAuthMiddleware(token) {
|
|
|
4294
4373
|
if (pathMatch && pathMatch[2] === token) {
|
|
4295
4374
|
const cleanPath = pathMatch[1] || "/";
|
|
4296
4375
|
const cleanUrl = cleanPath + (url2.search || "");
|
|
4297
|
-
debug("auth", `token URL match (/t/:token) \u2192 redirect to ${cleanUrl}`);
|
|
4376
|
+
debug("auth", `token URL match (/t/:token) \u2192 wait-page redirect to ${cleanUrl}`);
|
|
4298
4377
|
res.setHeader(
|
|
4299
4378
|
"Set-Cookie",
|
|
4300
4379
|
`viagen_session=${token}; HttpOnly; SameSite=Lax; Path=/`
|
|
4301
4380
|
);
|
|
4302
|
-
res.writeHead(
|
|
4303
|
-
res.end();
|
|
4381
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
4382
|
+
res.end(buildWaitPage(cleanUrl));
|
|
4304
4383
|
return;
|
|
4305
4384
|
}
|
|
4306
4385
|
const queryToken = url2.searchParams.get("token");
|
|
4307
4386
|
if (queryToken === token) {
|
|
4308
4387
|
url2.searchParams.delete("token");
|
|
4309
4388
|
const cleanUrl = url2.pathname + (url2.search || "");
|
|
4310
|
-
debug("auth", `query token match (?token=) \u2192 redirect to ${cleanUrl}`);
|
|
4389
|
+
debug("auth", `query token match (?token=) \u2192 wait-page redirect to ${cleanUrl}`);
|
|
4311
4390
|
res.setHeader(
|
|
4312
4391
|
"Set-Cookie",
|
|
4313
4392
|
`viagen_session=${token}; HttpOnly; SameSite=Lax; Path=/`
|
|
4314
4393
|
);
|
|
4315
|
-
res.writeHead(
|
|
4316
|
-
res.end();
|
|
4394
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
4395
|
+
res.end(buildWaitPage(cleanUrl));
|
|
4317
4396
|
return;
|
|
4318
4397
|
}
|
|
4319
4398
|
if (req.headers.cookie) {
|
|
@@ -18657,8 +18736,9 @@ import {
|
|
|
18657
18736
|
tool
|
|
18658
18737
|
} from "@anthropic-ai/claude-agent-sdk";
|
|
18659
18738
|
function createViagenTools(config2) {
|
|
18660
|
-
const { client,
|
|
18739
|
+
const { client, projectId, processManager } = config2;
|
|
18661
18740
|
const taskId = process.env["VIAGEN_TASK_ID"];
|
|
18741
|
+
debug("tools", `MCP tools created (projectId: ${projectId}, taskId: ${taskId || "none"})`);
|
|
18662
18742
|
const tools = [
|
|
18663
18743
|
tool(
|
|
18664
18744
|
"viagen_update_task",
|
|
@@ -18674,6 +18754,7 @@ function createViagenTools(config2) {
|
|
|
18674
18754
|
prReviewStatus: external_exports.string().optional().describe("PR review outcome \u2014 e.g. 'pass', 'flag', or 'fail'.")
|
|
18675
18755
|
},
|
|
18676
18756
|
async (args) => {
|
|
18757
|
+
debug("tools", `viagen_update_task called (taskId: ${args.taskId || taskId || "none"}, status: ${args.status || "none"}, projectId: ${projectId})`);
|
|
18677
18758
|
const id = args.taskId || taskId;
|
|
18678
18759
|
if (!id) {
|
|
18679
18760
|
return {
|
|
@@ -18682,7 +18763,7 @@ function createViagenTools(config2) {
|
|
|
18682
18763
|
}
|
|
18683
18764
|
const internalStatus = args.status === "review" ? "validating" : args.status === "completed" ? "completed" : args.status;
|
|
18684
18765
|
try {
|
|
18685
|
-
await client.tasks.update(
|
|
18766
|
+
await client.tasks.update(projectId, id, {
|
|
18686
18767
|
...internalStatus && { status: internalStatus },
|
|
18687
18768
|
...args.prUrl && { prUrl: args.prUrl },
|
|
18688
18769
|
result: args.result,
|
|
@@ -18695,6 +18776,9 @@ function createViagenTools(config2) {
|
|
|
18695
18776
|
};
|
|
18696
18777
|
} catch (err) {
|
|
18697
18778
|
const message = err instanceof Error ? err.message : "Unknown error";
|
|
18779
|
+
const status = err?.status;
|
|
18780
|
+
const detail = err?.detail;
|
|
18781
|
+
debug("tools", `viagen_update_task FAILED: ${status || "?"} ${message}${detail ? ` (${detail})` : ""}`);
|
|
18698
18782
|
return {
|
|
18699
18783
|
content: [{ type: "text", text: `Error updating task: ${message}` }]
|
|
18700
18784
|
};
|
|
@@ -18703,20 +18787,30 @@ function createViagenTools(config2) {
|
|
|
18703
18787
|
),
|
|
18704
18788
|
tool(
|
|
18705
18789
|
"viagen_list_tasks",
|
|
18706
|
-
"List tasks in the current
|
|
18790
|
+
"List tasks in the current project. Optionally filter by status.",
|
|
18707
18791
|
{
|
|
18708
18792
|
status: external_exports.enum(["ready", "running", "validating", "completed", "timed_out"]).optional().describe("Filter tasks by status.")
|
|
18709
18793
|
},
|
|
18710
18794
|
async (args) => {
|
|
18711
|
-
|
|
18712
|
-
|
|
18713
|
-
|
|
18714
|
-
|
|
18715
|
-
|
|
18716
|
-
|
|
18717
|
-
|
|
18718
|
-
|
|
18719
|
-
|
|
18795
|
+
debug("tools", `viagen_list_tasks called (projectId: ${projectId}, status: ${args.status || "all"})`);
|
|
18796
|
+
try {
|
|
18797
|
+
const tasks = await client.tasks.list(projectId, args.status);
|
|
18798
|
+
debug("tools", `viagen_list_tasks returned ${tasks.length} tasks`);
|
|
18799
|
+
return {
|
|
18800
|
+
content: [
|
|
18801
|
+
{
|
|
18802
|
+
type: "text",
|
|
18803
|
+
text: JSON.stringify(tasks, null, 2)
|
|
18804
|
+
}
|
|
18805
|
+
]
|
|
18806
|
+
};
|
|
18807
|
+
} catch (err) {
|
|
18808
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
18809
|
+
debug("tools", `viagen_list_tasks FAILED: ${message}`);
|
|
18810
|
+
return {
|
|
18811
|
+
content: [{ type: "text", text: `Error listing tasks: ${message}` }]
|
|
18812
|
+
};
|
|
18813
|
+
}
|
|
18720
18814
|
}
|
|
18721
18815
|
),
|
|
18722
18816
|
tool(
|
|
@@ -18726,42 +18820,89 @@ function createViagenTools(config2) {
|
|
|
18726
18820
|
taskId: external_exports.string().describe("The task ID to retrieve.")
|
|
18727
18821
|
},
|
|
18728
18822
|
async (args) => {
|
|
18729
|
-
|
|
18730
|
-
|
|
18731
|
-
|
|
18732
|
-
|
|
18733
|
-
|
|
18734
|
-
|
|
18735
|
-
|
|
18736
|
-
|
|
18737
|
-
|
|
18823
|
+
debug("tools", `viagen_get_task called (projectId: ${projectId}, taskId: ${args.taskId})`);
|
|
18824
|
+
try {
|
|
18825
|
+
const task = await client.tasks.get(projectId, args.taskId);
|
|
18826
|
+
debug("tools", `viagen_get_task success (status: ${task.status})`);
|
|
18827
|
+
return {
|
|
18828
|
+
content: [
|
|
18829
|
+
{
|
|
18830
|
+
type: "text",
|
|
18831
|
+
text: JSON.stringify(task, null, 2)
|
|
18832
|
+
}
|
|
18833
|
+
]
|
|
18834
|
+
};
|
|
18835
|
+
} catch (err) {
|
|
18836
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
18837
|
+
const status = err?.status;
|
|
18838
|
+
debug("tools", `viagen_get_task FAILED: ${status || "?"} ${message}`);
|
|
18839
|
+
return {
|
|
18840
|
+
content: [{ type: "text", text: `Error getting task: ${message}` }]
|
|
18841
|
+
};
|
|
18842
|
+
}
|
|
18738
18843
|
}
|
|
18739
18844
|
),
|
|
18740
18845
|
tool(
|
|
18741
18846
|
"viagen_create_task",
|
|
18742
|
-
"Create a new task in the current
|
|
18847
|
+
"Create a new task in the current project. Use this to create follow-up work.",
|
|
18743
18848
|
{
|
|
18744
18849
|
prompt: external_exports.string().describe("The task prompt / instructions."),
|
|
18745
18850
|
branch: external_exports.string().optional().describe("Git branch name for the task."),
|
|
18746
18851
|
type: external_exports.enum(["task", "plan"]).optional().describe("Task type: 'task' for code changes, 'plan' for implementation plans.")
|
|
18747
18852
|
},
|
|
18748
18853
|
async (args) => {
|
|
18749
|
-
|
|
18750
|
-
|
|
18751
|
-
|
|
18752
|
-
|
|
18753
|
-
|
|
18754
|
-
|
|
18755
|
-
|
|
18756
|
-
|
|
18757
|
-
|
|
18758
|
-
|
|
18759
|
-
|
|
18760
|
-
|
|
18761
|
-
|
|
18854
|
+
debug("tools", `viagen_create_task called (projectId: ${projectId}, type: ${args.type || "task"}, prompt: "${args.prompt.slice(0, 80)}...")`);
|
|
18855
|
+
try {
|
|
18856
|
+
const task = await client.tasks.create(projectId, {
|
|
18857
|
+
prompt: args.prompt,
|
|
18858
|
+
branch: args.branch,
|
|
18859
|
+
type: args.type
|
|
18860
|
+
});
|
|
18861
|
+
debug("tools", `viagen_create_task success (taskId: ${task.id})`);
|
|
18862
|
+
return {
|
|
18863
|
+
content: [
|
|
18864
|
+
{
|
|
18865
|
+
type: "text",
|
|
18866
|
+
text: JSON.stringify(task, null, 2)
|
|
18867
|
+
}
|
|
18868
|
+
]
|
|
18869
|
+
};
|
|
18870
|
+
} catch (err) {
|
|
18871
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
18872
|
+
const status = err?.status;
|
|
18873
|
+
const detail = err?.detail;
|
|
18874
|
+
debug("tools", `viagen_create_task FAILED: ${status || "?"} ${message}${detail ? ` (${detail})` : ""}`);
|
|
18875
|
+
return {
|
|
18876
|
+
content: [{ type: "text", text: `Error creating task: ${message}` }]
|
|
18877
|
+
};
|
|
18878
|
+
}
|
|
18762
18879
|
}
|
|
18763
18880
|
)
|
|
18764
18881
|
];
|
|
18882
|
+
if (processManager) {
|
|
18883
|
+
tools.push(
|
|
18884
|
+
tool(
|
|
18885
|
+
"viagen_restart_preview",
|
|
18886
|
+
"Restart the preview server. Use after installing packages, changing configs, or when the preview is broken. Optionally provide a new command to run.",
|
|
18887
|
+
{
|
|
18888
|
+
command: external_exports.string().optional().describe("New command to use for the preview server (e.g. 'npm run build && npm run start'). If omitted, restarts with the current command.")
|
|
18889
|
+
},
|
|
18890
|
+
async (args) => {
|
|
18891
|
+
try {
|
|
18892
|
+
const result = await processManager.restart(args.command);
|
|
18893
|
+
return {
|
|
18894
|
+
content: [{ type: "text", text: result }]
|
|
18895
|
+
};
|
|
18896
|
+
} catch (err) {
|
|
18897
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
18898
|
+
return {
|
|
18899
|
+
content: [{ type: "text", text: `Error restarting preview: ${message}` }]
|
|
18900
|
+
};
|
|
18901
|
+
}
|
|
18902
|
+
}
|
|
18903
|
+
)
|
|
18904
|
+
);
|
|
18905
|
+
}
|
|
18765
18906
|
return createSdkMcpServer({ name: "viagen", tools });
|
|
18766
18907
|
}
|
|
18767
18908
|
var PLAN_MODE_DISALLOWED_TOOLS = ["Edit", "NotebookEdit"];
|
|
@@ -18791,23 +18932,313 @@ Constraints:
|
|
|
18791
18932
|
- Only create new files inside the plans/ directory.
|
|
18792
18933
|
- Your plan should include: context, proposed changes (with file paths and descriptions), implementation order, and potential risks.
|
|
18793
18934
|
`;
|
|
18794
|
-
|
|
18795
|
-
|
|
18796
|
-
|
|
18797
|
-
|
|
18798
|
-
|
|
18799
|
-
|
|
18800
|
-
|
|
18801
|
-
|
|
18802
|
-
|
|
18935
|
+
function buildTaskToolsPrompt(opts) {
|
|
18936
|
+
const parts = [];
|
|
18937
|
+
parts.push(`
|
|
18938
|
+
## Viagen Platform
|
|
18939
|
+
|
|
18940
|
+
You are connected to the viagen development platform. This session is part of a viagen project (ID: ${opts.projectId}).${opts.taskId ? ` You are currently working on task ${opts.taskId}.` : ""}${opts.branch ? ` Current branch: ${opts.branch}.` : ""}
|
|
18941
|
+
|
|
18942
|
+
When users ask about the project, tasks, work history, or anything related to the viagen platform \u2014 **always use your viagen MCP tools** to answer. Do NOT try to grep the codebase or guess \u2014 the platform is your source of truth for project and task information.
|
|
18943
|
+
|
|
18944
|
+
### Available viagen tools
|
|
18945
|
+
|
|
18946
|
+
- **viagen_list_tasks** \u2014 List tasks in this project. Filter by status: ready, running, validating, completed, timed_out. Call this when users ask "what tasks are there?", "what's been done?", "what's pending?", etc.
|
|
18947
|
+
- **viagen_get_task** \u2014 Get full details of a specific task including its prompt, status, branch, PR URL, and results.
|
|
18948
|
+
- **viagen_create_task** \u2014 Create a new task. Use when you identify follow-up work or when the user asks you to create a task.
|
|
18949
|
+
- **viagen_update_task** \u2014 Update a task's status. Use 'review' after creating a PR, 'completed' when fully done. Defaults to the current task if one is set.`);
|
|
18950
|
+
if (opts.hasProcessManager) {
|
|
18951
|
+
parts.push(`
|
|
18952
|
+
- **viagen_restart_preview** \u2014 Restart the app preview server. Use after installing packages, changing configs, or when the preview is broken.`);
|
|
18953
|
+
}
|
|
18954
|
+
parts.push(`
|
|
18955
|
+
|
|
18956
|
+
### How to respond to platform questions
|
|
18957
|
+
|
|
18958
|
+
- "What tasks are there?" \u2192 call viagen_list_tasks, summarize the results conversationally
|
|
18959
|
+
- "What's the status of task X?" \u2192 call viagen_get_task with the ID
|
|
18960
|
+
- "What work has been done?" \u2192 call viagen_list_tasks with status "completed" or "validating"
|
|
18961
|
+
- "What's pending?" \u2192 call viagen_list_tasks with status "ready"
|
|
18962
|
+
- "Tell me about this project" \u2192 call viagen_list_tasks (no filter) to give an overview of all work
|
|
18963
|
+
|
|
18964
|
+
Always present the results in a friendly, conversational way \u2014 not raw JSON.`);
|
|
18965
|
+
return parts.join("");
|
|
18966
|
+
}
|
|
18967
|
+
var TASK_TOOLS_PROMPT = buildTaskToolsPrompt({ projectId: "unknown" });
|
|
18803
18968
|
|
|
18804
18969
|
// src/index.ts
|
|
18805
18970
|
import { createViagen } from "viagen-sdk";
|
|
18806
18971
|
|
|
18972
|
+
// src/process-manager.ts
|
|
18973
|
+
import { spawn } from "child_process";
|
|
18974
|
+
import { closeSync, existsSync as existsSync2, mkdirSync as mkdirSync3, openSync, readFileSync as readFileSync4, readSync, statSync as statSync2, unlinkSync, writeFileSync as writeFileSync4 } from "fs";
|
|
18975
|
+
import { join as join5 } from "path";
|
|
18976
|
+
var debug2 = (label, ...args) => debug(`pm:${label}`, ...args);
|
|
18977
|
+
var ProcessManager = class {
|
|
18978
|
+
child = null;
|
|
18979
|
+
adoptedPid = null;
|
|
18980
|
+
command;
|
|
18981
|
+
cwd;
|
|
18982
|
+
env;
|
|
18983
|
+
logBuffer;
|
|
18984
|
+
appPort;
|
|
18985
|
+
viagenDir;
|
|
18986
|
+
pidFile;
|
|
18987
|
+
stdoutLog;
|
|
18988
|
+
stderrLog;
|
|
18989
|
+
logTailInterval = null;
|
|
18990
|
+
stdoutOffset = 0;
|
|
18991
|
+
stderrOffset = 0;
|
|
18992
|
+
restarting = false;
|
|
18993
|
+
restartCount = 0;
|
|
18994
|
+
constructor(opts) {
|
|
18995
|
+
this.command = opts.command;
|
|
18996
|
+
this.cwd = opts.cwd;
|
|
18997
|
+
this.env = opts.env ?? {};
|
|
18998
|
+
this.logBuffer = opts.logBuffer;
|
|
18999
|
+
this.appPort = opts.appPort ?? 5173;
|
|
19000
|
+
this.viagenDir = join5(this.cwd, ".viagen");
|
|
19001
|
+
mkdirSync3(this.viagenDir, { recursive: true });
|
|
19002
|
+
this.pidFile = join5(this.viagenDir, "app.pid");
|
|
19003
|
+
this.stdoutLog = join5(this.viagenDir, "app.stdout.log");
|
|
19004
|
+
this.stderrLog = join5(this.viagenDir, "app.stderr.log");
|
|
19005
|
+
}
|
|
19006
|
+
/** Start the app process. If already running (or adopted from a previous session), does nothing. */
|
|
19007
|
+
start() {
|
|
19008
|
+
if (this.child && !this.child.killed) {
|
|
19009
|
+
debug2("start", "process already running (spawned), skipping");
|
|
19010
|
+
return;
|
|
19011
|
+
}
|
|
19012
|
+
const existingPid = this.readPid();
|
|
19013
|
+
if (existingPid && this.isProcessAlive(existingPid)) {
|
|
19014
|
+
debug2("start", `adopting existing app process (pid: ${existingPid})`);
|
|
19015
|
+
this.logBuffer.push("info", `[viagen:pm] App already running from previous session (pid: ${existingPid})`);
|
|
19016
|
+
this.adoptedPid = existingPid;
|
|
19017
|
+
return;
|
|
19018
|
+
}
|
|
19019
|
+
if (existingPid) {
|
|
19020
|
+
debug2("start", `stale pid file (pid: ${existingPid} not alive), removing`);
|
|
19021
|
+
this.removePid();
|
|
19022
|
+
}
|
|
19023
|
+
writeFileSync4(this.stdoutLog, "");
|
|
19024
|
+
writeFileSync4(this.stderrLog, "");
|
|
19025
|
+
this.stdoutOffset = 0;
|
|
19026
|
+
this.stderrOffset = 0;
|
|
19027
|
+
debug2("start", `spawning: ${this.command} (cwd: ${this.cwd}, port: ${this.appPort})`);
|
|
19028
|
+
this.logBuffer.push("info", `[viagen:pm] Starting app: ${this.command} (PORT=${this.appPort})`);
|
|
19029
|
+
const stdoutFd = openSync(this.stdoutLog, "a");
|
|
19030
|
+
const stderrFd = openSync(this.stderrLog, "a");
|
|
19031
|
+
this.child = spawn(this.command, [], {
|
|
19032
|
+
cwd: this.cwd,
|
|
19033
|
+
env: { ...process.env, ...this.env, PORT: String(this.appPort), __VIAGEN_CHILD: "1" },
|
|
19034
|
+
stdio: ["ignore", stdoutFd, stderrFd],
|
|
19035
|
+
shell: true,
|
|
19036
|
+
// Detached so the app survives if the viagen chat server restarts
|
|
19037
|
+
// (e.g. during a viagen dependency update in a sandbox).
|
|
19038
|
+
detached: true
|
|
19039
|
+
});
|
|
19040
|
+
const pid = this.child.pid;
|
|
19041
|
+
debug2("start", `process started (pid: ${pid})`);
|
|
19042
|
+
this.logBuffer.push("info", `[viagen:pm] App process started (pid: ${pid})`);
|
|
19043
|
+
if (pid) {
|
|
19044
|
+
this.writePid(pid);
|
|
19045
|
+
}
|
|
19046
|
+
this.child.unref();
|
|
19047
|
+
this.startLogTail();
|
|
19048
|
+
this.child.on("exit", (code, signal) => {
|
|
19049
|
+
debug2("exit", `process exited (code: ${code}, signal: ${signal})`);
|
|
19050
|
+
this.logBuffer.push(
|
|
19051
|
+
code === 0 ? "info" : "warn",
|
|
19052
|
+
`[viagen:pm] App process exited (code: ${code}, signal: ${signal})`
|
|
19053
|
+
);
|
|
19054
|
+
this.child = null;
|
|
19055
|
+
this.stopLogTail();
|
|
19056
|
+
this.removePid();
|
|
19057
|
+
if (!this.restarting && code !== 0 && code !== null) {
|
|
19058
|
+
this.restartCount++;
|
|
19059
|
+
if (this.restartCount <= 3) {
|
|
19060
|
+
const delay = this.restartCount * 2e3;
|
|
19061
|
+
this.logBuffer.push("warn", `[viagen:pm] Auto-restarting in ${delay / 1e3}s (attempt ${this.restartCount}/3)`);
|
|
19062
|
+
setTimeout(() => this.start(), delay);
|
|
19063
|
+
} else {
|
|
19064
|
+
this.logBuffer.push("error", `[viagen:pm] App crashed ${this.restartCount} times, giving up`);
|
|
19065
|
+
}
|
|
19066
|
+
}
|
|
19067
|
+
});
|
|
19068
|
+
this.child.on("error", (err) => {
|
|
19069
|
+
debug2("error", `spawn error: ${err.message}`);
|
|
19070
|
+
this.logBuffer.push("error", `[viagen:pm] Failed to start app: ${err.message}`);
|
|
19071
|
+
this.child = null;
|
|
19072
|
+
this.stopLogTail();
|
|
19073
|
+
this.removePid();
|
|
19074
|
+
});
|
|
19075
|
+
}
|
|
19076
|
+
/** Stop the app process. Returns when the process has exited. */
|
|
19077
|
+
async stop() {
|
|
19078
|
+
if (this.adoptedPid) {
|
|
19079
|
+
const pid2 = this.adoptedPid;
|
|
19080
|
+
debug2("stop", `killing adopted process (pid: ${pid2})`);
|
|
19081
|
+
this.logBuffer.push("info", `[viagen:pm] Stopping adopted app (pid: ${pid2})`);
|
|
19082
|
+
try {
|
|
19083
|
+
process.kill(-pid2, "SIGTERM");
|
|
19084
|
+
} catch {
|
|
19085
|
+
}
|
|
19086
|
+
this.adoptedPid = null;
|
|
19087
|
+
this.removePid();
|
|
19088
|
+
await new Promise((resolve3) => {
|
|
19089
|
+
const check2 = () => {
|
|
19090
|
+
if (!this.isProcessAlive(pid2)) {
|
|
19091
|
+
resolve3();
|
|
19092
|
+
return;
|
|
19093
|
+
}
|
|
19094
|
+
try {
|
|
19095
|
+
process.kill(-pid2, "SIGKILL");
|
|
19096
|
+
} catch {
|
|
19097
|
+
}
|
|
19098
|
+
resolve3();
|
|
19099
|
+
};
|
|
19100
|
+
setTimeout(check2, 5e3);
|
|
19101
|
+
});
|
|
19102
|
+
return;
|
|
19103
|
+
}
|
|
19104
|
+
if (!this.child || this.child.killed) {
|
|
19105
|
+
debug2("stop", "no process to stop");
|
|
19106
|
+
return;
|
|
19107
|
+
}
|
|
19108
|
+
const pid = this.child.pid;
|
|
19109
|
+
debug2("stop", `killing process (pid: ${pid})`);
|
|
19110
|
+
this.logBuffer.push("info", `[viagen:pm] Stopping app (pid: ${pid})`);
|
|
19111
|
+
return new Promise((resolve3) => {
|
|
19112
|
+
const timeout = setTimeout(() => {
|
|
19113
|
+
debug2("stop", "SIGTERM timeout, sending SIGKILL");
|
|
19114
|
+
if (pid) {
|
|
19115
|
+
try {
|
|
19116
|
+
process.kill(-pid, "SIGKILL");
|
|
19117
|
+
} catch {
|
|
19118
|
+
}
|
|
19119
|
+
}
|
|
19120
|
+
}, 5e3);
|
|
19121
|
+
this.child.once("exit", () => {
|
|
19122
|
+
clearTimeout(timeout);
|
|
19123
|
+
this.child = null;
|
|
19124
|
+
this.removePid();
|
|
19125
|
+
debug2("stop", "process stopped");
|
|
19126
|
+
this.logBuffer.push("info", "[viagen:pm] App stopped");
|
|
19127
|
+
resolve3();
|
|
19128
|
+
});
|
|
19129
|
+
if (pid) {
|
|
19130
|
+
try {
|
|
19131
|
+
process.kill(-pid, "SIGTERM");
|
|
19132
|
+
} catch {
|
|
19133
|
+
}
|
|
19134
|
+
} else {
|
|
19135
|
+
this.child.kill("SIGTERM");
|
|
19136
|
+
}
|
|
19137
|
+
});
|
|
19138
|
+
}
|
|
19139
|
+
/** Restart the app process, optionally with a new command. */
|
|
19140
|
+
async restart(newCommand) {
|
|
19141
|
+
this.restarting = true;
|
|
19142
|
+
this.restartCount = 0;
|
|
19143
|
+
if (newCommand) {
|
|
19144
|
+
debug2("restart", `changing command to: ${newCommand}`);
|
|
19145
|
+
this.logBuffer.push("info", `[viagen:pm] Changing app command to: ${newCommand}`);
|
|
19146
|
+
this.command = newCommand;
|
|
19147
|
+
}
|
|
19148
|
+
debug2("restart", "restarting app process");
|
|
19149
|
+
this.logBuffer.push("info", "[viagen:pm] Restarting app...");
|
|
19150
|
+
await this.stop();
|
|
19151
|
+
this.start();
|
|
19152
|
+
this.restarting = false;
|
|
19153
|
+
return `App restarted with: ${this.command}`;
|
|
19154
|
+
}
|
|
19155
|
+
/** Check if the process is currently running. */
|
|
19156
|
+
get running() {
|
|
19157
|
+
if (this.adoptedPid) return this.isProcessAlive(this.adoptedPid);
|
|
19158
|
+
return this.child !== null && !this.child.killed;
|
|
19159
|
+
}
|
|
19160
|
+
/** Get the current command. */
|
|
19161
|
+
get currentCommand() {
|
|
19162
|
+
return this.command;
|
|
19163
|
+
}
|
|
19164
|
+
// ── Log tailing ───────────────────────────────────────────────
|
|
19165
|
+
/** Poll log files and feed new lines into the log buffer. */
|
|
19166
|
+
startLogTail() {
|
|
19167
|
+
this.stopLogTail();
|
|
19168
|
+
this.logTailInterval = setInterval(() => this.tailLogs(), 1e3);
|
|
19169
|
+
}
|
|
19170
|
+
stopLogTail() {
|
|
19171
|
+
if (this.logTailInterval) {
|
|
19172
|
+
clearInterval(this.logTailInterval);
|
|
19173
|
+
this.logTailInterval = null;
|
|
19174
|
+
}
|
|
19175
|
+
this.tailLogs();
|
|
19176
|
+
}
|
|
19177
|
+
tailLogs() {
|
|
19178
|
+
this.stdoutOffset = this.tailFile(this.stdoutLog, this.stdoutOffset, "info");
|
|
19179
|
+
this.stderrOffset = this.tailFile(this.stderrLog, this.stderrOffset, "error");
|
|
19180
|
+
}
|
|
19181
|
+
/** Read new bytes from a log file starting at offset. Returns new offset. */
|
|
19182
|
+
tailFile(filePath, offset, level) {
|
|
19183
|
+
try {
|
|
19184
|
+
if (!existsSync2(filePath)) return offset;
|
|
19185
|
+
const stat = statSync2(filePath);
|
|
19186
|
+
if (stat.size <= offset) return offset;
|
|
19187
|
+
const buf = Buffer.alloc(stat.size - offset);
|
|
19188
|
+
const fd = openSync(filePath, "r");
|
|
19189
|
+
const bytesRead = readSync(fd, buf, 0, buf.length, offset);
|
|
19190
|
+
closeSync(fd);
|
|
19191
|
+
if (bytesRead > 0) {
|
|
19192
|
+
const lines = buf.toString("utf-8", 0, bytesRead).split("\n").filter(Boolean);
|
|
19193
|
+
for (const line of lines) {
|
|
19194
|
+
debug2(level === "info" ? "stdout" : "stderr", line);
|
|
19195
|
+
this.logBuffer.push(level, `[preview] ${line}`);
|
|
19196
|
+
}
|
|
19197
|
+
}
|
|
19198
|
+
return offset + bytesRead;
|
|
19199
|
+
} catch {
|
|
19200
|
+
return offset;
|
|
19201
|
+
}
|
|
19202
|
+
}
|
|
19203
|
+
// ── PID file helpers ──────────────────────────────────────────
|
|
19204
|
+
writePid(pid) {
|
|
19205
|
+
try {
|
|
19206
|
+
writeFileSync4(this.pidFile, String(pid));
|
|
19207
|
+
debug2("pid", `wrote pid ${pid} to ${this.pidFile}`);
|
|
19208
|
+
} catch (err) {
|
|
19209
|
+
debug2("pid", `failed to write pid file: ${err}`);
|
|
19210
|
+
}
|
|
19211
|
+
}
|
|
19212
|
+
readPid() {
|
|
19213
|
+
try {
|
|
19214
|
+
if (!existsSync2(this.pidFile)) return null;
|
|
19215
|
+
const raw = readFileSync4(this.pidFile, "utf-8").trim();
|
|
19216
|
+
const pid = parseInt(raw, 10);
|
|
19217
|
+
return Number.isFinite(pid) ? pid : null;
|
|
19218
|
+
} catch {
|
|
19219
|
+
return null;
|
|
19220
|
+
}
|
|
19221
|
+
}
|
|
19222
|
+
removePid() {
|
|
19223
|
+
try {
|
|
19224
|
+
if (existsSync2(this.pidFile)) unlinkSync(this.pidFile);
|
|
19225
|
+
} catch {
|
|
19226
|
+
}
|
|
19227
|
+
}
|
|
19228
|
+
isProcessAlive(pid) {
|
|
19229
|
+
try {
|
|
19230
|
+
process.kill(pid, 0);
|
|
19231
|
+
return true;
|
|
19232
|
+
} catch {
|
|
19233
|
+
return false;
|
|
19234
|
+
}
|
|
19235
|
+
}
|
|
19236
|
+
};
|
|
19237
|
+
|
|
18807
19238
|
// src/sandbox.ts
|
|
18808
19239
|
import { randomUUID } from "crypto";
|
|
18809
|
-
import { readFileSync as
|
|
18810
|
-
import { join as
|
|
19240
|
+
import { readFileSync as readFileSync5, readdirSync as readdirSync2 } from "fs";
|
|
19241
|
+
import { join as join6, relative as relative2 } from "path";
|
|
18811
19242
|
import { Sandbox } from "@vercel/sandbox";
|
|
18812
19243
|
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
18813
19244
|
"node_modules",
|
|
@@ -18823,13 +19254,13 @@ function collectFiles2(dir, base) {
|
|
|
18823
19254
|
for (const entry of readdirSync2(dir, { withFileTypes: true })) {
|
|
18824
19255
|
if (entry.name.startsWith(".") && SKIP_DIRS.has(entry.name)) continue;
|
|
18825
19256
|
if (SKIP_DIRS.has(entry.name)) continue;
|
|
18826
|
-
const fullPath =
|
|
19257
|
+
const fullPath = join6(dir, entry.name);
|
|
18827
19258
|
const relPath = relative2(base, fullPath);
|
|
18828
19259
|
if (entry.isDirectory()) {
|
|
18829
19260
|
files.push(...collectFiles2(fullPath, base));
|
|
18830
19261
|
} else if (entry.isFile()) {
|
|
18831
19262
|
if (SKIP_FILES.has(entry.name)) continue;
|
|
18832
|
-
files.push({ path: relPath, content:
|
|
19263
|
+
files.push({ path: relPath, content: readFileSync5(fullPath) });
|
|
18833
19264
|
}
|
|
18834
19265
|
}
|
|
18835
19266
|
return files;
|
|
@@ -18900,6 +19331,11 @@ async function waitForServer(baseUrl, token, devServer) {
|
|
|
18900
19331
|
async function deploySandbox(opts) {
|
|
18901
19332
|
const token = randomUUID();
|
|
18902
19333
|
const useGit = !!opts.git;
|
|
19334
|
+
const rootDir = opts.rootDir ?? null;
|
|
19335
|
+
const rootPrefix = rootDir ? `${rootDir}/` : "";
|
|
19336
|
+
if (rootDir) {
|
|
19337
|
+
console.log(` Monorepo root dir: ${rootDir}`);
|
|
19338
|
+
}
|
|
18903
19339
|
const timeoutMs = (opts.timeoutMinutes ?? 30) * 60 * 1e3;
|
|
18904
19340
|
const sourceOpts = opts.git ? {
|
|
18905
19341
|
source: {
|
|
@@ -19016,21 +19452,23 @@ async function deploySandbox(opts) {
|
|
|
19016
19452
|
const envLines = Object.entries(envMap).map(([k, v]) => `${k}=${v}`);
|
|
19017
19453
|
await sandbox.writeFiles([
|
|
19018
19454
|
{
|
|
19019
|
-
path:
|
|
19455
|
+
path: `${rootPrefix}.env`,
|
|
19020
19456
|
content: Buffer.from(envLines.join("\n"))
|
|
19021
19457
|
}
|
|
19022
19458
|
]);
|
|
19023
19459
|
const dots = startDots(" Installing dependencies");
|
|
19024
|
-
const
|
|
19460
|
+
const installCmd = rootDir ? `cd ${rootDir} && npm install` : "npm install";
|
|
19461
|
+
const install = await sandbox.runCommand("bash", ["-c", installCmd]);
|
|
19025
19462
|
dots.stop();
|
|
19026
19463
|
if (install.exitCode !== 0) {
|
|
19027
19464
|
const stderr = await install.stderr();
|
|
19028
19465
|
console.error(stderr);
|
|
19029
19466
|
throw new Error(`npm install failed (exit ${install.exitCode})`);
|
|
19030
19467
|
}
|
|
19468
|
+
const devCmd = rootDir ? `cd ${rootDir} && npm run dev` : "npm run dev";
|
|
19031
19469
|
const devServer = await sandbox.runCommand({
|
|
19032
|
-
cmd: "
|
|
19033
|
-
args: ["
|
|
19470
|
+
cmd: "bash",
|
|
19471
|
+
args: ["-c", devCmd],
|
|
19034
19472
|
detached: true
|
|
19035
19473
|
});
|
|
19036
19474
|
const baseUrl = sandbox.domain(5173);
|
|
@@ -19049,7 +19487,9 @@ async function deploySandbox(opts) {
|
|
|
19049
19487
|
sandboxId: sandbox.sandboxId,
|
|
19050
19488
|
mode: useGit ? "git" : "upload",
|
|
19051
19489
|
streamLogs: (opts2) => devServer.logs(opts2),
|
|
19052
|
-
stop: () =>
|
|
19490
|
+
stop: async () => {
|
|
19491
|
+
await sandbox.stop();
|
|
19492
|
+
}
|
|
19053
19493
|
};
|
|
19054
19494
|
} catch (err) {
|
|
19055
19495
|
await sandbox.stop().catch(() => {
|
|
@@ -19078,8 +19518,18 @@ function viagen(options) {
|
|
|
19078
19518
|
name: "viagen",
|
|
19079
19519
|
config(_, { mode }) {
|
|
19080
19520
|
const e = loadEnv(mode, process.cwd(), "");
|
|
19521
|
+
const serverConfig = {};
|
|
19081
19522
|
if (e["VIAGEN_AUTH_TOKEN"] || e["VIAGEN_USER_TOKEN"]) {
|
|
19082
|
-
|
|
19523
|
+
serverConfig.host = true;
|
|
19524
|
+
serverConfig.allowedHosts = true;
|
|
19525
|
+
}
|
|
19526
|
+
if (e["VIAGEN_APP_COMMAND"] && !process.env["__VIAGEN_CHILD"]) {
|
|
19527
|
+
const viagenPort = parseInt(e["VIAGEN_SERVER_PORT"] || "5199", 10);
|
|
19528
|
+
serverConfig.port = viagenPort;
|
|
19529
|
+
serverConfig.strictPort = true;
|
|
19530
|
+
}
|
|
19531
|
+
if (Object.keys(serverConfig).length > 0) {
|
|
19532
|
+
return { server: serverConfig };
|
|
19083
19533
|
}
|
|
19084
19534
|
},
|
|
19085
19535
|
configResolved(config2) {
|
|
@@ -19091,6 +19541,10 @@ function viagen(options) {
|
|
|
19091
19541
|
debug("init", "plugin initializing");
|
|
19092
19542
|
debug("init", `projectRoot: ${projectRoot}`);
|
|
19093
19543
|
debug("init", `mode: ${config2.mode}`);
|
|
19544
|
+
debug("init", `server port: ${config2.server.port}`);
|
|
19545
|
+
debug("init", `VIAGEN_APP_COMMAND: ${env["VIAGEN_APP_COMMAND"] || "(not set)"}`);
|
|
19546
|
+
debug("init", `VIAGEN_APP_PORT: ${env["VIAGEN_APP_PORT"] || "(not set)"}`);
|
|
19547
|
+
debug("init", `__VIAGEN_CHILD: ${process.env["__VIAGEN_CHILD"] || "no"}`);
|
|
19094
19548
|
debug("init", `ANTHROPIC_API_KEY: ${env["ANTHROPIC_API_KEY"] ? "set (" + env["ANTHROPIC_API_KEY"].slice(0, 8) + "...)" : "NOT SET"}`);
|
|
19095
19549
|
debug("init", `CLAUDE_ACCESS_TOKEN: ${env["CLAUDE_ACCESS_TOKEN"] ? "set" : "NOT SET"}`);
|
|
19096
19550
|
debug("init", `GITHUB_TOKEN: ${env["GITHUB_TOKEN"] ? "set" : "NOT SET"}`);
|
|
@@ -19103,10 +19557,10 @@ function viagen(options) {
|
|
|
19103
19557
|
debug("init", `ui: ${opts.ui}, overlay: ${opts.overlay}, position: ${opts.position}`);
|
|
19104
19558
|
logBuffer.init(projectRoot);
|
|
19105
19559
|
wrapLogger(config2.logger, logBuffer);
|
|
19106
|
-
const viagenDir =
|
|
19107
|
-
|
|
19108
|
-
|
|
19109
|
-
|
|
19560
|
+
const viagenDir = join7(projectRoot, ".viagen");
|
|
19561
|
+
mkdirSync4(viagenDir, { recursive: true });
|
|
19562
|
+
writeFileSync5(
|
|
19563
|
+
join7(viagenDir, "config.json"),
|
|
19110
19564
|
JSON.stringify({
|
|
19111
19565
|
sandboxFiles: options?.sandboxFiles ?? [],
|
|
19112
19566
|
editable: options?.editable ?? []
|
|
@@ -19182,11 +19636,12 @@ ${payload.err.frame || ""}`
|
|
|
19182
19636
|
}
|
|
19183
19637
|
const platformToken = env["VIAGEN_USER_TOKEN"] || env["VIAGEN_AUTH_TOKEN"];
|
|
19184
19638
|
const platformUrl = env["VIAGEN_PLATFORM_URL"] || "https://app.viagen.dev";
|
|
19185
|
-
const
|
|
19639
|
+
const projectId = env["VIAGEN_PROJECT_ID"];
|
|
19186
19640
|
let viagenClient = null;
|
|
19641
|
+
const orgId = env["VIAGEN_ORG_ID"];
|
|
19187
19642
|
if (platformToken) {
|
|
19188
|
-
viagenClient = createViagen({ token: platformToken, baseUrl: platformUrl });
|
|
19189
|
-
debug("server", `platform client created (baseUrl: ${platformUrl})`);
|
|
19643
|
+
viagenClient = createViagen({ token: platformToken, baseUrl: platformUrl, orgId });
|
|
19644
|
+
debug("server", `platform client created (baseUrl: ${platformUrl}, orgId: ${orgId || "none \u2014 will use default org"})`);
|
|
19190
19645
|
}
|
|
19191
19646
|
const hasEditor = !!(options?.editable && options.editable.length > 0);
|
|
19192
19647
|
const clientJs = buildClientScript({
|
|
@@ -19206,9 +19661,11 @@ ${payload.err.frame || ""}`
|
|
|
19206
19661
|
res.writeHead(302, { Location: "/?_viagen_chat" });
|
|
19207
19662
|
res.end();
|
|
19208
19663
|
});
|
|
19209
|
-
server.middlewares.use("/via/iframe", (
|
|
19664
|
+
server.middlewares.use("/via/iframe", (req, res) => {
|
|
19665
|
+
const url2 = new URL(req.url || "/", "http://localhost");
|
|
19666
|
+
const appUrl = url2.searchParams.get("appUrl") || void 0;
|
|
19210
19667
|
res.setHeader("Content-Type", "text/html");
|
|
19211
|
-
res.end(buildIframeHtml({ panelWidth: opts.panelWidth }));
|
|
19668
|
+
res.end(buildIframeHtml({ panelWidth: opts.panelWidth, appUrl }));
|
|
19212
19669
|
});
|
|
19213
19670
|
if (previewEnabled) {
|
|
19214
19671
|
const previewJs = buildPreviewScript();
|
|
@@ -19222,9 +19679,9 @@ ${payload.err.frame || ""}`
|
|
|
19222
19679
|
res.end(JSON.stringify({ error: "Method not allowed" }));
|
|
19223
19680
|
return;
|
|
19224
19681
|
}
|
|
19225
|
-
if (!viagenClient || !
|
|
19682
|
+
if (!viagenClient || !projectId) {
|
|
19226
19683
|
res.writeHead(503, { "Content-Type": "application/json" });
|
|
19227
|
-
res.end(JSON.stringify({ error: "Task creation not configured: missing platform credentials or
|
|
19684
|
+
res.end(JSON.stringify({ error: "Task creation not configured: missing platform credentials or VIAGEN_PROJECT_ID" }));
|
|
19228
19685
|
return;
|
|
19229
19686
|
}
|
|
19230
19687
|
let body = "";
|
|
@@ -19246,7 +19703,7 @@ Page URL: ${pageUrl}`);
|
|
|
19246
19703
|
if (screenshot) parts.push("\n[Screenshot attached]");
|
|
19247
19704
|
parts.push("\n\n[Submitted via viagen preview feedback]");
|
|
19248
19705
|
const fullPrompt = parts.join("");
|
|
19249
|
-
const task = await viagenClient.tasks.create(
|
|
19706
|
+
const task = await viagenClient.tasks.create(projectId, { prompt: fullPrompt, type: "task" });
|
|
19250
19707
|
if (screenshot && task.id) {
|
|
19251
19708
|
try {
|
|
19252
19709
|
const [header, b64] = screenshot.split(",");
|
|
@@ -19254,7 +19711,7 @@ Page URL: ${pageUrl}`);
|
|
|
19254
19711
|
const binary = Buffer.from(b64, "base64");
|
|
19255
19712
|
const blob = new Blob([binary], { type: mime });
|
|
19256
19713
|
const ext = mime === "image/png" ? "png" : "jpeg";
|
|
19257
|
-
await viagenClient.tasks.addAttachment(
|
|
19714
|
+
await viagenClient.tasks.addAttachment(projectId, task.id, blob, `screenshot.${ext}`);
|
|
19258
19715
|
debug("preview", `screenshot attached to task ${task.id}`);
|
|
19259
19716
|
} catch (attachErr) {
|
|
19260
19717
|
debug("preview", `screenshot attachment failed (non-fatal): ${attachErr}`);
|
|
@@ -19276,13 +19733,31 @@ Page URL: ${pageUrl}`);
|
|
|
19276
19733
|
});
|
|
19277
19734
|
const resolvedModel = env["VIAGEN_MODEL"] || opts.model;
|
|
19278
19735
|
debug("server", `creating ChatSession (model: ${resolvedModel})`);
|
|
19736
|
+
let processManager;
|
|
19737
|
+
const appCommand = env["VIAGEN_APP_COMMAND"];
|
|
19738
|
+
const isChildProcess = process.env["__VIAGEN_CHILD"] === "1";
|
|
19739
|
+
if (isChildProcess) {
|
|
19740
|
+
debug("server", "skipping process manager (running as child process)");
|
|
19741
|
+
} else if (appCommand) {
|
|
19742
|
+
const appPort = parseInt(env["VIAGEN_APP_PORT"] || "5173", 10);
|
|
19743
|
+
debug("server", `preview process manager enabled: "${appCommand}" on port ${appPort}`);
|
|
19744
|
+
logBuffer.push("info", `[viagen] Preview process manager: ${appCommand} (port ${appPort})`);
|
|
19745
|
+
processManager = new ProcessManager({
|
|
19746
|
+
command: appCommand,
|
|
19747
|
+
cwd: projectRoot,
|
|
19748
|
+
logBuffer,
|
|
19749
|
+
appPort
|
|
19750
|
+
});
|
|
19751
|
+
processManager.start();
|
|
19752
|
+
}
|
|
19279
19753
|
let mcpServers;
|
|
19280
|
-
const hasPlatformContext = !!(viagenClient &&
|
|
19754
|
+
const hasPlatformContext = !!(viagenClient && projectId);
|
|
19281
19755
|
if (hasPlatformContext) {
|
|
19282
19756
|
debug("server", "creating viagen MCP tools (platform connected)");
|
|
19283
19757
|
const viagenMcp = createViagenTools({
|
|
19284
19758
|
client: viagenClient,
|
|
19285
|
-
|
|
19759
|
+
projectId,
|
|
19760
|
+
processManager
|
|
19286
19761
|
});
|
|
19287
19762
|
mcpServers = { [viagenMcp.name]: viagenMcp };
|
|
19288
19763
|
}
|
|
@@ -19296,7 +19771,12 @@ Page URL: ${pageUrl}`);
|
|
|
19296
19771
|
debug("server", "plan mode active \u2014 restricting tools");
|
|
19297
19772
|
systemPrompt = PLAN_SYSTEM_PROMPT;
|
|
19298
19773
|
} else if (hasPlatformContext) {
|
|
19299
|
-
systemPrompt = (systemPrompt || "") +
|
|
19774
|
+
systemPrompt = (systemPrompt || "") + buildTaskToolsPrompt({
|
|
19775
|
+
projectId,
|
|
19776
|
+
taskId: env["VIAGEN_TASK_ID"] || void 0,
|
|
19777
|
+
branch: env["VIAGEN_BRANCH"] || void 0,
|
|
19778
|
+
hasProcessManager: !!processManager
|
|
19779
|
+
});
|
|
19300
19780
|
}
|
|
19301
19781
|
const chatSession = new ChatSession({
|
|
19302
19782
|
env,
|
|
@@ -19314,7 +19794,7 @@ Page URL: ${pageUrl}`);
|
|
|
19314
19794
|
registerChatRoutes(server, chatSession, {
|
|
19315
19795
|
env,
|
|
19316
19796
|
viagenClient: viagenClient ?? void 0,
|
|
19317
|
-
|
|
19797
|
+
projectId
|
|
19318
19798
|
});
|
|
19319
19799
|
if (hasEditor) {
|
|
19320
19800
|
registerFileRoutes(server, {
|
|
@@ -19334,8 +19814,8 @@ Page URL: ${pageUrl}`);
|
|
|
19334
19814
|
debug("server", "auto-prompt completed");
|
|
19335
19815
|
logBuffer.push("info", `[viagen] Prompt completed`);
|
|
19336
19816
|
const currentTaskId = env["VIAGEN_TASK_ID"];
|
|
19337
|
-
if (viagenClient &&
|
|
19338
|
-
viagenClient.tasks.update(
|
|
19817
|
+
if (viagenClient && projectId && currentTaskId && (event.inputTokens || event.outputTokens)) {
|
|
19818
|
+
viagenClient.tasks.update(projectId, currentTaskId, {
|
|
19339
19819
|
...event.inputTokens != null && { inputTokens: event.inputTokens },
|
|
19340
19820
|
...event.outputTokens != null && { outputTokens: event.outputTokens }
|
|
19341
19821
|
}).catch((err) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "viagen",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Vite dev server plugin that exposes endpoints for chatting with Claude Code SDK",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -45,11 +45,11 @@
|
|
|
45
45
|
"agent-browser": "^0.20.12",
|
|
46
46
|
"lucide-react": "^0.564.0",
|
|
47
47
|
"simple-git": "^3.31.1",
|
|
48
|
-
"viagen-sdk": "^0.1.
|
|
48
|
+
"viagen-sdk": "^0.1.2"
|
|
49
49
|
},
|
|
50
50
|
"license": "MIT",
|
|
51
51
|
"repository": {
|
|
52
52
|
"type": "git",
|
|
53
|
-
"url": "https://github.com/viagen-dev/viagen"
|
|
53
|
+
"url": "https://github.com/viagen-dev/viagen-sdk"
|
|
54
54
|
}
|
|
55
55
|
}
|
package/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2025 Ben Ipsen
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|