viagen 0.0.32 → 0.0.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/cli.js +274 -9
  2. package/dist/index.js +292 -56
  3. package/package.json +2 -2
package/dist/cli.js CHANGED
@@ -44,6 +44,57 @@ function extractHost(httpsUrl) {
44
44
  return "github.com";
45
45
  }
46
46
  }
47
+ async function waitForServer(baseUrl, devServer) {
48
+ const timeout = 6e4;
49
+ const start = Date.now();
50
+ const ac = new AbortController();
51
+ let serverOutput = "";
52
+ const logStream = (async () => {
53
+ try {
54
+ for await (const log of devServer.logs({ signal: ac.signal })) {
55
+ const line = log.data;
56
+ serverOutput += line;
57
+ if (log.stream === "stderr") {
58
+ process.stderr.write(` ${line}`);
59
+ } else {
60
+ process.stdout.write(` ${line}`);
61
+ }
62
+ }
63
+ } catch {
64
+ }
65
+ })();
66
+ while (Date.now() - start < timeout) {
67
+ if (devServer.exitCode !== null) {
68
+ ac.abort();
69
+ await logStream.catch(() => {
70
+ });
71
+ return {
72
+ ok: false,
73
+ error: serverOutput.slice(-2e3) || `Process exited with code ${devServer.exitCode}`
74
+ };
75
+ }
76
+ try {
77
+ const res = await fetch(`${baseUrl}/via/health`, {
78
+ signal: AbortSignal.timeout(3e3)
79
+ });
80
+ if (res.ok) {
81
+ ac.abort();
82
+ await logStream.catch(() => {
83
+ });
84
+ return { ok: true };
85
+ }
86
+ } catch {
87
+ }
88
+ await new Promise((r) => setTimeout(r, 2e3));
89
+ }
90
+ ac.abort();
91
+ await logStream.catch(() => {
92
+ });
93
+ return {
94
+ ok: false,
95
+ error: serverOutput.slice(-2e3) || "Timed out waiting for dev server"
96
+ };
97
+ }
47
98
  async function deploySandbox(opts) {
48
99
  const token = randomUUID();
49
100
  const useGit = !!opts.git;
@@ -95,7 +146,11 @@ async function deploySandbox(opts) {
95
146
  "user.email",
96
147
  opts.git.userEmail
97
148
  ]);
98
- await sandbox2.runCommand("git", ["checkout", "-B", opts.git.branch]);
149
+ const checkout = await sandbox2.runCommand("git", ["checkout", "-B", opts.git.branch]);
150
+ if (checkout.exitCode !== 0) {
151
+ const err = await checkout.stderr();
152
+ throw new Error(`git checkout failed (exit ${checkout.exitCode}): ${err}`);
153
+ }
99
154
  await sandbox2.runCommand("bash", [
100
155
  "-c",
101
156
  `echo 'https://x-access-token:${opts.git.token}@${extractHost(opts.git.remoteUrl)}' > ~/.git-credentials`
@@ -157,20 +212,30 @@ async function deploySandbox(opts) {
157
212
  content: Buffer.from(envLines.join("\n"))
158
213
  }
159
214
  ]);
160
- const install = await sandbox2.runCommand("npm", ["install"]);
215
+ console.log(" Installing dependencies...");
216
+ const install = await sandbox2.runCommand({
217
+ cmd: "npm",
218
+ args: ["install"],
219
+ stdout: process.stdout,
220
+ stderr: process.stderr
221
+ });
161
222
  if (install.exitCode !== 0) {
162
- const stderr = await install.stderr();
163
- throw new Error(
164
- `npm install failed (exit ${install.exitCode}): ${stderr}`
165
- );
223
+ throw new Error(`npm install failed (exit ${install.exitCode})`);
166
224
  }
167
- await sandbox2.runCommand({
225
+ const devServer = await sandbox2.runCommand({
168
226
  cmd: "npm",
169
227
  args: ["run", "dev", "--", "--host", "0.0.0.0"],
170
228
  detached: true
171
229
  });
230
+ console.log(" Starting dev server...");
172
231
  const baseUrl = sandbox2.domain(5173);
173
232
  const url = `${baseUrl}/t/${token}`;
233
+ const ready = await waitForServer(baseUrl, devServer);
234
+ if (!ready.ok) {
235
+ throw new Error(
236
+ `Dev server failed to start: ${ready.error}`
237
+ );
238
+ }
174
239
  return {
175
240
  url,
176
241
  token,
@@ -816,7 +881,7 @@ function parseFlag(args, flag) {
816
881
  if (idx !== -1 && idx + 1 < args.length) return args[idx + 1];
817
882
  return void 0;
818
883
  }
819
- async function sandbox(args) {
884
+ async function sandbox(args, options) {
820
885
  const subcommand = args[0];
821
886
  if (subcommand === "stop") {
822
887
  const sandboxId = args[1];
@@ -832,9 +897,12 @@ async function sandbox(args) {
832
897
  const branchOverride = parseFlag(args, "--branch") || parseFlag(args, "-b");
833
898
  const timeoutFlag = parseFlag(args, "--timeout") || parseFlag(args, "-t");
834
899
  const timeoutMinutes = timeoutFlag ? parseInt(timeoutFlag, 10) : void 0;
835
- const prompt = parseFlag(args, "--prompt") || parseFlag(args, "-p");
900
+ const prompt = options?.promptOverride || parseFlag(args, "--prompt") || parseFlag(args, "-p");
836
901
  const cwd = process.cwd();
837
902
  const dotenv = loadDotenv(cwd);
903
+ if (options?.extraEnvVars) {
904
+ Object.assign(dotenv, options.extraEnvVars);
905
+ }
838
906
  for (const [key, val] of Object.entries(dotenv)) {
839
907
  if (!process.env[key]) process.env[key] = val;
840
908
  }
@@ -1277,6 +1345,43 @@ async function requireClient() {
1277
1345
  }
1278
1346
  return createViagen({ baseUrl: creds.baseUrl, token: creds.token, orgId: creds.orgId });
1279
1347
  }
1348
+ async function requireProjectId(client) {
1349
+ const cwd = process.cwd();
1350
+ const env = loadDotenv(cwd);
1351
+ let projectId = env["VIAGEN_PROJECT_ID"];
1352
+ if (projectId) {
1353
+ try {
1354
+ await client.projects.get(projectId);
1355
+ return projectId;
1356
+ } catch {
1357
+ console.log("Project from .env not found. Choose a project:");
1358
+ console.log("");
1359
+ projectId = void 0;
1360
+ }
1361
+ }
1362
+ const projects2 = await client.projects.list();
1363
+ if (projects2.length === 0) {
1364
+ console.error("No projects. Create one with `viagen projects create <name>` or `viagen sync`.");
1365
+ process.exit(1);
1366
+ }
1367
+ if (projects2.length === 1) {
1368
+ return projects2[0].id;
1369
+ }
1370
+ console.log("Select a project:");
1371
+ console.log("");
1372
+ for (let i = 0; i < projects2.length; i++) {
1373
+ const repo = projects2[i].githubRepo ? ` (${projects2[i].githubRepo})` : "";
1374
+ console.log(` ${i + 1}) ${projects2[i].name}${repo}`);
1375
+ }
1376
+ console.log("");
1377
+ const choice = await promptUser("Choose: ");
1378
+ const idx = parseInt(choice, 10) - 1;
1379
+ if (idx < 0 || idx >= projects2.length) {
1380
+ console.error("Invalid selection.");
1381
+ process.exit(1);
1382
+ }
1383
+ return projects2[idx].id;
1384
+ }
1280
1385
  async function teams() {
1281
1386
  const client = await requireClient();
1282
1387
  const memberships = await client.orgs.list() ?? [];
@@ -1393,6 +1498,156 @@ async function projects(args) {
1393
1498
  console.log(` ${p.name}${repo} ${p.id}`);
1394
1499
  }
1395
1500
  }
1501
+ function formatTaskStatus(status) {
1502
+ const icons = {
1503
+ ready: "\u25CB",
1504
+ running: "\u25CF",
1505
+ completed: "\u2713",
1506
+ failed: "\u2717",
1507
+ validating: "\u25CE"
1508
+ };
1509
+ return `${icons[status] || "?"} ${status}`;
1510
+ }
1511
+ async function tasksList(args) {
1512
+ const client = await requireClient();
1513
+ const projectId = await requireProjectId(client);
1514
+ const status = parseFlag(args, "--status") || parseFlag(args, "-s");
1515
+ const list = await client.tasks.list(projectId, status);
1516
+ if (list.length === 0) {
1517
+ console.log(
1518
+ status ? `No tasks with status "${status}".` : "No tasks. Create one with `viagen tasks create <prompt>`."
1519
+ );
1520
+ return;
1521
+ }
1522
+ console.log(
1523
+ `${"ID".padEnd(10)} ${"STATUS".padEnd(14)} ${"BRANCH".padEnd(20)} ${"CREATED".padEnd(12)} PROMPT`
1524
+ );
1525
+ console.log("-".repeat(80));
1526
+ for (const t of list) {
1527
+ const id = t.id.slice(0, 8);
1528
+ const st = formatTaskStatus(t.status).padEnd(14);
1529
+ const branch = (t.branch || "-").slice(0, 18).padEnd(20);
1530
+ const created = t.createdAt.slice(0, 10).padEnd(12);
1531
+ const prompt = t.prompt.length > 40 ? t.prompt.slice(0, 37) + "..." : t.prompt;
1532
+ console.log(`${id.padEnd(10)} ${st} ${branch} ${created} ${prompt}`);
1533
+ }
1534
+ console.log("");
1535
+ console.log(`${list.length} task(s)`);
1536
+ }
1537
+ async function tasksCreate(args) {
1538
+ const branch = parseFlag(args, "--branch") || parseFlag(args, "-b");
1539
+ const positional = [];
1540
+ for (let i = 0; i < args.length; i++) {
1541
+ if (args[i] === "--branch" || args[i] === "-b") {
1542
+ i++;
1543
+ continue;
1544
+ }
1545
+ positional.push(args[i]);
1546
+ }
1547
+ let prompt = positional.join(" ");
1548
+ if (!prompt) {
1549
+ prompt = await promptUser("Task prompt: ");
1550
+ if (!prompt) {
1551
+ console.log("Cancelled.");
1552
+ return;
1553
+ }
1554
+ }
1555
+ const client = await requireClient();
1556
+ const projectId = await requireProjectId(client);
1557
+ const input = { prompt };
1558
+ if (branch) input.branch = branch;
1559
+ const task = await client.tasks.create(projectId, input);
1560
+ console.log(`Task created: ${task.id}`);
1561
+ console.log(` Prompt: ${task.prompt}`);
1562
+ console.log(` Branch: ${task.branch}`);
1563
+ console.log(` Status: ${task.status}`);
1564
+ console.log("");
1565
+ console.log(`Run with: viagen tasks run ${task.id}`);
1566
+ }
1567
+ async function tasksGet(args) {
1568
+ const taskId = args[0];
1569
+ if (!taskId) {
1570
+ console.error("Usage: viagen tasks get <id>");
1571
+ process.exit(1);
1572
+ }
1573
+ const client = await requireClient();
1574
+ const projectId = await requireProjectId(client);
1575
+ const task = await client.tasks.get(projectId, taskId);
1576
+ console.log(` ID: ${task.id}`);
1577
+ console.log(` Status: ${formatTaskStatus(task.status)}`);
1578
+ console.log(` Prompt: ${task.prompt}`);
1579
+ console.log(` Branch: ${task.branch}`);
1580
+ console.log(` Model: ${task.model}`);
1581
+ console.log(` Created: ${task.createdAt}`);
1582
+ console.log(` Created By: ${task.createdBy}`);
1583
+ if (task.startedAt) console.log(` Started: ${task.startedAt}`);
1584
+ if (task.completedAt) console.log(` Completed: ${task.completedAt}`);
1585
+ if (task.durationMs) {
1586
+ const secs = (task.durationMs / 1e3).toFixed(1);
1587
+ console.log(` Duration: ${secs}s`);
1588
+ }
1589
+ if (task.inputTokens || task.outputTokens) {
1590
+ console.log(
1591
+ ` Tokens: ${(task.inputTokens || 0).toLocaleString()} in / ${(task.outputTokens || 0).toLocaleString()} out`
1592
+ );
1593
+ }
1594
+ if (task.prUrl) console.log(` PR: ${task.prUrl}`);
1595
+ if (task.result) console.log(` Result: ${task.result}`);
1596
+ if (task.error) console.log(` Error: ${task.error}`);
1597
+ }
1598
+ async function tasksRun(args) {
1599
+ const taskId = args[0];
1600
+ if (!taskId) {
1601
+ console.error("Usage: viagen tasks run <id>");
1602
+ process.exit(1);
1603
+ }
1604
+ const client = await requireClient();
1605
+ const projectId = await requireProjectId(client);
1606
+ const task = await client.tasks.get(projectId, taskId);
1607
+ console.log(`Running task: ${task.id}`);
1608
+ console.log(` Prompt: ${task.prompt}`);
1609
+ console.log(` Branch: ${task.branch}`);
1610
+ console.log("");
1611
+ await client.tasks.update(projectId, taskId, { status: "running" });
1612
+ const sandboxArgs = [];
1613
+ if (task.branch) {
1614
+ sandboxArgs.push("--branch", task.branch);
1615
+ }
1616
+ const timeout = parseFlag(args, "--timeout") || parseFlag(args, "-t");
1617
+ if (timeout) {
1618
+ sandboxArgs.push("--timeout", timeout);
1619
+ }
1620
+ try {
1621
+ await sandbox(sandboxArgs, {
1622
+ promptOverride: task.prompt,
1623
+ extraEnvVars: {
1624
+ VIAGEN_TASK_ID: task.id,
1625
+ VIAGEN_PROJECT_ID: projectId
1626
+ }
1627
+ });
1628
+ } catch (err) {
1629
+ console.error(`Sandbox deploy failed: ${err instanceof Error ? err.message : String(err)}`);
1630
+ await client.tasks.update(projectId, taskId, { status: "ready" }).catch(() => {
1631
+ });
1632
+ process.exit(1);
1633
+ }
1634
+ }
1635
+ async function tasks(args) {
1636
+ const sub = args[0];
1637
+ if (sub === "create") {
1638
+ await tasksCreate(args.slice(1));
1639
+ return;
1640
+ }
1641
+ if (sub === "get") {
1642
+ await tasksGet(args.slice(1));
1643
+ return;
1644
+ }
1645
+ if (sub === "run") {
1646
+ await tasksRun(args.slice(1));
1647
+ return;
1648
+ }
1649
+ await tasksList(args);
1650
+ }
1396
1651
  function help() {
1397
1652
  console.log("viagen \u2014 Claude Code in your Vite dev server");
1398
1653
  console.log("");
@@ -1411,6 +1666,10 @@ function help() {
1411
1666
  console.log(" projects create <name> Create a new project");
1412
1667
  console.log(" projects get <id> Show project details");
1413
1668
  console.log(" projects delete <id> Delete a project");
1669
+ console.log(" tasks List tasks for current project");
1670
+ console.log(" tasks create <prompt> Create a new task");
1671
+ console.log(" tasks get <id> Show task details");
1672
+ console.log(" tasks run <id> Run a task in a sandbox");
1414
1673
  console.log(" dev Start Vite and open the split view");
1415
1674
  console.log(" setup Set up .env with API keys and tokens");
1416
1675
  console.log(" sandbox [-b branch] [-t min] Deploy your project to a Vercel Sandbox");
@@ -1422,6 +1681,10 @@ function help() {
1422
1681
  console.log(" -b, --branch <name> Branch to clone (default: current branch)");
1423
1682
  console.log(" -t, --timeout <min> Sandbox timeout in minutes (default: 30)");
1424
1683
  console.log("");
1684
+ console.log("Task options:");
1685
+ console.log(" -s, --status <status> Filter tasks by status (list)");
1686
+ console.log(" -b, --branch <name> Branch for the task (create)");
1687
+ console.log("");
1425
1688
  console.log("Getting started:");
1426
1689
  console.log(" 1. npm install viagen");
1427
1690
  console.log(" 2. Add viagen() to your vite.config.ts plugins");
@@ -1483,6 +1746,8 @@ async function main() {
1483
1746
  await orgs(args.slice(1));
1484
1747
  } else if (command === "projects") {
1485
1748
  await projects(args.slice(1));
1749
+ } else if (command === "tasks") {
1750
+ await tasks(args.slice(1));
1486
1751
  } else if (command === "dev") {
1487
1752
  dev();
1488
1753
  return;
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // src/index.ts
2
2
  import { execSync as execSync2 } from "child_process";
3
3
  import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync4 } from "fs";
4
- import { join as join6 } from "path";
4
+ import { join as join7 } from "path";
5
5
  import { loadEnv } from "vite";
6
6
 
7
7
  // src/logger.ts
@@ -59,6 +59,8 @@ function wrapLogger(logger, buffer) {
59
59
  }
60
60
 
61
61
  // src/health.ts
62
+ import { readFileSync } from "fs";
63
+ import { join as join2 } from "path";
62
64
  function registerHealthRoutes(server, env, errorRef) {
63
65
  server.middlewares.use("/via/error", (_req, res) => {
64
66
  res.setHeader("Content-Type", "application/json");
@@ -84,20 +86,81 @@ function registerHealthRoutes(server, env, errorRef) {
84
86
  timeoutSeconds: sessionTimeout
85
87
  } : null;
86
88
  const prompt = env["VIAGEN_PROMPT"] || null;
89
+ const taskId = env["VIAGEN_TASK_ID"] || null;
90
+ const projectId = env["VIAGEN_PROJECT_ID"] || null;
87
91
  res.setHeader("Content-Type", "application/json");
88
92
  if (configured) {
89
- res.end(JSON.stringify({ status: "ok", configured: true, git, vercel, branch, session, prompt, missing }));
93
+ res.end(JSON.stringify({ status: "ok", configured: true, git, vercel, branch, session, prompt, taskId, projectId, missing }));
90
94
  } else {
91
95
  res.end(
92
- JSON.stringify({ status: "error", configured: false, git, vercel, branch, session, prompt, missing })
96
+ JSON.stringify({ status: "error", configured: false, git, vercel, branch, session, prompt, taskId, projectId, missing })
97
+ );
98
+ }
99
+ });
100
+ let currentVersion = null;
101
+ let versionCache = null;
102
+ function getCurrentVersion() {
103
+ if (currentVersion) return currentVersion;
104
+ try {
105
+ const pkg = JSON.parse(
106
+ readFileSync(join2(__dirname, "..", "package.json"), "utf-8")
107
+ );
108
+ currentVersion = pkg.version;
109
+ } catch {
110
+ try {
111
+ const pkg = JSON.parse(
112
+ readFileSync(
113
+ join2(__dirname, "package.json"),
114
+ "utf-8"
115
+ )
116
+ );
117
+ currentVersion = pkg.version;
118
+ } catch {
119
+ currentVersion = "0.0.0";
120
+ }
121
+ }
122
+ return currentVersion;
123
+ }
124
+ server.middlewares.use("/via/version", (_req, res) => {
125
+ res.setHeader("Content-Type", "application/json");
126
+ const current = getCurrentVersion();
127
+ if (versionCache && Date.now() - versionCache.ts < 3e5) {
128
+ res.end(
129
+ JSON.stringify({
130
+ current,
131
+ latest: versionCache.latest,
132
+ updateAvailable: versionCache.latest !== current
133
+ })
93
134
  );
135
+ return;
94
136
  }
137
+ fetch("https://registry.npmjs.org/viagen/latest", {
138
+ headers: { Accept: "application/json" }
139
+ }).then((r) => r.json()).then((data) => {
140
+ const latest = data.version ?? current;
141
+ versionCache = { latest, ts: Date.now() };
142
+ res.end(
143
+ JSON.stringify({
144
+ current,
145
+ latest,
146
+ updateAvailable: latest !== current
147
+ })
148
+ );
149
+ }).catch(() => {
150
+ res.end(
151
+ JSON.stringify({
152
+ current,
153
+ latest: null,
154
+ updateAvailable: false
155
+ })
156
+ );
157
+ });
95
158
  });
96
159
  }
97
160
 
98
161
  // src/chat.ts
99
- import { readFileSync, writeFileSync as writeFileSync2, appendFileSync, existsSync } from "fs";
100
- import { join as join2 } from "path";
162
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, appendFileSync, existsSync } from "fs";
163
+ import { join as join3 } from "path";
101
164
  import {
102
165
  query
103
166
  } from "@anthropic-ai/claude-agent-sdk";
@@ -197,7 +260,7 @@ var ChatSession = class {
197
260
  lastUsedToken;
198
261
  constructor(opts) {
199
262
  this.opts = opts;
200
- this.chatLogPath = join2(opts.projectRoot, ".viagen", "chat.log");
263
+ this.chatLogPath = join3(opts.projectRoot, ".viagen", "chat.log");
201
264
  }
202
265
  reset() {
203
266
  this.sessionId = void 0;
@@ -208,7 +271,7 @@ var ChatSession = class {
208
271
  }
209
272
  getHistory() {
210
273
  try {
211
- const raw = readFileSync(this.chatLogPath, "utf-8");
274
+ const raw = readFileSync2(this.chatLogPath, "utf-8");
212
275
  const entries = [];
213
276
  for (const line of raw.split("\n")) {
214
277
  if (!line.trim()) continue;
@@ -245,9 +308,9 @@ var ChatSession = class {
245
308
  this.opts.env["CLAUDE_TOKEN_EXPIRES"] = String(
246
309
  nowSec + tokens.expires_in
247
310
  );
248
- const envPath = join2(this.opts.projectRoot, ".env");
311
+ const envPath = join3(this.opts.projectRoot, ".env");
249
312
  if (existsSync(envPath)) {
250
- let content = readFileSync(envPath, "utf-8");
313
+ let content = readFileSync2(envPath, "utf-8");
251
314
  const replacements = {
252
315
  CLAUDE_ACCESS_TOKEN: tokens.access_token,
253
316
  CLAUDE_REFRESH_TOKEN: tokens.refresh_token,
@@ -414,7 +477,19 @@ var ChatSession = class {
414
477
  sink?.({ type: "error", text: err });
415
478
  }
416
479
  }
417
- sink?.({ type: "done" });
480
+ const doneEvent = { type: "done" };
481
+ if ("total_cost_usd" in msg) {
482
+ doneEvent.costUsd = msg.total_cost_usd;
483
+ }
484
+ if ("duration_ms" in msg) {
485
+ doneEvent.durationMs = msg.duration_ms;
486
+ }
487
+ if ("usage" in msg && msg.usage) {
488
+ const u = msg.usage;
489
+ doneEvent.inputTokens = u.input_tokens ?? 0;
490
+ doneEvent.outputTokens = u.output_tokens ?? 0;
491
+ }
492
+ sink?.(doneEvent);
418
493
  this.currentDoneResolve?.();
419
494
  this.currentDoneResolve = null;
420
495
  this.currentEventSink = null;
@@ -1374,6 +1449,30 @@ function buildUiHtml(opts) {
1374
1449
  color: #d4d4d8;
1375
1450
  font-size: 11px;
1376
1451
  }
1452
+ .update-banner {
1453
+ padding: 6px 12px;
1454
+ border-bottom: 1px solid #27272a;
1455
+ background: #18181b;
1456
+ font-family: ui-monospace, monospace;
1457
+ font-size: 11px;
1458
+ color: #a1a1aa;
1459
+ flex-shrink: 0;
1460
+ display: none;
1461
+ align-items: center;
1462
+ gap: 8px;
1463
+ cursor: pointer;
1464
+ transition: background 0.15s;
1465
+ }
1466
+ .update-banner:hover { background: #1e1e22; }
1467
+ .update-banner .update-badge {
1468
+ font-size: 9px;
1469
+ font-weight: 700;
1470
+ padding: 1px 5px;
1471
+ border-radius: 3px;
1472
+ background: #365314;
1473
+ color: #86efac;
1474
+ text-transform: uppercase;
1475
+ }
1377
1476
  .btn {
1378
1477
  padding: 5px 10px;
1379
1478
  border: 1px solid #3f3f46;
@@ -1800,13 +1899,14 @@ function buildUiHtml(opts) {
1800
1899
  </div>` : ""}
1801
1900
  <div id="chat-view" style="display:flex;flex-direction:column;flex:1;overflow:hidden;">
1802
1901
  <div class="setup-banner" id="setup-banner"></div>
1902
+ <div class="update-banner" id="update-banner"><span class="update-badge">update</span><span id="update-text"></span></div>
1803
1903
  <div class="activity-bar" id="activity-bar"></div>
1804
1904
  <div class="messages" id="messages"></div>
1805
1905
  <div class="input-area">
1806
1906
  <input type="text" id="input" placeholder="What do you want to build?" autofocus />
1807
1907
  <button class="send-btn" id="send-btn">Send</button>
1808
1908
  </div>
1809
- ${hasGit ? '<div class="status-bar" id="status-bar" style="display:none;"><span id="status-branch"></span><span id="status-diff"></span></div>' : ""}
1909
+ <div class="status-bar" id="status-bar">${hasGit ? '<span id="status-branch"></span><span id="status-diff"></span>' : ""}<span id="status-cost" style="display:none;margin-left:auto;"></span></div>
1810
1910
  </div>
1811
1911
  ${editor ? editor.html : ""}
1812
1912
  ${hasGit ? `<div id="changes-view" style="display:none;flex-direction:column;flex:1;overflow:hidden;">
@@ -1895,6 +1995,12 @@ function buildUiHtml(opts) {
1895
1995
  } catch(e) {}
1896
1996
  }
1897
1997
 
1998
+ function scrollToBottom() {
1999
+ requestAnimationFrame(function() {
2000
+ scrollToBottom();
2001
+ });
2002
+ }
2003
+
1898
2004
  function formatDuration(ms) {
1899
2005
  if (ms < 1000) return ms + 'ms';
1900
2006
  var secs = Math.round(ms / 1000);
@@ -1919,14 +2025,41 @@ function buildUiHtml(opts) {
1919
2025
  activityTimer = setInterval(updateActivityBar, 1000);
1920
2026
  }
1921
2027
 
1922
- function hideActivity() {
2028
+ var sessionCostUsd = 0;
2029
+ var sessionInputTokens = 0;
2030
+ var sessionOutputTokens = 0;
2031
+
2032
+ function formatCost(usd) {
2033
+ if (usd < 0.01) return '<$0.01';
2034
+ return '$' + usd.toFixed(2);
2035
+ }
2036
+
2037
+ function formatTokens(n) {
2038
+ if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M';
2039
+ if (n >= 1000) return (n / 1000).toFixed(1) + 'k';
2040
+ return String(n);
2041
+ }
2042
+
2043
+ function hideActivity(usage) {
1923
2044
  if (activityTimer) { clearInterval(activityTimer); activityTimer = null; }
1924
2045
  var elapsed = formatDuration(Date.now() - sendStartTime);
1925
2046
  var parts = ['Done in ' + elapsed];
1926
2047
  if (toolCount > 0) parts.push(toolCount + (toolCount === 1 ? ' action' : ' actions'));
1927
- activityBar.textContent = parts.join(' \xB7 ');
2048
+ if (usage && usage.costUsd != null) {
2049
+ parts.push(formatCost(usage.costUsd));
2050
+ sessionCostUsd += usage.costUsd;
2051
+ sessionInputTokens += (usage.inputTokens || 0);
2052
+ sessionOutputTokens += (usage.outputTokens || 0);
2053
+ }
2054
+ activityBar.textContent = parts.join(' \\u00b7 ');
1928
2055
  activityBar.classList.add('done');
1929
2056
  setTimeout(function() { activityBar.style.display = 'none'; }, 5000);
2057
+ // Update status bar with session total
2058
+ var costEl = document.getElementById('status-cost');
2059
+ if (costEl && sessionCostUsd > 0) {
2060
+ costEl.textContent = formatCost(sessionCostUsd) + ' (' + formatTokens(sessionInputTokens + sessionOutputTokens) + ' tokens)';
2061
+ costEl.style.display = '';
2062
+ }
1930
2063
  }
1931
2064
  window.addEventListener('beforeunload', function() { unloading = true; stopHistoryPolling(); });
1932
2065
  window.addEventListener('pagehide', function() { unloading = true; });
@@ -2110,7 +2243,7 @@ function buildUiHtml(opts) {
2110
2243
  chatLog.push({ type: 'user', content: text });
2111
2244
 
2112
2245
  renderUserMessage(text);
2113
- messagesEl.scrollTop = messagesEl.scrollHeight;
2246
+ scrollToBottom();
2114
2247
  }
2115
2248
 
2116
2249
  function appendText(text) {
@@ -2131,7 +2264,7 @@ function buildUiHtml(opts) {
2131
2264
  }
2132
2265
  var fullText = chatLog[chatLog.length - 1].content;
2133
2266
  currentTextSpan.innerHTML = renderMarkdown(fullText);
2134
- messagesEl.scrollTop = messagesEl.scrollHeight;
2267
+ scrollToBottom();
2135
2268
  }
2136
2269
 
2137
2270
  function addToolBlock(name, input) {
@@ -2140,7 +2273,7 @@ function buildUiHtml(opts) {
2140
2273
  chatLog.push({ type: 'tool', content: label });
2141
2274
 
2142
2275
  renderToolBlock(label);
2143
- messagesEl.scrollTop = messagesEl.scrollHeight;
2276
+ scrollToBottom();
2144
2277
  }
2145
2278
 
2146
2279
  function renderToolResult(text) {
@@ -2167,7 +2300,7 @@ function buildUiHtml(opts) {
2167
2300
  chatLog.push({ type: 'error', content: text });
2168
2301
 
2169
2302
  renderErrorBlock(text);
2170
- messagesEl.scrollTop = messagesEl.scrollHeight;
2303
+ scrollToBottom();
2171
2304
  }
2172
2305
 
2173
2306
  function setStreaming(v) {
@@ -2179,18 +2312,7 @@ function buildUiHtml(opts) {
2179
2312
  else startHistoryPolling();
2180
2313
  }
2181
2314
 
2182
- async function send() {
2183
- var text = inputEl.value.trim();
2184
- if (!text || isStreaming) return;
2185
-
2186
- addUserMessage(text);
2187
- inputEl.value = '';
2188
- setStreaming(true);
2189
- currentTextSpan = null;
2190
- sendStartTime = Date.now();
2191
- toolCount = 0;
2192
- showActivity();
2193
-
2315
+ async function sendRaw(text) {
2194
2316
  try {
2195
2317
  var res = await fetch('/via/chat', {
2196
2318
  method: 'POST',
@@ -2201,6 +2323,7 @@ function buildUiHtml(opts) {
2201
2323
  var reader = res.body.getReader();
2202
2324
  var decoder = new TextDecoder();
2203
2325
  var buffer = '';
2326
+ var lastUsage = null;
2204
2327
 
2205
2328
  while (true) {
2206
2329
  var result = await reader.read();
@@ -2218,6 +2341,7 @@ function buildUiHtml(opts) {
2218
2341
  else if (data.type === 'tool_use') { toolCount++; updateActivityBar(); addToolBlock(data.name, data.input); }
2219
2342
  else if (data.type === 'tool_result') addToolResult(data.text);
2220
2343
  else if (data.type === 'error') addErrorBlock(data.text);
2344
+ else if (data.type === 'done') lastUsage = data;
2221
2345
  } catch (e) {}
2222
2346
  }
2223
2347
  }
@@ -2225,14 +2349,29 @@ function buildUiHtml(opts) {
2225
2349
  if (!unloading) addErrorBlock('Connection failed');
2226
2350
  }
2227
2351
 
2228
- hideActivity();
2352
+ hideActivity(lastUsage);
2229
2353
  playDoneSound();
2230
- // Advance timestamp so polling doesn't re-render messages from this stream
2231
2354
  historyTimestamp = Date.now();
2232
2355
  setStreaming(false);
2233
2356
  inputEl.focus();
2234
2357
  }
2235
2358
 
2359
+ async function send() {
2360
+ var text = inputEl.value.trim();
2361
+ if (!text || isStreaming) return;
2362
+
2363
+ addUserMessage(text);
2364
+ inputEl.value = '';
2365
+ setStreaming(true);
2366
+ currentTextSpan = null;
2367
+ sendStartTime = Date.now();
2368
+ toolCount = 0;
2369
+ showActivity();
2370
+ scrollToBottom();
2371
+
2372
+ await sendRaw(text);
2373
+ }
2374
+
2236
2375
  sendBtn.addEventListener('click', send);
2237
2376
  inputEl.addEventListener('keydown', function (e) {
2238
2377
  if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); send(); }
@@ -2383,7 +2522,6 @@ function buildUiHtml(opts) {
2383
2522
  var statusBar = document.getElementById('status-bar');
2384
2523
  var statusBranch = document.getElementById('status-branch');
2385
2524
  if (statusBar && statusBranch) {
2386
- statusBar.style.display = 'flex';
2387
2525
  statusBranch.textContent = '\\u2387 ' + d.branch;
2388
2526
  if (branchUrl) {
2389
2527
  statusBranch.addEventListener('click', function() { window.open(branchUrl, '_blank'); });
@@ -2416,10 +2554,43 @@ function buildUiHtml(opts) {
2416
2554
  }).catch(function() {});
2417
2555
  }
2418
2556
 
2557
+ // Check for viagen updates
2558
+ fetch('/via/version').then(function(r) { return r.json(); }).then(function(v) {
2559
+ if (v.updateAvailable && v.latest) {
2560
+ var updateBanner = document.getElementById('update-banner');
2561
+ var updateText = document.getElementById('update-text');
2562
+ if (updateBanner && updateText) {
2563
+ updateText.textContent = 'viagen ' + v.latest + ' available (current: ' + v.current + ')';
2564
+ updateBanner.style.display = 'flex';
2565
+ updateBanner.addEventListener('click', function() {
2566
+ updateBanner.style.display = 'none';
2567
+ inputEl.value = 'Update viagen to v' + v.latest + ' (npm install viagen@' + v.latest + ') and restart the dev server.';
2568
+ send();
2569
+ });
2570
+ }
2571
+ }
2572
+ }).catch(function() {});
2573
+
2419
2574
  // Only auto-send prompt if no history exists (first boot)
2420
2575
  if (data.prompt && data.configured && chatLog.length === 0) {
2421
- inputEl.value = data.prompt;
2422
- send();
2576
+ if (data.taskId) {
2577
+ // Task mode: show link instead of raw prompt
2578
+ var taskUrl = 'https://app.viagen.dev' + (data.projectId ? '/' + data.projectId : '') + '/' + data.taskId;
2579
+ var div = document.createElement('div');
2580
+ div.className = 'msg msg-user';
2581
+ div.innerHTML = '<span class="label">Task</span><span class="text">Received instructions from <a href="' + escapeHtml(taskUrl) + '" target="_blank" style="color:#93c5fd;text-decoration:underline;">Viagen Task</a></span>';
2582
+ messagesEl.appendChild(div);
2583
+ scrollToBottom();
2584
+ // Send the prompt silently (don't show it as a user message)
2585
+ showActivity();
2586
+ setStreaming(true);
2587
+ sendStartTime = Date.now();
2588
+ toolCount = 0;
2589
+ sendRaw(data.prompt);
2590
+ } else {
2591
+ inputEl.value = data.prompt;
2592
+ send();
2593
+ }
2423
2594
  }
2424
2595
  })
2425
2596
  .catch(function() {
@@ -2804,11 +2975,11 @@ function createAuthMiddleware(token) {
2804
2975
  // src/files.ts
2805
2976
  import {
2806
2977
  readdirSync,
2807
- readFileSync as readFileSync2,
2978
+ readFileSync as readFileSync3,
2808
2979
  writeFileSync as writeFileSync3,
2809
2980
  statSync
2810
2981
  } from "fs";
2811
- import { join as join3, resolve, relative } from "path";
2982
+ import { join as join4, resolve, relative } from "path";
2812
2983
  function readBody2(req) {
2813
2984
  return new Promise((resolve3, reject) => {
2814
2985
  let body = "";
@@ -2823,7 +2994,7 @@ function collectFiles(dir, projectRoot) {
2823
2994
  const results = [];
2824
2995
  try {
2825
2996
  for (const entry of readdirSync(dir, { withFileTypes: true })) {
2826
- const fullPath = join3(dir, entry.name);
2997
+ const fullPath = join4(dir, entry.name);
2827
2998
  if (entry.isDirectory()) {
2828
2999
  if (entry.name === "node_modules" || entry.name === ".git") continue;
2829
3000
  results.push(...collectFiles(fullPath, projectRoot));
@@ -2911,7 +3082,7 @@ function registerFileRoutes(server, opts) {
2911
3082
  const ext = filePath.split(".").pop()?.toLowerCase() ?? "";
2912
3083
  const mime = MIME_TYPES[ext] ?? "application/octet-stream";
2913
3084
  try {
2914
- const data = readFileSync2(abs);
3085
+ const data = readFileSync3(abs);
2915
3086
  res.setHeader("Content-Type", mime);
2916
3087
  res.setHeader("Cache-Control", "no-cache");
2917
3088
  res.end(data);
@@ -2949,7 +3120,7 @@ function registerFileRoutes(server, opts) {
2949
3120
  }
2950
3121
  const abs = resolve(opts.projectRoot, filePath);
2951
3122
  try {
2952
- const content = readFileSync2(abs, "utf-8");
3123
+ const content = readFileSync3(abs, "utf-8");
2953
3124
  res.setHeader("Content-Type", "application/json");
2954
3125
  res.end(JSON.stringify({ path: filePath, content }));
2955
3126
  } catch {
@@ -3057,8 +3228,8 @@ function createInjectionMiddleware() {
3057
3228
  }
3058
3229
 
3059
3230
  // src/git.ts
3060
- import { readFileSync as readFileSync3 } from "fs";
3061
- import { join as join4, resolve as resolve2 } from "path";
3231
+ import { readFileSync as readFileSync4 } from "fs";
3232
+ import { join as join5, resolve as resolve2 } from "path";
3062
3233
  import {
3063
3234
  simpleGit
3064
3235
  } from "simple-git";
@@ -3101,7 +3272,7 @@ async function getDiffStats(git, repoRoot, untrackedFiles) {
3101
3272
  }
3102
3273
  for (const filePath of untrackedFiles) {
3103
3274
  try {
3104
- const content = readFileSync3(join4(repoRoot, filePath), "utf-8");
3275
+ const content = readFileSync4(join5(repoRoot, filePath), "utf-8");
3105
3276
  const lines = content.split("\n").length;
3106
3277
  stats.set(filePath, { ins: lines, del: 0 });
3107
3278
  } catch {
@@ -3127,7 +3298,7 @@ async function getFileDiff(git, repoRoot, filePath) {
3127
3298
  if (staged) return staged;
3128
3299
  if (unstaged) return unstaged;
3129
3300
  try {
3130
- const content = readFileSync3(join4(repoRoot, filePath), "utf-8");
3301
+ const content = readFileSync4(join5(repoRoot, filePath), "utf-8");
3131
3302
  const lines = content.split("\n");
3132
3303
  const added = lines.map((l) => `+${l}`).join("\n");
3133
3304
  return `--- /dev/null
@@ -3332,8 +3503,8 @@ function registerLogRoutes(server, opts) {
3332
3503
 
3333
3504
  // src/sandbox.ts
3334
3505
  import { randomUUID } from "crypto";
3335
- import { readFileSync as readFileSync4, readdirSync as readdirSync2 } from "fs";
3336
- import { join as join5, relative as relative2 } from "path";
3506
+ import { readFileSync as readFileSync5, readdirSync as readdirSync2 } from "fs";
3507
+ import { join as join6, relative as relative2 } from "path";
3337
3508
  import { Sandbox } from "@vercel/sandbox";
3338
3509
  var SKIP_DIRS = /* @__PURE__ */ new Set([
3339
3510
  "node_modules",
@@ -3349,13 +3520,13 @@ function collectFiles2(dir, base) {
3349
3520
  for (const entry of readdirSync2(dir, { withFileTypes: true })) {
3350
3521
  if (entry.name.startsWith(".") && SKIP_DIRS.has(entry.name)) continue;
3351
3522
  if (SKIP_DIRS.has(entry.name)) continue;
3352
- const fullPath = join5(dir, entry.name);
3523
+ const fullPath = join6(dir, entry.name);
3353
3524
  const relPath = relative2(base, fullPath);
3354
3525
  if (entry.isDirectory()) {
3355
3526
  files.push(...collectFiles2(fullPath, base));
3356
3527
  } else if (entry.isFile()) {
3357
3528
  if (SKIP_FILES.has(entry.name)) continue;
3358
- files.push({ path: relPath, content: readFileSync4(fullPath) });
3529
+ files.push({ path: relPath, content: readFileSync5(fullPath) });
3359
3530
  }
3360
3531
  }
3361
3532
  return files;
@@ -3367,6 +3538,57 @@ function extractHost(httpsUrl) {
3367
3538
  return "github.com";
3368
3539
  }
3369
3540
  }
3541
+ async function waitForServer(baseUrl, devServer) {
3542
+ const timeout = 6e4;
3543
+ const start = Date.now();
3544
+ const ac = new AbortController();
3545
+ let serverOutput = "";
3546
+ const logStream = (async () => {
3547
+ try {
3548
+ for await (const log of devServer.logs({ signal: ac.signal })) {
3549
+ const line = log.data;
3550
+ serverOutput += line;
3551
+ if (log.stream === "stderr") {
3552
+ process.stderr.write(` ${line}`);
3553
+ } else {
3554
+ process.stdout.write(` ${line}`);
3555
+ }
3556
+ }
3557
+ } catch {
3558
+ }
3559
+ })();
3560
+ while (Date.now() - start < timeout) {
3561
+ if (devServer.exitCode !== null) {
3562
+ ac.abort();
3563
+ await logStream.catch(() => {
3564
+ });
3565
+ return {
3566
+ ok: false,
3567
+ error: serverOutput.slice(-2e3) || `Process exited with code ${devServer.exitCode}`
3568
+ };
3569
+ }
3570
+ try {
3571
+ const res = await fetch(`${baseUrl}/via/health`, {
3572
+ signal: AbortSignal.timeout(3e3)
3573
+ });
3574
+ if (res.ok) {
3575
+ ac.abort();
3576
+ await logStream.catch(() => {
3577
+ });
3578
+ return { ok: true };
3579
+ }
3580
+ } catch {
3581
+ }
3582
+ await new Promise((r) => setTimeout(r, 2e3));
3583
+ }
3584
+ ac.abort();
3585
+ await logStream.catch(() => {
3586
+ });
3587
+ return {
3588
+ ok: false,
3589
+ error: serverOutput.slice(-2e3) || "Timed out waiting for dev server"
3590
+ };
3591
+ }
3370
3592
  async function deploySandbox(opts) {
3371
3593
  const token = randomUUID();
3372
3594
  const useGit = !!opts.git;
@@ -3418,7 +3640,11 @@ async function deploySandbox(opts) {
3418
3640
  "user.email",
3419
3641
  opts.git.userEmail
3420
3642
  ]);
3421
- await sandbox.runCommand("git", ["checkout", "-B", opts.git.branch]);
3643
+ const checkout = await sandbox.runCommand("git", ["checkout", "-B", opts.git.branch]);
3644
+ if (checkout.exitCode !== 0) {
3645
+ const err = await checkout.stderr();
3646
+ throw new Error(`git checkout failed (exit ${checkout.exitCode}): ${err}`);
3647
+ }
3422
3648
  await sandbox.runCommand("bash", [
3423
3649
  "-c",
3424
3650
  `echo 'https://x-access-token:${opts.git.token}@${extractHost(opts.git.remoteUrl)}' > ~/.git-credentials`
@@ -3480,20 +3706,30 @@ async function deploySandbox(opts) {
3480
3706
  content: Buffer.from(envLines.join("\n"))
3481
3707
  }
3482
3708
  ]);
3483
- const install = await sandbox.runCommand("npm", ["install"]);
3709
+ console.log(" Installing dependencies...");
3710
+ const install = await sandbox.runCommand({
3711
+ cmd: "npm",
3712
+ args: ["install"],
3713
+ stdout: process.stdout,
3714
+ stderr: process.stderr
3715
+ });
3484
3716
  if (install.exitCode !== 0) {
3485
- const stderr = await install.stderr();
3486
- throw new Error(
3487
- `npm install failed (exit ${install.exitCode}): ${stderr}`
3488
- );
3717
+ throw new Error(`npm install failed (exit ${install.exitCode})`);
3489
3718
  }
3490
- await sandbox.runCommand({
3719
+ const devServer = await sandbox.runCommand({
3491
3720
  cmd: "npm",
3492
3721
  args: ["run", "dev", "--", "--host", "0.0.0.0"],
3493
3722
  detached: true
3494
3723
  });
3724
+ console.log(" Starting dev server...");
3495
3725
  const baseUrl = sandbox.domain(5173);
3496
3726
  const url = `${baseUrl}/t/${token}`;
3727
+ const ready = await waitForServer(baseUrl, devServer);
3728
+ if (!ready.ok) {
3729
+ throw new Error(
3730
+ `Dev server failed to start: ${ready.error}`
3731
+ );
3732
+ }
3497
3733
  return {
3498
3734
  url,
3499
3735
  token,
@@ -3535,10 +3771,10 @@ function viagen(options) {
3535
3771
  projectRoot = config.root;
3536
3772
  logBuffer.init(projectRoot);
3537
3773
  wrapLogger(config.logger, logBuffer);
3538
- const viagenDir = join6(projectRoot, ".viagen");
3774
+ const viagenDir = join7(projectRoot, ".viagen");
3539
3775
  mkdirSync2(viagenDir, { recursive: true });
3540
3776
  writeFileSync4(
3541
- join6(viagenDir, "config.json"),
3777
+ join7(viagenDir, "config.json"),
3542
3778
  JSON.stringify({
3543
3779
  sandboxFiles: options?.sandboxFiles ?? [],
3544
3780
  editable: options?.editable ?? []
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "viagen",
3
- "version": "0.0.32",
3
+ "version": "0.0.36",
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",
@@ -44,7 +44,7 @@
44
44
  "@vercel/sandbox": "^1",
45
45
  "lucide-react": "^0.564.0",
46
46
  "simple-git": "^3.31.1",
47
- "viagen-sdk": "^0.0.2"
47
+ "viagen-sdk": "^0.0.4"
48
48
  },
49
49
  "license": "MIT",
50
50
  "repository": {