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 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: ".env",
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 install = await sandbox2.runCommand("npm", ["install"]);
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: "npm",
236
- args: ["run", "dev"],
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: () => sandbox2.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: false
974
- // can't know from env, assume clean
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 (!envRemoteUrl && gitInfo.isDirty && !branchOverride) {
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 mkdirSync3, writeFileSync as writeFileSync4 } from "fs";
10
- import { join as join6 } from "path";
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.0.57" : "0.0.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.environmentId) {
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.environmentId, taskId, {
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/environments/' + data.environmentId + '/tasks/' + data.taskId;
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: #ffffff; overflow: hidden; }
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
- <iframe id="app-frame" src="/?_viagen_embed=1"></iframe>
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" src="/via/ui"></iframe>
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
- document.getElementById('chat-frame').contentWindow.postMessage(ev.data, '*');
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(302, { Location: cleanUrl });
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(302, { Location: cleanUrl });
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, environmentId } = config2;
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(environmentId, id, {
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 environment. Optionally filter by status.",
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
- const tasks = await client.tasks.list(environmentId, args.status);
18712
- return {
18713
- content: [
18714
- {
18715
- type: "text",
18716
- text: JSON.stringify(tasks, null, 2)
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
- const task = await client.tasks.get(environmentId, args.taskId);
18730
- return {
18731
- content: [
18732
- {
18733
- type: "text",
18734
- text: JSON.stringify(task, null, 2)
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 environment. Use this to create follow-up work.",
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
- const task = await client.tasks.create(environmentId, {
18750
- prompt: args.prompt,
18751
- branch: args.branch,
18752
- type: args.type
18753
- });
18754
- return {
18755
- content: [
18756
- {
18757
- type: "text",
18758
- text: JSON.stringify(task, null, 2)
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
- var TASK_TOOLS_PROMPT = `
18795
- You have access to viagen platform tools for task management:
18796
- - viagen_list_tasks: List tasks in this environment (optionally filter by status)
18797
- - viagen_get_task: Get full details of a specific task
18798
- - viagen_create_task: Create follow-up tasks for work you identify
18799
- - viagen_update_task: Update a task's status ('review' or 'completed'). Accepts an optional taskId \u2014 defaults to the current task if one is set.
18800
-
18801
- Use these to understand environment context and create follow-up work when appropriate.
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 readFileSync4, readdirSync as readdirSync2 } from "fs";
18810
- import { join as join5, relative as relative2 } from "path";
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 = join5(dir, entry.name);
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: readFileSync4(fullPath) });
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: ".env",
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 install = await sandbox.runCommand("npm", ["install"]);
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: "npm",
19033
- args: ["run", "dev"],
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: () => sandbox.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
- return { server: { host: true, allowedHosts: true } };
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 = join6(projectRoot, ".viagen");
19107
- mkdirSync3(viagenDir, { recursive: true });
19108
- writeFileSync4(
19109
- join6(viagenDir, "config.json"),
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 environmentId = env["VIAGEN_ENVIRONMENT_ID"];
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", (_req, res) => {
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 || !environmentId) {
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 VIAGEN_ENVIRONMENT_ID" }));
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(environmentId, { prompt: fullPrompt, type: "task" });
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(environmentId, task.id, blob, `screenshot.${ext}`);
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 && environmentId);
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
- environmentId
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 || "") + TASK_TOOLS_PROMPT;
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
- environmentId
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 && environmentId && currentTaskId && (event.inputTokens || event.outputTokens)) {
19338
- viagenClient.tasks.update(environmentId, currentTaskId, {
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.0.57",
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.0"
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.