viagen 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,189 @@
1
+ # viagen
2
+
3
+ A Vite dev server plugin and CLI tool that enables you to use Claude Code in a sandbox — instantly.
4
+
5
+ ## Prerequisites
6
+
7
+ - [Claude](https://claude.ai/signup) — Max, Pro, or API plan. The setup wizard handles auth.
8
+ - [Vercel](https://vercel.com/signup) — Free plan works. Sandboxes last 45 min on Hobby, 5 hours on Pro.
9
+ - [GitHub CLI](https://cli.github.com) — Enables git clone and push from sandboxes.
10
+
11
+ ## Quick Setup (Claude Code Plugin)
12
+
13
+ ```
14
+ /plugin marketplace add viagen-dev/viagen-claude-plugin
15
+ /plugin install viagen@viagen-marketplace
16
+ ```
17
+
18
+ **Restart Claude Code to load the plugin.**
19
+
20
+ ```
21
+ /viagen-install
22
+ ```
23
+
24
+ The plugin will handle npm installation, vite config updates, and run the setup wizard for you.
25
+
26
+ ## Manual Setup
27
+
28
+ ### Step 1 — Add viagen to your app
29
+
30
+ ```bash
31
+ npm install --save-dev viagen
32
+ ```
33
+
34
+ ```ts
35
+ // vite.config.ts
36
+ import { defineConfig } from 'vite'
37
+ import { viagen } from 'viagen'
38
+
39
+ export default defineConfig({
40
+ plugins: [viagen()],
41
+ })
42
+ ```
43
+
44
+ ### Step 2 — Setup
45
+
46
+ ```bash
47
+ npx viagen setup
48
+ ```
49
+
50
+ The setup wizard authenticates with Claude, detects your GitHub and Vercel credentials, and captures your git remote info — all written to your local `.env`. This ensures sandboxes clone the correct repo instead of inferring it at runtime.
51
+
52
+ You can now run `npm run dev` to start the local dev server. At this point you can launch viagen and chat with Claude to make changes to your app.
53
+
54
+ ### Step 3 — Sandbox
55
+
56
+ ```bash
57
+ npx viagen sandbox
58
+ ```
59
+
60
+ Deploys your dev server to a remote Vercel Sandbox — an isolated VM-like environment where Claude can read, write, and push code.
61
+
62
+ ```bash
63
+ # Deploy on a specific branch
64
+ npx viagen sandbox --branch feature/my-thing
65
+
66
+ # Set a longer timeout (default: 30 min)
67
+ npx viagen sandbox --timeout 60
68
+
69
+ # Auto-send a prompt on load
70
+ npx viagen sandbox --prompt "build me a landing page"
71
+
72
+ # Stop a running sandbox
73
+ npx viagen sandbox stop <sandboxId>
74
+ ```
75
+
76
+
77
+ ## Plugin Options
78
+
79
+ ```ts
80
+ viagen({
81
+ position: 'bottom-right', // toggle button position
82
+ model: 'sonnet', // claude model
83
+ panelWidth: 375, // chat panel width in px
84
+ overlay: true, // fix button on error overlay
85
+ ui: true, // inject chat panel into pages
86
+ sandboxFiles: [...], // copy files manually into sandbox
87
+ systemPrompt: '...', // custom system prompt (see below)
88
+ editable: ['src','conf'], // files/dirs editable in the UI
89
+ mcpServers: { ... }, // additional MCP servers for Claude
90
+ })
91
+ ```
92
+
93
+
94
+ ### Custom MCP Servers
95
+
96
+ Pass additional [MCP server](https://modelcontextprotocol.io) configurations to give Claude access to custom tools:
97
+
98
+ ```ts
99
+ viagen({
100
+ mcpServers: {
101
+ 'my-db': {
102
+ command: 'npx',
103
+ args: ['-y', '@my-org/db-mcp-server'],
104
+ env: { DATABASE_URL: process.env.DATABASE_URL },
105
+ },
106
+ },
107
+ })
108
+ ```
109
+
110
+ These are merged with viagen's built-in platform tools (when connected). User-provided servers take precedence if names collide.
111
+
112
+ ### Editable Files
113
+
114
+ Add a file editor panel to the chat UI:
115
+
116
+ ```ts
117
+ viagen({
118
+ editable: ['src/components', 'vite.config.ts']
119
+ })
120
+ ```
121
+
122
+ Paths can be files or directories (directories include all files within). The editor appears as a "Files" tab in the chat panel with a collapsible directory tree, syntax highlighting (TypeScript, JavaScript, CSS, HTML, JSON, Markdown), and image preview.
123
+
124
+ The default system prompt tells Claude it's embedded in a Vite dev server, that file edits trigger HMR, and how to check server logs. Recent build errors are automatically appended to give Claude context about what went wrong.
125
+
126
+ To customize the prompt, you can replace it entirely or extend the default:
127
+
128
+ ```ts
129
+ import { viagen, DEFAULT_SYSTEM_PROMPT } from 'viagen'
130
+
131
+ viagen({
132
+ // Replace entirely
133
+ systemPrompt: 'You are a React expert. Only use TypeScript.',
134
+
135
+ // Or extend the default
136
+ systemPrompt: DEFAULT_SYSTEM_PROMPT + '\nAlways use Tailwind for styling.',
137
+ })
138
+ ```
139
+
140
+ ## API
141
+
142
+ Every viagen endpoint is available as an API. Build your own UI, integrate with CI, or script Claude from the command line.
143
+
144
+ ```
145
+ POST /via/chat — send a message, streamed SSE response
146
+ POST /via/chat/reset — clear conversation history
147
+ GET /via/health — check API key status
148
+ GET /via/error — latest build error (if any)
149
+ GET /via/ui — standalone chat interface
150
+ GET /via/iframe — split view (app + chat side by side)
151
+ GET /via/files — list editable files (when configured)
152
+ GET /via/file?path= — read file content
153
+ POST /via/file — write file content { path, content }
154
+ GET /via/file/raw — serve raw file (images, etc.) with correct MIME type
155
+ GET /via/git/status — list changed files (git status)
156
+ GET /via/git/diff — full diff, or single file with ?path=
157
+ GET /via/git/branch — current branch, remote URL, open PR info
158
+ GET /via/logs — dev server log entries, optional ?since=<timestamp>
159
+ ```
160
+
161
+ When `VIAGEN_AUTH_TOKEN` is set (always on in sandboxes), pass the token as a `Bearer` header, a `/t/:token` path segment, or a `?token=` query param.
162
+
163
+ ```bash
164
+ # With curl
165
+ curl -X POST http://localhost:5173/via/chat \
166
+ -H "Authorization: Bearer $VIAGEN_AUTH_TOKEN" \
167
+ -H "Content-Type: application/json" \
168
+ -d '{"message": "add a hello world route"}'
169
+
170
+ # Or pass the token in the URL path (sets a session cookie)
171
+ open "http://localhost:5173/via/ui/t/$VIAGEN_AUTH_TOKEN"
172
+
173
+ # ?token= query param also works (fallback for backwards compat)
174
+ open "http://localhost:5173/via/ui?token=$VIAGEN_AUTH_TOKEN"
175
+ ```
176
+
177
+ ## Development
178
+
179
+ ```bash
180
+ npm install
181
+ npm run dev # Dev server (site)
182
+ npm run build # Build with tsup
183
+ npm run test # Run tests
184
+ npm run typecheck # Type check
185
+ ```
186
+
187
+ ## License
188
+
189
+ [MIT](LICENSE)
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);
@@ -972,8 +979,13 @@ async function sandbox(args, options) {
972
979
  branch: envBranch || "main",
973
980
  userName: envUserName || "viagen",
974
981
  userEmail: envUserEmail || "noreply@viagen.dev",
975
- isDirty: false
976
- // 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
+ })()
977
989
  } : getGitInfo(cwd);
978
990
  if (envRemoteUrl) {
979
991
  console.log("Using git info from .env");
@@ -1013,7 +1025,7 @@ async function sandbox(args, options) {
1013
1025
  userEmail: gitInfo.userEmail,
1014
1026
  token: githubToken
1015
1027
  });
1016
- if (!envRemoteUrl && gitInfo.isDirty && !branchOverride) {
1028
+ if (gitInfo.isDirty && !branchOverride) {
1017
1029
  console.log("");
1018
1030
  console.log("Your working tree has uncommitted changes.");
1019
1031
  console.log("");
@@ -1072,6 +1084,9 @@ async function sandbox(args, options) {
1072
1084
  console.log(` Repo: ${deployGit.remoteUrl}`);
1073
1085
  console.log(` Branch: ${deployGit.branch}`);
1074
1086
  }
1087
+ if (env["SANDBOX_ROOT_DIR"]) {
1088
+ console.log(` Root: ${env["SANDBOX_ROOT_DIR"]}`);
1089
+ }
1075
1090
  const frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
1076
1091
  let frameIdx = 0;
1077
1092
  const spinner = setInterval(() => {
@@ -1095,7 +1110,8 @@ async function sandbox(args, options) {
1095
1110
  } : void 0,
1096
1111
  fling: existsSync(join2(homedir(), ".fling", "token")) ? { token: readFileSync2(join2(homedir(), ".fling", "token"), "utf-8").trim() } : void 0,
1097
1112
  timeoutMinutes,
1098
- prompt
1113
+ prompt,
1114
+ rootDir: env["SANDBOX_ROOT_DIR"] || void 0
1099
1115
  });
1100
1116
  clearInterval(spinner);
1101
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
@@ -111,7 +111,7 @@ function registerHealthRoutes(server, env, errorRef) {
111
111
  );
112
112
  }
113
113
  });
114
- const currentVersion = true ? "0.1.2" : "0.0.0";
114
+ const currentVersion = true ? "0.1.4" : "0.0.0";
115
115
  debug("health", `version resolved: ${currentVersion}`);
116
116
  let versionCache = null;
117
117
  server.middlewares.use("/via/version", (_req, res) => {
@@ -4200,15 +4200,17 @@ function buildUiHtml(opts) {
4200
4200
  // src/iframe.ts
4201
4201
  function buildIframeHtml(opts) {
4202
4202
  const pw = opts.panelWidth;
4203
+ const appSrc = opts.appUrl ?? "/?_viagen_embed=1";
4203
4204
  return `<!DOCTYPE html>
4204
4205
  <html lang="en">
4205
4206
  <head>
4206
4207
  <meta charset="UTF-8">
4207
4208
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
4209
+ <meta name="color-scheme" content="light dark">
4208
4210
  <title>viagen</title>
4209
4211
  <style>
4210
4212
  * { margin: 0; padding: 0; box-sizing: border-box; }
4211
- body { display: flex; height: 100vh; background: #ffffff; overflow: hidden; }
4213
+ body { display: flex; height: 100vh; background: #f5f5f5; overflow: hidden; }
4212
4214
  #app-frame { flex: 1; border: none; height: 100%; min-width: 200px; }
4213
4215
  #divider {
4214
4216
  width: 1px;
@@ -4230,24 +4232,67 @@ function buildIframeHtml(opts) {
4230
4232
  #divider:hover, #divider.active { background: #a3a3a3; }
4231
4233
  #chat-frame { width: ${pw}px; border: none; height: 100%; min-width: 280px; background: #ffffff; }
4232
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
+ }
4233
4254
  </style>
4234
4255
  </head>
4235
4256
  <body>
4236
- <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>
4237
4259
  <div id="divider"></div>
4238
- <iframe id="chat-frame" src="/via/ui"></iframe>
4260
+ <iframe id="chat-frame"></iframe>
4239
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
+
4240
4290
  // Relay postMessage from app iframe to chat iframe (e.g. "Fix This Error")
4241
4291
  window.addEventListener('message', function(ev) {
4242
4292
  if (ev.data && ev.data.type === 'viagen:send') {
4243
- document.getElementById('chat-frame').contentWindow.postMessage(ev.data, '*');
4293
+ chatFrame.contentWindow.postMessage(ev.data, '*');
4244
4294
  }
4245
4295
  });
4246
- // Tell chat iframe it's in split-view mode (hides pop-out button)
4247
- var chatFrame = document.getElementById('chat-frame');
4248
- chatFrame.addEventListener('load', function() {
4249
- chatFrame.contentWindow.postMessage({ type: 'viagen:context', iframe: true }, '*');
4250
- });
4251
4296
 
4252
4297
  // Drag-resizable divider
4253
4298
  var divider = document.getElementById('divider');
@@ -4293,21 +4338,26 @@ function buildWaitPage(targetUrl) {
4293
4338
  <html><head><meta charset="utf-8"><title>Loading\u2026</title>
4294
4339
  <style>
4295
4340
  body { margin: 0; display: flex; align-items: center; justify-content: center;
4296
- height: 100vh; background: #0a0a0a; color: #888; font-family: system-ui; }
4297
- .spinner { width: 20px; height: 20px; border: 2px solid #333; border-top-color: #f97316;
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;
4298
4343
  border-radius: 50%; animation: spin .6s linear infinite; margin-right: 12px; }
4299
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
+ }
4300
4349
  </style></head>
4301
4350
  <body><div class="spinner"></div><span>Starting dev server\u2026</span>
4302
4351
  <script>
4303
4352
  (async () => {
4304
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.
4305
4357
  for (let i = 0; i < 60; i++) {
4306
4358
  try {
4307
- const r = await fetch(target, { credentials: 'same-origin' });
4308
- const text = await r.text();
4309
- // Vite is ready when we get a non-empty HTML response
4310
- if (r.ok && text.length > 0) { window.location.replace(target); return; }
4359
+ const r = await fetch('/@vite/client', { credentials: 'same-origin' });
4360
+ if (r.ok) { window.location.replace(target); return; }
4311
4361
  } catch {}
4312
4362
  await new Promise(r => setTimeout(r, 500));
4313
4363
  }
@@ -18686,8 +18736,9 @@ import {
18686
18736
  tool
18687
18737
  } from "@anthropic-ai/claude-agent-sdk";
18688
18738
  function createViagenTools(config2) {
18689
- const { client, projectId } = config2;
18739
+ const { client, projectId, processManager } = config2;
18690
18740
  const taskId = process.env["VIAGEN_TASK_ID"];
18741
+ debug("tools", `MCP tools created (projectId: ${projectId}, taskId: ${taskId || "none"})`);
18691
18742
  const tools = [
18692
18743
  tool(
18693
18744
  "viagen_update_task",
@@ -18703,6 +18754,7 @@ function createViagenTools(config2) {
18703
18754
  prReviewStatus: external_exports.string().optional().describe("PR review outcome \u2014 e.g. 'pass', 'flag', or 'fail'.")
18704
18755
  },
18705
18756
  async (args) => {
18757
+ debug("tools", `viagen_update_task called (taskId: ${args.taskId || taskId || "none"}, status: ${args.status || "none"}, projectId: ${projectId})`);
18706
18758
  const id = args.taskId || taskId;
18707
18759
  if (!id) {
18708
18760
  return {
@@ -18724,6 +18776,9 @@ function createViagenTools(config2) {
18724
18776
  };
18725
18777
  } catch (err) {
18726
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})` : ""}`);
18727
18782
  return {
18728
18783
  content: [{ type: "text", text: `Error updating task: ${message}` }]
18729
18784
  };
@@ -18737,15 +18792,25 @@ function createViagenTools(config2) {
18737
18792
  status: external_exports.enum(["ready", "running", "validating", "completed", "timed_out"]).optional().describe("Filter tasks by status.")
18738
18793
  },
18739
18794
  async (args) => {
18740
- const tasks = await client.tasks.list(projectId, args.status);
18741
- return {
18742
- content: [
18743
- {
18744
- type: "text",
18745
- text: JSON.stringify(tasks, null, 2)
18746
- }
18747
- ]
18748
- };
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
+ }
18749
18814
  }
18750
18815
  ),
18751
18816
  tool(
@@ -18755,15 +18820,26 @@ function createViagenTools(config2) {
18755
18820
  taskId: external_exports.string().describe("The task ID to retrieve.")
18756
18821
  },
18757
18822
  async (args) => {
18758
- const task = await client.tasks.get(projectId, args.taskId);
18759
- return {
18760
- content: [
18761
- {
18762
- type: "text",
18763
- text: JSON.stringify(task, null, 2)
18764
- }
18765
- ]
18766
- };
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
+ }
18767
18843
  }
18768
18844
  ),
18769
18845
  tool(
@@ -18775,22 +18851,58 @@ function createViagenTools(config2) {
18775
18851
  type: external_exports.enum(["task", "plan"]).optional().describe("Task type: 'task' for code changes, 'plan' for implementation plans.")
18776
18852
  },
18777
18853
  async (args) => {
18778
- const task = await client.tasks.create(projectId, {
18779
- prompt: args.prompt,
18780
- branch: args.branch,
18781
- type: args.type
18782
- });
18783
- return {
18784
- content: [
18785
- {
18786
- type: "text",
18787
- text: JSON.stringify(task, null, 2)
18788
- }
18789
- ]
18790
- };
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
+ }
18791
18879
  }
18792
18880
  )
18793
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
+ }
18794
18906
  return createSdkMcpServer({ name: "viagen", tools });
18795
18907
  }
18796
18908
  var PLAN_MODE_DISALLOWED_TOOLS = ["Edit", "NotebookEdit"];
@@ -18820,23 +18932,313 @@ Constraints:
18820
18932
  - Only create new files inside the plans/ directory.
18821
18933
  - Your plan should include: context, proposed changes (with file paths and descriptions), implementation order, and potential risks.
18822
18934
  `;
18823
- var TASK_TOOLS_PROMPT = `
18824
- You have access to viagen platform tools for task management:
18825
- - viagen_list_tasks: List tasks in this project (optionally filter by status)
18826
- - viagen_get_task: Get full details of a specific task
18827
- - viagen_create_task: Create follow-up tasks for work you identify
18828
- - 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.
18829
-
18830
- Use these to understand project context and create follow-up work when appropriate.
18831
- `;
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" });
18832
18968
 
18833
18969
  // src/index.ts
18834
18970
  import { createViagen } from "viagen-sdk";
18835
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
+
18836
19238
  // src/sandbox.ts
18837
19239
  import { randomUUID } from "crypto";
18838
- import { readFileSync as readFileSync4, readdirSync as readdirSync2 } from "fs";
18839
- 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";
18840
19242
  import { Sandbox } from "@vercel/sandbox";
18841
19243
  var SKIP_DIRS = /* @__PURE__ */ new Set([
18842
19244
  "node_modules",
@@ -18852,13 +19254,13 @@ function collectFiles2(dir, base) {
18852
19254
  for (const entry of readdirSync2(dir, { withFileTypes: true })) {
18853
19255
  if (entry.name.startsWith(".") && SKIP_DIRS.has(entry.name)) continue;
18854
19256
  if (SKIP_DIRS.has(entry.name)) continue;
18855
- const fullPath = join5(dir, entry.name);
19257
+ const fullPath = join6(dir, entry.name);
18856
19258
  const relPath = relative2(base, fullPath);
18857
19259
  if (entry.isDirectory()) {
18858
19260
  files.push(...collectFiles2(fullPath, base));
18859
19261
  } else if (entry.isFile()) {
18860
19262
  if (SKIP_FILES.has(entry.name)) continue;
18861
- files.push({ path: relPath, content: readFileSync4(fullPath) });
19263
+ files.push({ path: relPath, content: readFileSync5(fullPath) });
18862
19264
  }
18863
19265
  }
18864
19266
  return files;
@@ -18929,6 +19331,11 @@ async function waitForServer(baseUrl, token, devServer) {
18929
19331
  async function deploySandbox(opts) {
18930
19332
  const token = randomUUID();
18931
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
+ }
18932
19339
  const timeoutMs = (opts.timeoutMinutes ?? 30) * 60 * 1e3;
18933
19340
  const sourceOpts = opts.git ? {
18934
19341
  source: {
@@ -19045,21 +19452,23 @@ async function deploySandbox(opts) {
19045
19452
  const envLines = Object.entries(envMap).map(([k, v]) => `${k}=${v}`);
19046
19453
  await sandbox.writeFiles([
19047
19454
  {
19048
- path: ".env",
19455
+ path: `${rootPrefix}.env`,
19049
19456
  content: Buffer.from(envLines.join("\n"))
19050
19457
  }
19051
19458
  ]);
19052
19459
  const dots = startDots(" Installing dependencies");
19053
- 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]);
19054
19462
  dots.stop();
19055
19463
  if (install.exitCode !== 0) {
19056
19464
  const stderr = await install.stderr();
19057
19465
  console.error(stderr);
19058
19466
  throw new Error(`npm install failed (exit ${install.exitCode})`);
19059
19467
  }
19468
+ const devCmd = rootDir ? `cd ${rootDir} && npm run dev` : "npm run dev";
19060
19469
  const devServer = await sandbox.runCommand({
19061
- cmd: "npm",
19062
- args: ["run", "dev"],
19470
+ cmd: "bash",
19471
+ args: ["-c", devCmd],
19063
19472
  detached: true
19064
19473
  });
19065
19474
  const baseUrl = sandbox.domain(5173);
@@ -19109,8 +19518,23 @@ function viagen(options) {
19109
19518
  name: "viagen",
19110
19519
  config(_, { mode }) {
19111
19520
  const e = loadEnv(mode, process.cwd(), "");
19112
- if (e["VIAGEN_AUTH_TOKEN"] || e["VIAGEN_USER_TOKEN"]) {
19113
- return { server: { host: true, allowedHosts: true } };
19521
+ const serverConfig = {};
19522
+ if (e["VIAGEN_AUTH_TOKEN"] || e["VIAGEN_USER_TOKEN"] || e["VIAGEN_PROMPT"]) {
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
+ const childAppPort = e["VIAGEN_APP_PORT"] || process.env["VIAGEN_APP_PORT"] || process.env["PORT"];
19532
+ if (process.env["__VIAGEN_CHILD"] && childAppPort) {
19533
+ serverConfig.port = parseInt(childAppPort, 10);
19534
+ serverConfig.strictPort = true;
19535
+ }
19536
+ if (Object.keys(serverConfig).length > 0) {
19537
+ return { server: serverConfig };
19114
19538
  }
19115
19539
  },
19116
19540
  configResolved(config2) {
@@ -19122,6 +19546,10 @@ function viagen(options) {
19122
19546
  debug("init", "plugin initializing");
19123
19547
  debug("init", `projectRoot: ${projectRoot}`);
19124
19548
  debug("init", `mode: ${config2.mode}`);
19549
+ debug("init", `server port: ${config2.server.port}`);
19550
+ debug("init", `VIAGEN_APP_COMMAND: ${env["VIAGEN_APP_COMMAND"] || "(not set)"}`);
19551
+ debug("init", `VIAGEN_APP_PORT: ${env["VIAGEN_APP_PORT"] || "(not set)"}`);
19552
+ debug("init", `__VIAGEN_CHILD: ${process.env["__VIAGEN_CHILD"] || "no"}`);
19125
19553
  debug("init", `ANTHROPIC_API_KEY: ${env["ANTHROPIC_API_KEY"] ? "set (" + env["ANTHROPIC_API_KEY"].slice(0, 8) + "...)" : "NOT SET"}`);
19126
19554
  debug("init", `CLAUDE_ACCESS_TOKEN: ${env["CLAUDE_ACCESS_TOKEN"] ? "set" : "NOT SET"}`);
19127
19555
  debug("init", `GITHUB_TOKEN: ${env["GITHUB_TOKEN"] ? "set" : "NOT SET"}`);
@@ -19134,10 +19562,10 @@ function viagen(options) {
19134
19562
  debug("init", `ui: ${opts.ui}, overlay: ${opts.overlay}, position: ${opts.position}`);
19135
19563
  logBuffer.init(projectRoot);
19136
19564
  wrapLogger(config2.logger, logBuffer);
19137
- const viagenDir = join6(projectRoot, ".viagen");
19138
- mkdirSync3(viagenDir, { recursive: true });
19139
- writeFileSync4(
19140
- join6(viagenDir, "config.json"),
19565
+ const viagenDir = join7(projectRoot, ".viagen");
19566
+ mkdirSync4(viagenDir, { recursive: true });
19567
+ writeFileSync5(
19568
+ join7(viagenDir, "config.json"),
19141
19569
  JSON.stringify({
19142
19570
  sandboxFiles: options?.sandboxFiles ?? [],
19143
19571
  editable: options?.editable ?? []
@@ -19215,9 +19643,10 @@ ${payload.err.frame || ""}`
19215
19643
  const platformUrl = env["VIAGEN_PLATFORM_URL"] || "https://app.viagen.dev";
19216
19644
  const projectId = env["VIAGEN_PROJECT_ID"];
19217
19645
  let viagenClient = null;
19646
+ const orgId = env["VIAGEN_ORG_ID"];
19218
19647
  if (platformToken) {
19219
- viagenClient = createViagen({ token: platformToken, baseUrl: platformUrl });
19220
- debug("server", `platform client created (baseUrl: ${platformUrl})`);
19648
+ viagenClient = createViagen({ token: platformToken, baseUrl: platformUrl, orgId });
19649
+ debug("server", `platform client created (baseUrl: ${platformUrl}, orgId: ${orgId || "none \u2014 will use default org"})`);
19221
19650
  }
19222
19651
  const hasEditor = !!(options?.editable && options.editable.length > 0);
19223
19652
  const clientJs = buildClientScript({
@@ -19237,9 +19666,11 @@ ${payload.err.frame || ""}`
19237
19666
  res.writeHead(302, { Location: "/?_viagen_chat" });
19238
19667
  res.end();
19239
19668
  });
19240
- server.middlewares.use("/via/iframe", (_req, res) => {
19669
+ server.middlewares.use("/via/iframe", (req, res) => {
19670
+ const url2 = new URL(req.url || "/", "http://localhost");
19671
+ const appUrl = url2.searchParams.get("appUrl") || void 0;
19241
19672
  res.setHeader("Content-Type", "text/html");
19242
- res.end(buildIframeHtml({ panelWidth: opts.panelWidth }));
19673
+ res.end(buildIframeHtml({ panelWidth: opts.panelWidth, appUrl }));
19243
19674
  });
19244
19675
  if (previewEnabled) {
19245
19676
  const previewJs = buildPreviewScript();
@@ -19307,13 +19738,36 @@ Page URL: ${pageUrl}`);
19307
19738
  });
19308
19739
  const resolvedModel = env["VIAGEN_MODEL"] || opts.model;
19309
19740
  debug("server", `creating ChatSession (model: ${resolvedModel})`);
19741
+ let processManager;
19742
+ const appCommand = env["VIAGEN_APP_COMMAND"];
19743
+ const isChildProcess = process.env["__VIAGEN_CHILD"] === "1";
19744
+ if (isChildProcess) {
19745
+ debug("server", "skipping process manager (running as child process)");
19746
+ } else if (appCommand) {
19747
+ const appPort = parseInt(env["VIAGEN_APP_PORT"] || "5173", 10);
19748
+ debug("server", `preview process manager enabled: "${appCommand}" on port ${appPort}`);
19749
+ logBuffer.push("info", `[viagen] Preview process manager: ${appCommand} (port ${appPort})`);
19750
+ processManager = new ProcessManager({
19751
+ command: appCommand,
19752
+ cwd: projectRoot,
19753
+ logBuffer,
19754
+ appPort
19755
+ });
19756
+ processManager.start();
19757
+ const cleanup = () => {
19758
+ processManager?.stop().finally(() => process.exit());
19759
+ };
19760
+ process.on("SIGINT", cleanup);
19761
+ process.on("SIGTERM", cleanup);
19762
+ }
19310
19763
  let mcpServers;
19311
19764
  const hasPlatformContext = !!(viagenClient && projectId);
19312
19765
  if (hasPlatformContext) {
19313
19766
  debug("server", "creating viagen MCP tools (platform connected)");
19314
19767
  const viagenMcp = createViagenTools({
19315
19768
  client: viagenClient,
19316
- projectId
19769
+ projectId,
19770
+ processManager
19317
19771
  });
19318
19772
  mcpServers = { [viagenMcp.name]: viagenMcp };
19319
19773
  }
@@ -19327,7 +19781,12 @@ Page URL: ${pageUrl}`);
19327
19781
  debug("server", "plan mode active \u2014 restricting tools");
19328
19782
  systemPrompt = PLAN_SYSTEM_PROMPT;
19329
19783
  } else if (hasPlatformContext) {
19330
- systemPrompt = (systemPrompt || "") + TASK_TOOLS_PROMPT;
19784
+ systemPrompt = (systemPrompt || "") + buildTaskToolsPrompt({
19785
+ projectId,
19786
+ taskId: env["VIAGEN_TASK_ID"] || void 0,
19787
+ branch: env["VIAGEN_BRANCH"] || void 0,
19788
+ hasProcessManager: !!processManager
19789
+ });
19331
19790
  }
19332
19791
  const chatSession = new ChatSession({
19333
19792
  env,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "viagen",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
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,7 +45,7 @@
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": {