stashes 0.1.11 → 0.1.13

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/mcp.js CHANGED
@@ -347,7 +347,7 @@ class WorktreeManager {
347
347
  }
348
348
 
349
349
  // ../core/dist/persistence.js
350
- import { readFileSync, writeFileSync, mkdirSync as mkdirSync2, existsSync as existsSync3, rmSync as rmSync2 } from "fs";
350
+ import { readFileSync, writeFileSync, mkdirSync as mkdirSync2, existsSync as existsSync3, rmSync as rmSync2, readdirSync } from "fs";
351
351
  import { join as join3, dirname } from "path";
352
352
  var STASHES_DIR = ".stashes";
353
353
  function ensureDir(dirPath) {
@@ -424,14 +424,64 @@ class PersistenceService {
424
424
  const filePath = join3(this.basePath, "projects", projectId, "stashes.json");
425
425
  writeJson(filePath, stashes);
426
426
  }
427
- getChatHistory(projectId) {
428
- const filePath = join3(this.basePath, "projects", projectId, "chat.json");
429
- return readJson(filePath, []);
427
+ listChats(projectId) {
428
+ const dir = join3(this.basePath, "projects", projectId, "chats");
429
+ if (!existsSync3(dir))
430
+ return [];
431
+ const files = readdirSync(dir).filter((f) => f.endsWith(".json"));
432
+ return files.map((f) => {
433
+ const data = readJson(join3(dir, f), null);
434
+ return data?.chat;
435
+ }).filter(Boolean).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
436
+ }
437
+ getChat(projectId, chatId) {
438
+ const filePath = join3(this.basePath, "projects", projectId, "chats", `${chatId}.json`);
439
+ const data = readJson(filePath, null);
440
+ return data?.chat;
441
+ }
442
+ saveChat(chat) {
443
+ const filePath = join3(this.basePath, "projects", chat.projectId, "chats", `${chat.id}.json`);
444
+ const existing = readJson(filePath, { chat, messages: [] });
445
+ writeJson(filePath, { ...existing, chat });
446
+ }
447
+ deleteChat(projectId, chatId) {
448
+ const filePath = join3(this.basePath, "projects", projectId, "chats", `${chatId}.json`);
449
+ if (existsSync3(filePath)) {
450
+ rmSync2(filePath);
451
+ }
430
452
  }
431
- saveChatMessage(projectId, message) {
432
- const messages = [...this.getChatHistory(projectId), message];
433
- const filePath = join3(this.basePath, "projects", projectId, "chat.json");
434
- writeJson(filePath, messages);
453
+ getChatMessages(projectId, chatId) {
454
+ const filePath = join3(this.basePath, "projects", projectId, "chats", `${chatId}.json`);
455
+ const data = readJson(filePath, { messages: [] });
456
+ return data.messages;
457
+ }
458
+ saveChatMessage(projectId, chatId, message) {
459
+ const filePath = join3(this.basePath, "projects", projectId, "chats", `${chatId}.json`);
460
+ const data = readJson(filePath, { chat: this.getChat(projectId, chatId), messages: [] });
461
+ writeJson(filePath, { ...data, messages: [...data.messages, message] });
462
+ }
463
+ migrateOldChat(projectId) {
464
+ const oldPath = join3(this.basePath, "projects", projectId, "chat.json");
465
+ if (!existsSync3(oldPath))
466
+ return null;
467
+ const messages = readJson(oldPath, []);
468
+ if (messages.length === 0) {
469
+ rmSync2(oldPath);
470
+ return null;
471
+ }
472
+ const chatId = `chat_${crypto.randomUUID().substring(0, 8)}`;
473
+ const chat = {
474
+ id: chatId,
475
+ projectId,
476
+ title: "Initial conversation",
477
+ createdAt: messages[0].createdAt,
478
+ updatedAt: messages[messages.length - 1].createdAt
479
+ };
480
+ const filePath = join3(this.basePath, "projects", projectId, "chats", `${chatId}.json`);
481
+ writeJson(filePath, { chat, messages });
482
+ rmSync2(oldPath);
483
+ logger.info("persistence", `migrated old chat.json \u2192 ${chatId}`);
484
+ return chatId;
435
485
  }
436
486
  ensureGitignore(projectPath) {
437
487
  const gitignorePath = join3(projectPath, ".gitignore");
@@ -657,7 +707,7 @@ async function allocatePort() {
657
707
  throw new Error("No available ports in range 4010-4030");
658
708
  }
659
709
  async function generate(opts) {
660
- const { projectPath, projectId, prompt, component, count = DEFAULT_STASH_COUNT, directives = DEFAULT_DIRECTIVES, onProgress } = opts;
710
+ const { projectPath, projectId, chatId, prompt, component, count = DEFAULT_STASH_COUNT, directives = DEFAULT_DIRECTIVES, onProgress } = opts;
661
711
  const worktreeManager = new WorktreeManager(projectPath);
662
712
  const persistence = new PersistenceService(projectPath);
663
713
  const selectedDirectives = directives.slice(0, count);
@@ -669,12 +719,17 @@ async function generate(opts) {
669
719
  }
670
720
  }
671
721
  const completedStashes = [];
672
- const stashPromises = selectedDirectives.map(async (directive) => {
722
+ const existingStashes = persistence.listStashes(projectId);
723
+ const maxNumber = existingStashes.reduce((max, s) => Math.max(max, s.number ?? 0), 0);
724
+ const stashPromises = selectedDirectives.map(async (directive, idx) => {
673
725
  const stashId = `stash_${crypto.randomUUID().substring(0, 8)}`;
726
+ const stashNumber = maxNumber + idx + 1;
674
727
  const worktree = await worktreeManager.createForGeneration(stashId);
675
728
  const stash = {
676
729
  id: stashId,
730
+ number: stashNumber,
677
731
  projectId,
732
+ originChatId: chatId,
678
733
  prompt,
679
734
  componentPath: component?.filePath,
680
735
  branch: worktree.branch,
@@ -687,7 +742,7 @@ async function generate(opts) {
687
742
  createdAt: new Date().toISOString()
688
743
  };
689
744
  persistence.saveStash(stash);
690
- emit(onProgress, { type: "generating", stashId });
745
+ emit(onProgress, { type: "generating", stashId, number: stashNumber });
691
746
  let stashPrompt;
692
747
  if (component?.filePath) {
693
748
  stashPrompt = buildStashPrompt({ name: component.exportName || component.filePath, filePath: component.filePath, domSelector: "" }, sourceCode, prompt, directive);
@@ -776,7 +831,7 @@ async function allocatePort2() {
776
831
  throw new Error("No available ports in range 4010-4030");
777
832
  }
778
833
  async function vary(opts) {
779
- const { projectPath, sourceStashId, prompt, onProgress } = opts;
834
+ const { projectPath, sourceStashId, chatId, prompt, onProgress } = opts;
780
835
  const persistence = new PersistenceService(projectPath);
781
836
  const worktreeManager = new WorktreeManager(projectPath);
782
837
  let sourceStash;
@@ -792,10 +847,14 @@ async function vary(opts) {
792
847
  if (!sourceStash)
793
848
  throw new Error(`Source stash ${sourceStashId} not found`);
794
849
  const stashId = `stash_${crypto.randomUUID().substring(0, 8)}`;
850
+ const existingStashes = persistence.listStashes(projectId);
851
+ const stashNumber = existingStashes.reduce((max, s) => Math.max(max, s.number ?? 0), 0) + 1;
795
852
  const worktree = await worktreeManager.createForVary(stashId, sourceStash.branch);
796
853
  const stash = {
797
854
  id: stashId,
855
+ number: stashNumber,
798
856
  projectId,
857
+ originChatId: chatId,
799
858
  prompt,
800
859
  componentPath: sourceStash.componentPath,
801
860
  branch: worktree.branch,
@@ -808,7 +867,7 @@ async function vary(opts) {
808
867
  createdAt: new Date().toISOString()
809
868
  };
810
869
  persistence.saveStash(stash);
811
- emit2(onProgress, { type: "generating", stashId });
870
+ emit2(onProgress, { type: "generating", stashId, number: stashNumber });
812
871
  const varyPrompt = `The user wants to vary the current UI. Apply this change: ${prompt}`;
813
872
  const aiProcess = startAiProcess(stashId, varyPrompt, worktree.path);
814
873
  try {
@@ -1052,7 +1111,7 @@ import { existsSync as existsSync8, readFileSync as readFileSync5 } from "fs";
1052
1111
 
1053
1112
  // ../server/dist/routes/api.js
1054
1113
  import { Hono } from "hono";
1055
- import { join as join6 } from "path";
1114
+ import { join as join6, basename } from "path";
1056
1115
  import { existsSync as existsSync6, readFileSync as readFileSync3 } from "fs";
1057
1116
  var app = new Hono;
1058
1117
  app.get("/health", (c) => c.json({ status: "ok", service: "stashes" }));
@@ -1084,14 +1143,54 @@ app.get("/projects/:id", (c) => {
1084
1143
  if (!project)
1085
1144
  return c.json({ error: "Project not found" }, 404);
1086
1145
  const stashes = persistence.listStashes(project.id);
1087
- const chat = persistence.getChatHistory(project.id);
1088
- return c.json({ data: { ...project, stashes, chat } });
1146
+ const chats = persistence.listChats(project.id);
1147
+ return c.json({ data: { ...project, stashes, chats } });
1089
1148
  });
1090
1149
  app.delete("/projects/:id", (c) => {
1091
1150
  const id = c.req.param("id");
1092
1151
  getPersistence().deleteProject(id);
1093
1152
  return c.json({ data: { deleted: id } });
1094
1153
  });
1154
+ app.get("/chats", (c) => {
1155
+ const persistence = getPersistence();
1156
+ const project = ensureProject(persistence);
1157
+ const chats = persistence.listChats(project.id);
1158
+ const stashes = persistence.listStashes(project.id);
1159
+ return c.json({ data: { project, chats, stashCount: stashes.length } });
1160
+ });
1161
+ app.post("/chats", async (c) => {
1162
+ const persistence = getPersistence();
1163
+ const project = ensureProject(persistence);
1164
+ const { title } = await c.req.json();
1165
+ const chatCount = persistence.listChats(project.id).length;
1166
+ const chat = {
1167
+ id: `chat_${crypto.randomUUID().substring(0, 8)}`,
1168
+ projectId: project.id,
1169
+ title: title?.trim() || `Chat ${chatCount + 1}`,
1170
+ createdAt: new Date().toISOString(),
1171
+ updatedAt: new Date().toISOString()
1172
+ };
1173
+ persistence.saveChat(chat);
1174
+ return c.json({ data: chat }, 201);
1175
+ });
1176
+ app.get("/chats/:chatId", (c) => {
1177
+ const persistence = getPersistence();
1178
+ const project = ensureProject(persistence);
1179
+ const chatId = c.req.param("chatId");
1180
+ const chat = persistence.getChat(project.id, chatId);
1181
+ if (!chat)
1182
+ return c.json({ error: "Chat not found" }, 404);
1183
+ const messages = persistence.getChatMessages(project.id, chatId);
1184
+ const stashes = persistence.listStashes(project.id).filter((s) => s.originChatId === chatId);
1185
+ return c.json({ data: { ...chat, messages, stashes } });
1186
+ });
1187
+ app.delete("/chats/:chatId", (c) => {
1188
+ const persistence = getPersistence();
1189
+ const project = ensureProject(persistence);
1190
+ const chatId = c.req.param("chatId");
1191
+ persistence.deleteChat(project.id, chatId);
1192
+ return c.json({ data: { deleted: chatId } });
1193
+ });
1095
1194
  app.get("/screenshots/:filename", (c) => {
1096
1195
  const filename = c.req.param("filename");
1097
1196
  const filePath = join6(serverState.projectPath, ".stashes", "screenshots", filename);
@@ -1102,6 +1201,20 @@ app.get("/screenshots/:filename", (c) => {
1102
1201
  headers: { "content-type": "image/png", "cache-control": "no-cache" }
1103
1202
  });
1104
1203
  });
1204
+ function ensureProject(persistence) {
1205
+ const projects = persistence.listProjects();
1206
+ if (projects.length > 0)
1207
+ return projects[0];
1208
+ const project = {
1209
+ id: `proj_${crypto.randomUUID().substring(0, 8)}`,
1210
+ name: basename(serverState.projectPath),
1211
+ createdAt: new Date().toISOString(),
1212
+ updatedAt: new Date().toISOString()
1213
+ };
1214
+ persistence.saveProject(project);
1215
+ persistence.migrateOldChat(project.id);
1216
+ return project;
1217
+ }
1105
1218
  var apiRoutes = app;
1106
1219
 
1107
1220
  // ../server/dist/services/stash-service.js
@@ -1347,8 +1460,8 @@ class StashService {
1347
1460
  });
1348
1461
  }
1349
1462
  }
1350
- async chat(projectId, message, referenceStashIds) {
1351
- const component = this.selectedComponent;
1463
+ async message(projectId, chatId, message, referenceStashIds, componentContext) {
1464
+ const component = componentContext ? { name: componentContext.name, filePath: this.selectedComponent?.filePath || "" } : this.selectedComponent;
1352
1465
  let sourceCode = "";
1353
1466
  const filePath = component?.filePath || "";
1354
1467
  if (filePath && filePath !== "auto-detect") {
@@ -1368,8 +1481,11 @@ ${refs.join(`
1368
1481
  }
1369
1482
  }
1370
1483
  const chatPrompt = [
1371
- "The user is asking about their UI project. Answer concisely.",
1372
- "Do NOT modify any files.",
1484
+ "You are helping the user explore UI design variations for their project.",
1485
+ "You have access to stashes MCP tools to generate, list, browse, vary, and apply stashes.",
1486
+ "If the user asks you to generate, create, or make variations, use the stashes_generate tool.",
1487
+ "If the user asks to vary an existing stash, use the stashes_vary tool.",
1488
+ "Otherwise, respond conversationally about their project and stashes.",
1373
1489
  "",
1374
1490
  component ? `Component: ${component.name}` : "",
1375
1491
  filePath !== "auto-detect" ? `File: ${filePath}` : "",
@@ -1380,7 +1496,7 @@ ${sourceCode.substring(0, 3000)}
1380
1496
  \`\`\`` : "",
1381
1497
  stashContext,
1382
1498
  "",
1383
- `User question: ${message}`
1499
+ `User: ${message}`
1384
1500
  ].filter(Boolean).join(`
1385
1501
  `);
1386
1502
  const aiProcess = startAiProcess("chat", chatPrompt, this.projectPath);
@@ -1389,12 +1505,60 @@ ${sourceCode.substring(0, 3000)}
1389
1505
  for await (const chunk of parseClaudeStream(aiProcess.process)) {
1390
1506
  if (chunk.type === "text") {
1391
1507
  fullResponse += chunk.content;
1392
- this.broadcast({ type: "ai_stream", content: chunk.content, streamType: "text", source: "chat" });
1508
+ this.broadcast({
1509
+ type: "ai_stream",
1510
+ content: chunk.content,
1511
+ streamType: "text",
1512
+ source: "chat"
1513
+ });
1514
+ } else if (chunk.type === "thinking") {
1515
+ this.broadcast({
1516
+ type: "ai_stream",
1517
+ content: chunk.content,
1518
+ streamType: "thinking",
1519
+ source: "chat"
1520
+ });
1521
+ } else if (chunk.type === "tool_use") {
1522
+ let toolName = "unknown";
1523
+ let toolParams = {};
1524
+ try {
1525
+ const parsed = JSON.parse(chunk.content);
1526
+ toolName = parsed.tool || parsed.name || "unknown";
1527
+ toolParams = parsed.input || parsed.params || {};
1528
+ } catch {}
1529
+ this.broadcast({
1530
+ type: "ai_stream",
1531
+ content: chunk.content,
1532
+ streamType: "tool_start",
1533
+ source: "chat",
1534
+ toolName,
1535
+ toolParams,
1536
+ toolStatus: "running"
1537
+ });
1538
+ } else if (chunk.type === "tool_result") {
1539
+ let toolName = "unknown";
1540
+ let toolResult = "";
1541
+ try {
1542
+ const parsed = JSON.parse(chunk.content);
1543
+ toolName = parsed.tool || parsed.name || "unknown";
1544
+ toolResult = typeof parsed.result === "string" ? parsed.result.substring(0, 200) : JSON.stringify(parsed.result).substring(0, 200);
1545
+ } catch {
1546
+ toolResult = chunk.content.substring(0, 200);
1547
+ }
1548
+ this.broadcast({
1549
+ type: "ai_stream",
1550
+ content: chunk.content,
1551
+ streamType: "tool_end",
1552
+ source: "chat",
1553
+ toolName,
1554
+ toolStatus: "completed",
1555
+ toolResult
1556
+ });
1393
1557
  }
1394
1558
  }
1395
1559
  await aiProcess.process.exited;
1396
1560
  if (fullResponse) {
1397
- this.persistence.saveChatMessage(projectId, {
1561
+ this.persistence.saveChatMessage(projectId, chatId, {
1398
1562
  id: crypto.randomUUID(),
1399
1563
  role: "assistant",
1400
1564
  content: fullResponse,
@@ -1405,7 +1569,7 @@ ${sourceCode.substring(0, 3000)}
1405
1569
  } catch (err) {
1406
1570
  this.broadcast({
1407
1571
  type: "ai_stream",
1408
- content: `Chat error: ${err instanceof Error ? err.message : String(err)}`,
1572
+ content: `Error: ${err instanceof Error ? err.message : String(err)}`,
1409
1573
  streamType: "text",
1410
1574
  source: "chat"
1411
1575
  });
@@ -1418,7 +1582,12 @@ ${sourceCode.substring(0, 3000)}
1418
1582
  case "generating":
1419
1583
  case "screenshotting":
1420
1584
  case "ready":
1421
- this.broadcast({ type: "stash:status", stashId: event.stashId, status: event.type === "ready" ? "ready" : event.type });
1585
+ this.broadcast({
1586
+ type: "stash:status",
1587
+ stashId: event.stashId,
1588
+ status: event.type === "ready" ? "ready" : event.type,
1589
+ ..."number" in event ? { number: event.number } : {}
1590
+ });
1422
1591
  if (event.type === "ready" && "screenshotPath" in event && event.screenshotPath) {
1423
1592
  this.broadcast({ type: "stash:screenshot", stashId: event.stashId, url: event.screenshotPath });
1424
1593
  }
@@ -1426,9 +1595,11 @@ ${sourceCode.substring(0, 3000)}
1426
1595
  case "error":
1427
1596
  this.broadcast({ type: "stash:error", stashId: event.stashId, error: event.error });
1428
1597
  break;
1429
- case "ai_stream":
1430
- this.broadcast({ type: "ai_stream", content: event.content, streamType: event.streamType });
1598
+ case "ai_stream": {
1599
+ const streamType = event.streamType === "tool_use" ? "tool_start" : event.streamType;
1600
+ this.broadcast({ type: "ai_stream", content: event.content, streamType });
1431
1601
  break;
1602
+ }
1432
1603
  }
1433
1604
  }
1434
1605
  async generate(projectId, prompt, stashCount = DEFAULT_STASH_COUNT, referenceStashIds) {
@@ -1503,7 +1674,14 @@ function createWebSocketHandler(projectPath, userDevPort, appProxyPort) {
1503
1674
  open(ws) {
1504
1675
  clients.add(ws);
1505
1676
  logger.info("ws", "client connected", { total: clients.size });
1506
- ws.send(JSON.stringify({ type: "server_ready", port: userDevPort, appProxyPort }));
1677
+ const project = ensureProject(persistence);
1678
+ ws.send(JSON.stringify({
1679
+ type: "server_ready",
1680
+ port: userDevPort,
1681
+ appProxyPort,
1682
+ projectId: project.id,
1683
+ projectName: project.name
1684
+ }));
1507
1685
  },
1508
1686
  async message(ws, message) {
1509
1687
  const raw = typeof message === "string" ? message : new TextDecoder().decode(message);
@@ -1522,28 +1700,17 @@ function createWebSocketHandler(projectPath, userDevPort, appProxyPort) {
1522
1700
  case "select_component":
1523
1701
  stashService.setSelectedComponent(event.component);
1524
1702
  break;
1525
- case "chat":
1526
- persistence.saveChatMessage(event.projectId, {
1703
+ case "message":
1704
+ persistence.saveChatMessage(event.projectId, event.chatId, {
1527
1705
  id: crypto.randomUUID(),
1528
1706
  role: "user",
1529
1707
  content: event.message,
1530
1708
  type: "text",
1531
- createdAt: new Date().toISOString()
1532
- });
1533
- await stashService.chat(event.projectId, event.message, event.referenceStashIds);
1534
- break;
1535
- case "generate":
1536
- persistence.saveChatMessage(event.projectId, {
1537
- id: crypto.randomUUID(),
1538
- role: "user",
1539
- content: event.prompt,
1540
- type: "text",
1541
- createdAt: new Date().toISOString()
1709
+ createdAt: new Date().toISOString(),
1710
+ referenceStashIds: event.referenceStashIds,
1711
+ componentContext: event.componentContext
1542
1712
  });
1543
- await stashService.generate(event.projectId, event.prompt, event.stashCount, event.referenceStashIds);
1544
- break;
1545
- case "vary":
1546
- await stashService.vary(event.sourceStashId, event.prompt);
1713
+ await stashService.message(event.projectId, event.chatId, event.message, event.referenceStashIds, event.componentContext);
1547
1714
  break;
1548
1715
  case "interact":
1549
1716
  await stashService.switchPreview(event.stashId, event.sortedStashIds);