stashes 0.1.51 → 0.1.53

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -19,8 +19,8 @@ var __toESM = (mod, isNodeMode, target) => {
19
19
  var __require = import.meta.require;
20
20
 
21
21
  // src/index.ts
22
- import { readFileSync as readFileSync9 } from "fs";
23
- import { join as join13, dirname as dirname5 } from "path";
22
+ import { readFileSync as readFileSync10 } from "fs";
23
+ import { join as join15, dirname as dirname6 } from "path";
24
24
  import { fileURLToPath as fileURLToPath3 } from "url";
25
25
  import { Command } from "commander";
26
26
 
@@ -31,9 +31,9 @@ import open from "open";
31
31
  // ../server/dist/index.js
32
32
  import { Hono as Hono2 } from "hono";
33
33
  import { cors } from "hono/cors";
34
- import { join as join9, dirname as dirname2 } from "path";
34
+ import { join as join11, dirname as dirname3 } from "path";
35
35
  import { fileURLToPath } from "url";
36
- import { existsSync as existsSync9, readFileSync as readFileSync5 } from "fs";
36
+ import { existsSync as existsSync11, readFileSync as readFileSync6 } from "fs";
37
37
  // ../shared/dist/constants/index.js
38
38
  var STASHES_PORT = 4000;
39
39
  var DEFAULT_STASH_COUNT = 3;
@@ -56,156 +56,23 @@ var DEFAULT_DIRECTIVES = [
56
56
  ];
57
57
  // ../server/dist/routes/api.js
58
58
  import { Hono } from "hono";
59
- import { join, basename } from "path";
60
- import { existsSync, readFileSync } from "fs";
61
- var app = new Hono;
62
- app.get("/health", (c) => c.json({ status: "ok", service: "stashes" }));
63
- app.get("/projects", (c) => {
64
- const persistence = getPersistence();
65
- const projects = persistence.listProjects();
66
- const projectsWithCounts = projects.map((p) => ({
67
- ...p,
68
- stashCount: persistence.listStashes(p.id).length,
69
- recentScreenshots: persistence.listStashes(p.id).filter((s) => s.screenshotUrl).slice(-4).map((s) => s.screenshotUrl)
70
- }));
71
- return c.json({ data: projectsWithCounts });
72
- });
73
- app.post("/projects", async (c) => {
74
- const { name, description } = await c.req.json();
75
- const project = {
76
- id: `proj_${crypto.randomUUID().substring(0, 8)}`,
77
- name,
78
- description,
79
- createdAt: new Date().toISOString(),
80
- updatedAt: new Date().toISOString()
81
- };
82
- getPersistence().saveProject(project);
83
- return c.json({ data: project }, 201);
84
- });
85
- app.get("/projects/:id", (c) => {
86
- const persistence = getPersistence();
87
- const project = persistence.getProject(c.req.param("id"));
88
- if (!project)
89
- return c.json({ error: "Project not found" }, 404);
90
- const stashes = persistence.listStashes(project.id);
91
- const chats = persistence.listChats(project.id);
92
- return c.json({ data: { ...project, stashes, chats } });
93
- });
94
- app.delete("/projects/:id", (c) => {
95
- const id = c.req.param("id");
96
- getPersistence().deleteProject(id);
97
- return c.json({ data: { deleted: id } });
98
- });
99
- app.get("/chats", (c) => {
100
- const persistence = getPersistence();
101
- const project = ensureProject(persistence);
102
- const chats = persistence.listChats(project.id);
103
- const stashes = persistence.listStashes(project.id);
104
- return c.json({ data: { project, chats, stashes } });
105
- });
106
- app.post("/chats", async (c) => {
107
- const persistence = getPersistence();
108
- const project = ensureProject(persistence);
109
- const { title, referencedStashIds } = await c.req.json();
110
- const chatCount = persistence.listChats(project.id).length;
111
- const chat = {
112
- id: `chat_${crypto.randomUUID().substring(0, 8)}`,
113
- projectId: project.id,
114
- title: title?.trim() || `Chat ${chatCount + 1}`,
115
- referencedStashIds: referencedStashIds ?? [],
116
- createdAt: new Date().toISOString(),
117
- updatedAt: new Date().toISOString()
118
- };
119
- persistence.saveChat(chat);
120
- return c.json({ data: chat }, 201);
121
- });
122
- app.patch("/chats/:chatId", async (c) => {
123
- const persistence = getPersistence();
124
- const project = ensureProject(persistence);
125
- const chatId = c.req.param("chatId");
126
- const chat = persistence.getChat(project.id, chatId);
127
- if (!chat)
128
- return c.json({ error: "Chat not found" }, 404);
129
- const body = await c.req.json();
130
- const updated = {
131
- ...chat,
132
- ...body.referencedStashIds !== undefined ? { referencedStashIds: body.referencedStashIds } : {},
133
- updatedAt: new Date().toISOString()
134
- };
135
- persistence.saveChat(updated);
136
- return c.json({ data: updated });
137
- });
138
- app.get("/chats/:chatId", (c) => {
139
- const persistence = getPersistence();
140
- const project = ensureProject(persistence);
141
- const chatId = c.req.param("chatId");
142
- const chat = persistence.getChat(project.id, chatId);
143
- if (!chat)
144
- return c.json({ error: "Chat not found" }, 404);
145
- const messages = persistence.getChatMessages(project.id, chatId);
146
- const refIds = new Set(chat.referencedStashIds ?? []);
147
- const stashes = persistence.listStashes(project.id).filter((s) => s.originChatId === chatId || refIds.has(s.id));
148
- return c.json({ data: { ...chat, messages, stashes } });
149
- });
150
- app.delete("/chats/:chatId", (c) => {
151
- const persistence = getPersistence();
152
- const project = ensureProject(persistence);
153
- const chatId = c.req.param("chatId");
154
- persistence.deleteChat(project.id, chatId);
155
- return c.json({ data: { deleted: chatId } });
156
- });
157
- app.get("/dev-server-status", async (c) => {
158
- const port = serverState.userDevPort;
159
- try {
160
- const res = await fetch(`http://localhost:${port}`, {
161
- method: "HEAD",
162
- signal: AbortSignal.timeout(2000)
163
- });
164
- return c.json({ up: res.status < 500, port });
165
- } catch {
166
- return c.json({ up: false, port });
167
- }
168
- });
169
- app.get("/screenshots/:filename", (c) => {
170
- const filename = c.req.param("filename");
171
- const filePath = join(serverState.projectPath, ".stashes", "screenshots", filename);
172
- if (!existsSync(filePath))
173
- return c.json({ error: "Not found" }, 404);
174
- const content = readFileSync(filePath);
175
- return new Response(content, {
176
- headers: { "content-type": "image/png", "cache-control": "no-cache" }
177
- });
178
- });
179
- function ensureProject(persistence) {
180
- const projects = persistence.listProjects();
181
- if (projects.length > 0)
182
- return projects[0];
183
- const project = {
184
- id: `proj_${crypto.randomUUID().substring(0, 8)}`,
185
- name: basename(serverState.projectPath),
186
- createdAt: new Date().toISOString(),
187
- updatedAt: new Date().toISOString()
188
- };
189
- persistence.saveProject(project);
190
- persistence.migrateOldChat(project.id);
191
- return project;
192
- }
193
- var apiRoutes = app;
59
+ import { join as join10, basename } from "path";
60
+ import { existsSync as existsSync10, readFileSync as readFileSync5 } from "fs";
194
61
 
195
62
  // ../core/dist/generation.js
196
- import { readFileSync as readFileSync3, existsSync as existsSync7 } from "fs";
63
+ import { readFileSync as readFileSync2, existsSync as existsSync7 } from "fs";
197
64
  import { join as join7 } from "path";
198
65
  var {spawn: spawn3 } = globalThis.Bun;
199
66
  import simpleGit3 from "simple-git";
200
67
 
201
68
  // ../core/dist/worktree.js
202
69
  import simpleGit from "simple-git";
203
- import { join as join3 } from "path";
204
- import { existsSync as existsSync3, rmSync, symlinkSync } from "fs";
70
+ import { join as join2 } from "path";
71
+ import { existsSync as existsSync2, rmSync, symlinkSync } from "fs";
205
72
 
206
73
  // ../core/dist/logger.js
207
- import { appendFileSync, mkdirSync, existsSync as existsSync2 } from "fs";
208
- import { join as join2 } from "path";
74
+ import { appendFileSync, mkdirSync, existsSync } from "fs";
75
+ import { join } from "path";
209
76
  var COLORS = {
210
77
  reset: "\x1B[0m",
211
78
  dim: "\x1B[2m",
@@ -229,12 +96,12 @@ var LEVEL_LABELS = {
229
96
  };
230
97
  var logFilePath = null;
231
98
  function initLogFile(projectPath) {
232
- const logDir = join2(projectPath, ".stashes", "logs");
233
- if (!existsSync2(logDir)) {
99
+ const logDir = join(projectPath, ".stashes", "logs");
100
+ if (!existsSync(logDir)) {
234
101
  mkdirSync(logDir, { recursive: true });
235
102
  }
236
103
  const date = new Date().toISOString().substring(0, 10);
237
- logFilePath = join2(logDir, `stashes-${date}.log`);
104
+ logFilePath = join(logDir, `stashes-${date}.log`);
238
105
  }
239
106
  function ts() {
240
107
  return new Date().toISOString().substring(11, 23);
@@ -274,8 +141,8 @@ class WorktreeManager {
274
141
  this.cleanupStaleWorktrees();
275
142
  }
276
143
  async cleanupStaleWorktrees() {
277
- const worktreesDir = join3(this.projectPath, ".stashes", "worktrees");
278
- if (!existsSync3(worktreesDir))
144
+ const worktreesDir = join2(this.projectPath, ".stashes", "worktrees");
145
+ if (!existsSync2(worktreesDir))
279
146
  return;
280
147
  try {
281
148
  const { readdirSync } = await import("fs");
@@ -285,7 +152,7 @@ class WorktreeManager {
285
152
  logger.info("worktree", `skipping active screenshot worktree: ${entry}`);
286
153
  continue;
287
154
  }
288
- const worktreePath = join3(worktreesDir, entry);
155
+ const worktreePath = join2(worktreesDir, entry);
289
156
  logger.info("worktree", `cleaning up stale worktree: ${entry}`);
290
157
  try {
291
158
  await this.git.raw(["worktree", "remove", "--force", worktreePath]);
@@ -303,8 +170,8 @@ class WorktreeManager {
303
170
  }
304
171
  async createForGeneration(stashId) {
305
172
  const branch = `stashes/${stashId}`;
306
- const worktreePath = join3(this.projectPath, ".stashes", "worktrees", stashId);
307
- if (existsSync3(worktreePath)) {
173
+ const worktreePath = join2(this.projectPath, ".stashes", "worktrees", stashId);
174
+ if (existsSync2(worktreePath)) {
308
175
  await this.removeGeneration(stashId);
309
176
  }
310
177
  logger.info("worktree", `creating generation: ${stashId}`, { branch, path: worktreePath });
@@ -317,8 +184,8 @@ class WorktreeManager {
317
184
  }
318
185
  async createForVary(stashId, sourceBranch) {
319
186
  const branch = `stashes/${stashId}`;
320
- const worktreePath = join3(this.projectPath, ".stashes", "worktrees", stashId);
321
- if (existsSync3(worktreePath)) {
187
+ const worktreePath = join2(this.projectPath, ".stashes", "worktrees", stashId);
188
+ if (existsSync2(worktreePath)) {
322
189
  await this.removeGeneration(stashId);
323
190
  }
324
191
  logger.info("worktree", `creating vary: ${stashId}`, { sourceBranch, branch });
@@ -331,20 +198,20 @@ class WorktreeManager {
331
198
  }
332
199
  async removeGeneration(stashId) {
333
200
  const info = this.worktrees.get(stashId);
334
- const worktreePath = info?.path ?? join3(this.projectPath, ".stashes", "worktrees", stashId);
201
+ const worktreePath = info?.path ?? join2(this.projectPath, ".stashes", "worktrees", stashId);
335
202
  logger.info("worktree", `removing generation worktree: ${stashId}`);
336
203
  try {
337
204
  await this.git.raw(["worktree", "remove", "--force", worktreePath]);
338
205
  } catch {
339
- if (existsSync3(worktreePath)) {
206
+ if (existsSync2(worktreePath)) {
340
207
  rmSync(worktreePath, { recursive: true, force: true });
341
208
  }
342
209
  }
343
210
  this.worktrees.delete(stashId);
344
211
  }
345
212
  async createPreview() {
346
- const previewPath = join3(this.projectPath, ".stashes", "preview");
347
- if (existsSync3(previewPath)) {
213
+ const previewPath = join2(this.projectPath, ".stashes", "preview");
214
+ if (existsSync2(previewPath)) {
348
215
  try {
349
216
  await this.git.raw(["worktree", "remove", "--force", previewPath]);
350
217
  } catch {
@@ -370,11 +237,11 @@ class WorktreeManager {
370
237
  const msg = err instanceof Error ? err.message : String(err);
371
238
  if (msg.includes("is already used by worktree")) {
372
239
  logger.warn("worktree", `stale worktree blocking ${branch}, cleaning up`);
373
- const staleDir = join3(this.projectPath, ".stashes", "worktrees", stashId);
240
+ const staleDir = join2(this.projectPath, ".stashes", "worktrees", stashId);
374
241
  try {
375
242
  await this.git.raw(["worktree", "remove", "--force", staleDir]);
376
243
  } catch {
377
- if (existsSync3(staleDir)) {
244
+ if (existsSync2(staleDir)) {
378
245
  rmSync(staleDir, { recursive: true, force: true });
379
246
  }
380
247
  }
@@ -393,9 +260,9 @@ class WorktreeManager {
393
260
  return PREVIEW_PORT;
394
261
  }
395
262
  async createPreviewForPool(stashId) {
396
- const previewPath = join3(this.projectPath, ".stashes", "previews", stashId);
263
+ const previewPath = join2(this.projectPath, ".stashes", "previews", stashId);
397
264
  const branch = `stashes/${stashId}`;
398
- if (existsSync3(previewPath)) {
265
+ if (existsSync2(previewPath)) {
399
266
  try {
400
267
  await this.git.raw(["worktree", "remove", "--force", previewPath]);
401
268
  } catch {
@@ -410,10 +277,10 @@ class WorktreeManager {
410
277
  const msg = err instanceof Error ? err.message : String(err);
411
278
  if (msg.includes("is already used by worktree")) {
412
279
  logger.warn("worktree", `branch ${branch} locked by stale worktree, cleaning up`);
413
- const staleDir = join3(this.projectPath, ".stashes", "worktrees", stashId);
414
- const legacyPreview = join3(this.projectPath, ".stashes", "preview");
280
+ const staleDir = join2(this.projectPath, ".stashes", "worktrees", stashId);
281
+ const legacyPreview = join2(this.projectPath, ".stashes", "preview");
415
282
  for (const dir of [staleDir, legacyPreview]) {
416
- if (existsSync3(dir)) {
283
+ if (existsSync2(dir)) {
417
284
  try {
418
285
  await this.git.raw(["worktree", "remove", "--force", dir]);
419
286
  } catch {
@@ -432,12 +299,12 @@ class WorktreeManager {
432
299
  return previewPath;
433
300
  }
434
301
  async removePreviewForPool(stashId) {
435
- const previewPath = join3(this.projectPath, ".stashes", "previews", stashId);
302
+ const previewPath = join2(this.projectPath, ".stashes", "previews", stashId);
436
303
  logger.info("worktree", `removing pool preview: ${stashId}`);
437
304
  try {
438
305
  await this.git.raw(["worktree", "remove", "--force", previewPath]);
439
306
  } catch {
440
- if (existsSync3(previewPath)) {
307
+ if (existsSync2(previewPath)) {
441
308
  rmSync(previewPath, { recursive: true, force: true });
442
309
  }
443
310
  }
@@ -454,7 +321,7 @@ class WorktreeManager {
454
321
  try {
455
322
  await this.git.raw(["worktree", "remove", "--force", this.previewPath]);
456
323
  } catch {
457
- if (existsSync3(this.previewPath)) {
324
+ if (existsSync2(this.previewPath)) {
458
325
  rmSync(this.previewPath, { recursive: true, force: true });
459
326
  }
460
327
  }
@@ -474,16 +341,16 @@ class WorktreeManager {
474
341
  }
475
342
  }
476
343
  } catch {}
477
- const worktreesDir = join3(this.projectPath, ".stashes", "worktrees");
478
- if (existsSync3(worktreesDir)) {
344
+ const worktreesDir = join2(this.projectPath, ".stashes", "worktrees");
345
+ if (existsSync2(worktreesDir)) {
479
346
  rmSync(worktreesDir, { recursive: true, force: true });
480
347
  }
481
- const previewDir = join3(this.projectPath, ".stashes", "preview");
482
- if (existsSync3(previewDir)) {
348
+ const previewDir = join2(this.projectPath, ".stashes", "preview");
349
+ if (existsSync2(previewDir)) {
483
350
  rmSync(previewDir, { recursive: true, force: true });
484
351
  }
485
- const previewsDir = join3(this.projectPath, ".stashes", "previews");
486
- if (existsSync3(previewsDir)) {
352
+ const previewsDir = join2(this.projectPath, ".stashes", "previews");
353
+ if (existsSync2(previewsDir)) {
487
354
  rmSync(previewsDir, { recursive: true, force: true });
488
355
  }
489
356
  logger.info("worktree", `cleanup complete`);
@@ -491,9 +358,9 @@ class WorktreeManager {
491
358
  symlinkDeps(worktreePath) {
492
359
  const symlinks = ["node_modules", ".env", ".env.local"];
493
360
  for (const name of symlinks) {
494
- const source = join3(this.projectPath, name);
495
- const target = join3(worktreePath, name);
496
- if (existsSync3(source) && !existsSync3(target)) {
361
+ const source = join2(this.projectPath, name);
362
+ const target = join2(worktreePath, name);
363
+ if (existsSync2(source) && !existsSync2(target)) {
497
364
  try {
498
365
  symlinkSync(source, target);
499
366
  } catch {}
@@ -503,19 +370,19 @@ class WorktreeManager {
503
370
  }
504
371
 
505
372
  // ../core/dist/persistence.js
506
- import { readFileSync as readFileSync2, writeFileSync, mkdirSync as mkdirSync2, existsSync as existsSync4, rmSync as rmSync2, readdirSync } from "fs";
507
- import { join as join4, dirname } from "path";
373
+ import { readFileSync, writeFileSync, mkdirSync as mkdirSync2, existsSync as existsSync3, rmSync as rmSync2, readdirSync } from "fs";
374
+ import { join as join3, dirname } from "path";
508
375
  var STASHES_DIR = ".stashes";
509
376
  function ensureDir(dirPath) {
510
- if (!existsSync4(dirPath)) {
377
+ if (!existsSync3(dirPath)) {
511
378
  mkdirSync2(dirPath, { recursive: true });
512
379
  }
513
380
  }
514
381
  function readJson(filePath, fallback) {
515
- if (!existsSync4(filePath))
382
+ if (!existsSync3(filePath))
516
383
  return fallback;
517
384
  try {
518
- return JSON.parse(readFileSync2(filePath, "utf-8"));
385
+ return JSON.parse(readFileSync(filePath, "utf-8"));
519
386
  } catch {
520
387
  logger.warn("persistence", `failed to read ${filePath}, using fallback`);
521
388
  return fallback;
@@ -529,12 +396,12 @@ function writeJson(filePath, data) {
529
396
  class PersistenceService {
530
397
  basePath;
531
398
  constructor(projectPath) {
532
- this.basePath = join4(projectPath, STASHES_DIR);
399
+ this.basePath = join3(projectPath, STASHES_DIR);
533
400
  ensureDir(this.basePath);
534
401
  this.ensureGitignore(projectPath);
535
402
  }
536
403
  listProjects() {
537
- return readJson(join4(this.basePath, "projects.json"), []);
404
+ return readJson(join3(this.basePath, "projects.json"), []);
538
405
  }
539
406
  getProject(projectId) {
540
407
  return this.listProjects().find((p) => p.id === projectId);
@@ -547,18 +414,18 @@ class PersistenceService {
547
414
  } else {
548
415
  projects.push(project);
549
416
  }
550
- writeJson(join4(this.basePath, "projects.json"), projects);
417
+ writeJson(join3(this.basePath, "projects.json"), projects);
551
418
  }
552
419
  deleteProject(projectId) {
553
420
  const projects = this.listProjects().filter((p) => p.id !== projectId);
554
- writeJson(join4(this.basePath, "projects.json"), projects);
555
- const projectDir = join4(this.basePath, "projects", projectId);
556
- if (existsSync4(projectDir)) {
421
+ writeJson(join3(this.basePath, "projects.json"), projects);
422
+ const projectDir = join3(this.basePath, "projects", projectId);
423
+ if (existsSync3(projectDir)) {
557
424
  rmSync2(projectDir, { recursive: true, force: true });
558
425
  }
559
426
  }
560
427
  listStashes(projectId) {
561
- const filePath = join4(this.basePath, "projects", projectId, "stashes.json");
428
+ const filePath = join3(this.basePath, "projects", projectId, "stashes.json");
562
429
  return readJson(filePath, []);
563
430
  }
564
431
  getStash(projectId, stashId) {
@@ -577,67 +444,67 @@ class PersistenceService {
577
444
  } else {
578
445
  stashes.push(stash);
579
446
  }
580
- const filePath = join4(this.basePath, "projects", stash.projectId, "stashes.json");
447
+ const filePath = join3(this.basePath, "projects", stash.projectId, "stashes.json");
581
448
  writeJson(filePath, stashes);
582
449
  }
583
450
  deleteStash(projectId, stashId) {
584
451
  const stashes = this.listStashes(projectId).filter((s) => s.id !== stashId);
585
- const filePath = join4(this.basePath, "projects", projectId, "stashes.json");
452
+ const filePath = join3(this.basePath, "projects", projectId, "stashes.json");
586
453
  writeJson(filePath, stashes);
587
454
  }
588
455
  getProjectSettings(projectId) {
589
- const filePath = join4(this.basePath, "projects", projectId, "settings.json");
456
+ const filePath = join3(this.basePath, "projects", projectId, "settings.json");
590
457
  return readJson(filePath, {});
591
458
  }
592
459
  saveProjectSettings(projectId, settings) {
593
- const filePath = join4(this.basePath, "projects", projectId, "settings.json");
460
+ const filePath = join3(this.basePath, "projects", projectId, "settings.json");
594
461
  writeJson(filePath, settings);
595
462
  }
596
463
  listChats(projectId) {
597
- const dir = join4(this.basePath, "projects", projectId, "chats");
598
- if (!existsSync4(dir))
464
+ const dir = join3(this.basePath, "projects", projectId, "chats");
465
+ if (!existsSync3(dir))
599
466
  return [];
600
467
  const files = readdirSync(dir).filter((f) => f.endsWith(".json"));
601
468
  return files.map((f) => {
602
- const data = readJson(join4(dir, f), null);
469
+ const data = readJson(join3(dir, f), null);
603
470
  return data?.chat;
604
471
  }).filter(Boolean).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
605
472
  }
606
473
  getChat(projectId, chatId) {
607
- const filePath = join4(this.basePath, "projects", projectId, "chats", `${chatId}.json`);
474
+ const filePath = join3(this.basePath, "projects", projectId, "chats", `${chatId}.json`);
608
475
  const data = readJson(filePath, null);
609
476
  return data?.chat;
610
477
  }
611
478
  saveChat(chat) {
612
- const filePath = join4(this.basePath, "projects", chat.projectId, "chats", `${chat.id}.json`);
479
+ const filePath = join3(this.basePath, "projects", chat.projectId, "chats", `${chat.id}.json`);
613
480
  const existing = readJson(filePath, { chat, messages: [] });
614
481
  writeJson(filePath, { ...existing, chat });
615
482
  }
616
483
  deleteChat(projectId, chatId) {
617
- const filePath = join4(this.basePath, "projects", projectId, "chats", `${chatId}.json`);
618
- if (existsSync4(filePath)) {
484
+ const filePath = join3(this.basePath, "projects", projectId, "chats", `${chatId}.json`);
485
+ if (existsSync3(filePath)) {
619
486
  rmSync2(filePath);
620
487
  }
621
488
  }
622
489
  getChatMessages(projectId, chatId) {
623
- const filePath = join4(this.basePath, "projects", projectId, "chats", `${chatId}.json`);
490
+ const filePath = join3(this.basePath, "projects", projectId, "chats", `${chatId}.json`);
624
491
  const data = readJson(filePath, { messages: [] });
625
492
  return data.messages;
626
493
  }
627
494
  saveChatMessage(projectId, chatId, message) {
628
- const filePath = join4(this.basePath, "projects", projectId, "chats", `${chatId}.json`);
495
+ const filePath = join3(this.basePath, "projects", projectId, "chats", `${chatId}.json`);
629
496
  const data = readJson(filePath, { chat: this.getChat(projectId, chatId), messages: [] });
630
497
  writeJson(filePath, { ...data, messages: [...data.messages, message] });
631
498
  }
632
499
  updateChatMessage(projectId, chatId, messageId, patch) {
633
- const filePath = join4(this.basePath, "projects", projectId, "chats", `${chatId}.json`);
500
+ const filePath = join3(this.basePath, "projects", projectId, "chats", `${chatId}.json`);
634
501
  const data = readJson(filePath, { chat: this.getChat(projectId, chatId), messages: [] });
635
502
  const messages = data.messages.map((m) => m.id === messageId ? { ...m, ...patch } : m);
636
503
  writeJson(filePath, { ...data, messages });
637
504
  }
638
505
  migrateOldChat(projectId) {
639
- const oldPath = join4(this.basePath, "projects", projectId, "chat.json");
640
- if (!existsSync4(oldPath))
506
+ const oldPath = join3(this.basePath, "projects", projectId, "chat.json");
507
+ if (!existsSync3(oldPath))
641
508
  return null;
642
509
  const messages = readJson(oldPath, []);
643
510
  if (messages.length === 0) {
@@ -652,16 +519,16 @@ class PersistenceService {
652
519
  createdAt: messages[0].createdAt,
653
520
  updatedAt: messages[messages.length - 1].createdAt
654
521
  };
655
- const filePath = join4(this.basePath, "projects", projectId, "chats", `${chatId}.json`);
522
+ const filePath = join3(this.basePath, "projects", projectId, "chats", `${chatId}.json`);
656
523
  writeJson(filePath, { chat, messages });
657
524
  rmSync2(oldPath);
658
525
  logger.info("persistence", `migrated old chat.json \u2192 ${chatId}`);
659
526
  return chatId;
660
527
  }
661
528
  ensureGitignore(projectPath) {
662
- const gitignorePath = join4(projectPath, ".gitignore");
663
- if (existsSync4(gitignorePath)) {
664
- const content = readFileSync2(gitignorePath, "utf-8");
529
+ const gitignorePath = join3(projectPath, ".gitignore");
530
+ if (existsSync3(gitignorePath)) {
531
+ const content = readFileSync(gitignorePath, "utf-8");
665
532
  if (!content.includes(".stashes/")) {
666
533
  writeFileSync(gitignorePath, content.trimEnd() + `
667
534
  .stashes/
@@ -677,37 +544,84 @@ class PersistenceService {
677
544
 
678
545
  // ../core/dist/ai-process.js
679
546
  var {spawn } = globalThis.Bun;
547
+ import { writeFileSync as writeFileSync2, existsSync as existsSync4, mkdirSync as mkdirSync3 } from "fs";
548
+ import { join as join4 } from "path";
549
+ import { tmpdir } from "os";
680
550
  var CLAUDE_BIN = "/opt/homebrew/bin/claude";
551
+ function getPlaywrightMcpConfigPath() {
552
+ const configDir = join4(tmpdir(), "stashes-mcp");
553
+ const configPath = join4(configDir, "playwright.json");
554
+ if (!existsSync4(configPath)) {
555
+ mkdirSync3(configDir, { recursive: true });
556
+ writeFileSync2(configPath, JSON.stringify({
557
+ mcpServers: {
558
+ playwright: { command: "npx", args: ["@playwright/mcp@latest"] }
559
+ }
560
+ }), "utf-8");
561
+ }
562
+ return configPath;
563
+ }
564
+ var OVERHEAD_TOOLS = [
565
+ "Agent",
566
+ "TodoWrite",
567
+ "TaskCreate",
568
+ "TaskUpdate",
569
+ "TaskList",
570
+ "TaskGet",
571
+ "Skill",
572
+ "ToolSearch",
573
+ "EnterPlanMode",
574
+ "ExitPlanMode",
575
+ "WebSearch",
576
+ "WebFetch",
577
+ "NotebookEdit",
578
+ "mcp__UseAI__*",
579
+ "mcp__stashes__*",
580
+ "mcp__plugin_drills*",
581
+ "mcp__plugin_coverit*"
582
+ ];
681
583
  var processes = new Map;
682
- function startAiProcess(id, prompt, cwd, resumeSessionId, model) {
683
- killAiProcess(id);
684
- logger.info("claude", `spawning process: ${id}`, {
685
- cwd,
686
- promptLength: prompt.length,
687
- promptPreview: prompt.substring(0, 100),
688
- resumeSessionId,
689
- model
584
+ function startAiProcess(idOrOpts, prompt, cwd, resumeSessionId, model) {
585
+ const opts = typeof idOrOpts === "string" ? { id: idOrOpts, prompt, cwd, resumeSessionId, model } : idOrOpts;
586
+ const restricted = opts.tools !== undefined;
587
+ killAiProcess(opts.id);
588
+ logger.info("claude", `spawning process: ${opts.id}`, {
589
+ cwd: opts.cwd,
590
+ promptLength: opts.prompt.length,
591
+ promptPreview: opts.prompt.substring(0, 100),
592
+ resumeSessionId: opts.resumeSessionId,
593
+ model: opts.model,
594
+ restricted,
595
+ tools: restricted ? opts.tools.join(",") || "none" : "all"
690
596
  });
691
- const cmd = [CLAUDE_BIN, "-p", prompt, "--output-format=stream-json", "--verbose", "--dangerously-skip-permissions"];
692
- if (resumeSessionId) {
693
- cmd.push("--resume", resumeSessionId);
597
+ const cmd = [CLAUDE_BIN, "-p", opts.prompt, "--output-format=stream-json", "--verbose", "--dangerously-skip-permissions"];
598
+ if (restricted) {
599
+ cmd.push("--tools", opts.tools.length > 0 ? opts.tools.join(",") : '""');
600
+ cmd.push("--disallowedTools", OVERHEAD_TOOLS.join(","));
601
+ cmd.push("--strict-mcp-config");
602
+ if (opts.mcpConfigPath) {
603
+ cmd.push("--mcp-config", opts.mcpConfigPath);
604
+ }
605
+ }
606
+ if (opts.resumeSessionId) {
607
+ cmd.push("--resume", opts.resumeSessionId);
694
608
  }
695
- if (model) {
696
- cmd.push("--model", model);
609
+ if (opts.model) {
610
+ cmd.push("--model", opts.model);
697
611
  }
698
612
  const proc = spawn({
699
613
  cmd,
700
614
  stdin: "ignore",
701
615
  stdout: "pipe",
702
616
  stderr: "pipe",
703
- cwd,
617
+ cwd: opts.cwd,
704
618
  env: { ...process.env }
705
619
  });
706
620
  proc.exited.then((code) => {
707
- logger.info("claude", `process exited: ${id}`, { exitCode: code });
621
+ logger.info("claude", `process exited: ${opts.id}`, { exitCode: code });
708
622
  });
709
- const aiProcess = { process: proc, id };
710
- processes.set(id, aiProcess);
623
+ const aiProcess = { process: proc, id: opts.id };
624
+ processes.set(opts.id, aiProcess);
711
625
  return aiProcess;
712
626
  }
713
627
  function killAiProcess(id) {
@@ -722,8 +636,8 @@ function killAiProcess(id) {
722
636
  }
723
637
  return false;
724
638
  }
725
- var toolNameMap = new Map;
726
639
  async function* parseClaudeStream(proc) {
640
+ const toolNameMap = new Map;
727
641
  const stdout = proc.stdout;
728
642
  if (!stdout || typeof stdout === "number") {
729
643
  throw new Error("Process stdout is not a readable stream");
@@ -820,18 +734,18 @@ async function* parseClaudeStream(proc) {
820
734
 
821
735
  // ../core/dist/smart-screenshot.js
822
736
  import { join as join6 } from "path";
823
- import { mkdirSync as mkdirSync4, existsSync as existsSync6 } from "fs";
737
+ import { mkdirSync as mkdirSync5, existsSync as existsSync6 } from "fs";
824
738
  import simpleGit2 from "simple-git";
825
739
 
826
740
  // ../core/dist/screenshot.js
827
741
  var {spawn: spawn2 } = globalThis.Bun;
828
742
  import { join as join5 } from "path";
829
- import { mkdirSync as mkdirSync3, existsSync as existsSync5 } from "fs";
743
+ import { mkdirSync as mkdirSync4, existsSync as existsSync5 } from "fs";
830
744
  var SCREENSHOTS_DIR = ".stashes/screenshots";
831
745
  async function captureScreenshot(port, projectPath, stashId) {
832
746
  const screenshotsDir = join5(projectPath, SCREENSHOTS_DIR);
833
747
  if (!existsSync5(screenshotsDir)) {
834
- mkdirSync3(screenshotsDir, { recursive: true });
748
+ mkdirSync4(screenshotsDir, { recursive: true });
835
749
  }
836
750
  const filename = `${stashId}.png`;
837
751
  const outputPath = join5(screenshotsDir, filename);
@@ -891,14 +805,8 @@ ${truncatedDiff}`;
891
805
  }
892
806
  function buildScreenshotPrompt(port, diff, screenshotDir, stashId) {
893
807
  return [
894
- "You are a screenshot assistant. Your ONLY job is to take screenshots of a running web app using Playwright MCP tools.",
895
- "",
896
- "IMPORTANT RULES:",
897
- "- ONLY use these Playwright MCP tools: mcp__playwright__browser_navigate, mcp__playwright__browser_take_screenshot, mcp__playwright__browser_click, mcp__playwright__browser_snapshot",
898
- "- Do NOT use any other tools (no Bash, Read, Grep, Agent, ToolSearch, etc.)",
899
- "- Do NOT start any sessions or call useai tools",
900
- "- Do NOT read or analyze code files \u2014 the diff below tells you everything",
901
- "- Be fast \u2014 you have a strict time limit",
808
+ "You are a screenshot assistant. Take screenshots of a running web app using Playwright MCP tools.",
809
+ "Be fast \u2014 you have a strict time limit.",
902
810
  "",
903
811
  `## The app is running at: http://localhost:${port}`,
904
812
  "",
@@ -965,7 +873,7 @@ async function captureSmartScreenshots(opts) {
965
873
  const { projectPath, stashId, stashBranch, parentBranch, worktreePath, port, model = "sonnet", timeout = DEFAULT_TIMEOUT } = opts;
966
874
  const screenshotDir = join6(projectPath, SCREENSHOTS_DIR2);
967
875
  if (!existsSync6(screenshotDir)) {
968
- mkdirSync4(screenshotDir, { recursive: true });
876
+ mkdirSync5(screenshotDir, { recursive: true });
969
877
  }
970
878
  const diff = await getStashDiff(worktreePath, parentBranch);
971
879
  if (!diff) {
@@ -975,7 +883,14 @@ async function captureSmartScreenshots(opts) {
975
883
  const processId = `screenshot-ai-${stashId}`;
976
884
  const prompt = buildScreenshotPrompt(port, diff, screenshotDir, stashId);
977
885
  const modelFlag = model === "sonnet" ? "sonnet" : "haiku";
978
- const aiProcess = startAiProcess(processId, prompt, worktreePath, undefined, modelFlag);
886
+ const aiProcess = startAiProcess({
887
+ id: processId,
888
+ prompt,
889
+ cwd: worktreePath,
890
+ model: modelFlag,
891
+ tools: [],
892
+ mcpConfigPath: getPlaywrightMcpConfigPath()
893
+ });
979
894
  let textOutput = "";
980
895
  let timedOut = false;
981
896
  const timeoutId = setTimeout(() => {
@@ -1103,7 +1018,7 @@ async function generate(opts) {
1103
1018
  if (component?.filePath) {
1104
1019
  const sourceFile = join7(projectPath, component.filePath);
1105
1020
  if (existsSync7(sourceFile)) {
1106
- sourceCode = readFileSync3(sourceFile, "utf-8");
1021
+ sourceCode = readFileSync2(sourceFile, "utf-8");
1107
1022
  }
1108
1023
  }
1109
1024
  const completedStashes = [];
@@ -1138,7 +1053,11 @@ async function generate(opts) {
1138
1053
  } else {
1139
1054
  stashPrompt = buildFreeformStashPrompt(prompt, directive);
1140
1055
  }
1141
- const aiProcess = startAiProcess(stashId, stashPrompt, worktree.path);
1056
+ const aiProcess = startAiProcess({
1057
+ id: stashId,
1058
+ prompt: stashPrompt,
1059
+ cwd: worktree.path
1060
+ });
1142
1061
  try {
1143
1062
  for await (const chunk of parseClaudeStream(aiProcess.process)) {
1144
1063
  emit(onProgress, {
@@ -1147,14 +1066,63 @@ async function generate(opts) {
1147
1066
  content: chunk.content,
1148
1067
  streamType: chunk.type
1149
1068
  });
1069
+ if (chunk.type === "tool_use" && chunk.toolName) {
1070
+ const knownTools = ["Read", "Write", "Edit", "Glob", "Grep", "Bash"];
1071
+ if (knownTools.includes(chunk.toolName)) {
1072
+ const filePath = chunk.toolInput?.file_path ?? chunk.toolInput?.path ?? chunk.toolInput?.command ?? undefined;
1073
+ const lines = chunk.toolInput?.content ? chunk.toolInput.content.split(`
1074
+ `).length : undefined;
1075
+ emit(onProgress, {
1076
+ type: "activity",
1077
+ stashId,
1078
+ action: chunk.toolName,
1079
+ file: filePath,
1080
+ lines,
1081
+ timestamp: Date.now()
1082
+ });
1083
+ }
1084
+ } else if (chunk.type === "thinking") {
1085
+ emit(onProgress, {
1086
+ type: "activity",
1087
+ stashId,
1088
+ action: "thinking",
1089
+ content: chunk.content.substring(0, 200),
1090
+ timestamp: Date.now()
1091
+ });
1092
+ } else if (chunk.type === "text") {
1093
+ emit(onProgress, {
1094
+ type: "activity",
1095
+ stashId,
1096
+ action: "text",
1097
+ content: chunk.content.substring(0, 200),
1098
+ timestamp: Date.now()
1099
+ });
1100
+ }
1150
1101
  }
1151
1102
  await aiProcess.process.exited;
1152
1103
  const wtGit = simpleGit3(worktree.path);
1104
+ let hasChanges = false;
1153
1105
  try {
1154
1106
  await wtGit.add("-A");
1155
- await wtGit.commit(`stashes: stash ${stashId}`);
1156
- } catch {}
1107
+ const status = await wtGit.status();
1108
+ if (status.staged.length > 0) {
1109
+ await wtGit.commit(`stashes: stash ${stashId}`);
1110
+ hasChanges = true;
1111
+ logger.info("generation", `committed changes for ${stashId}`, { files: status.staged.length });
1112
+ } else {
1113
+ logger.warn("generation", `AI produced no file changes for ${stashId} \u2014 skipping`);
1114
+ }
1115
+ } catch (commitErr) {
1116
+ logger.warn("generation", `commit failed for ${stashId}`, {
1117
+ error: commitErr instanceof Error ? commitErr.message : String(commitErr)
1118
+ });
1119
+ }
1157
1120
  await worktreeManager.removeGeneration(stashId);
1121
+ if (!hasChanges) {
1122
+ persistence.saveStash({ ...stash, status: "error", error: "AI generation produced no file changes" });
1123
+ emit(onProgress, { type: "error", stashId, error: "AI generation produced no file changes" });
1124
+ return;
1125
+ }
1158
1126
  const generatedStash = { ...stash, status: "screenshotting" };
1159
1127
  completedStashes.push(generatedStash);
1160
1128
  persistence.saveStash(generatedStash);
@@ -1285,7 +1253,11 @@ async function vary(opts) {
1285
1253
  persistence.saveStash(stash);
1286
1254
  emit2(onProgress, { type: "generating", stashId, number: stashNumber });
1287
1255
  const varyPrompt = `The user wants to vary the current UI. Apply this change: ${prompt}`;
1288
- const aiProcess = startAiProcess(stashId, varyPrompt, worktree.path);
1256
+ const aiProcess = startAiProcess({
1257
+ id: stashId,
1258
+ prompt: varyPrompt,
1259
+ cwd: worktree.path
1260
+ });
1289
1261
  try {
1290
1262
  for await (const chunk of parseClaudeStream(aiProcess.process)) {
1291
1263
  emit2(onProgress, {
@@ -1297,11 +1269,29 @@ async function vary(opts) {
1297
1269
  }
1298
1270
  await aiProcess.process.exited;
1299
1271
  const wtGit = simpleGit4(worktree.path);
1272
+ let hasChanges = false;
1300
1273
  try {
1301
1274
  await wtGit.add("-A");
1302
- await wtGit.commit(`stashes: vary ${stashId} from ${sourceStashId}`);
1303
- } catch {}
1275
+ const status = await wtGit.status();
1276
+ if (status.staged.length > 0) {
1277
+ await wtGit.commit(`stashes: vary ${stashId} from ${sourceStashId}`);
1278
+ hasChanges = true;
1279
+ logger.info("vary", `committed changes for ${stashId}`, { files: status.staged.length });
1280
+ } else {
1281
+ logger.warn("vary", `AI produced no file changes for ${stashId}`);
1282
+ }
1283
+ } catch (commitErr) {
1284
+ logger.warn("vary", `commit failed for ${stashId}`, {
1285
+ error: commitErr instanceof Error ? commitErr.message : String(commitErr)
1286
+ });
1287
+ }
1304
1288
  await worktreeManager.removeGeneration(stashId);
1289
+ if (!hasChanges) {
1290
+ const errorStash = { ...stash, status: "error", error: "AI generation produced no file changes" };
1291
+ persistence.saveStash(errorStash);
1292
+ emit2(onProgress, { type: "error", stashId, error: "AI generation produced no file changes" });
1293
+ return errorStash;
1294
+ }
1305
1295
  persistence.saveStash({ ...stash, status: "screenshotting" });
1306
1296
  emit2(onProgress, { type: "screenshotting", stashId });
1307
1297
  let screenshotPath = "";
@@ -1399,7 +1389,7 @@ async function cleanup(projectPath) {
1399
1389
  logger.info("manage", "cleanup complete");
1400
1390
  }
1401
1391
  // ../server/dist/services/stash-service.js
1402
- import { readFileSync as readFileSync4, existsSync as existsSync8 } from "fs";
1392
+ import { readFileSync as readFileSync3, existsSync as existsSync8 } from "fs";
1403
1393
  import { join as join8 } from "path";
1404
1394
 
1405
1395
  // ../server/dist/services/app-proxy.js
@@ -1883,6 +1873,7 @@ class StashService {
1883
1873
  worktreeManager;
1884
1874
  persistence;
1885
1875
  broadcast;
1876
+ activityStore;
1886
1877
  previewPool;
1887
1878
  selectedComponent = null;
1888
1879
  messageQueue = [];
@@ -1892,11 +1883,12 @@ class StashService {
1892
1883
  stashPollTimer = null;
1893
1884
  knownStashIds = new Set;
1894
1885
  pendingComponentResolve = null;
1895
- constructor(projectPath, worktreeManager, persistence, broadcast, stashPortStart, stashPortEnd) {
1886
+ constructor(projectPath, worktreeManager, persistence, broadcast, activityStore, stashPortStart, stashPortEnd) {
1896
1887
  this.projectPath = projectPath;
1897
1888
  this.worktreeManager = worktreeManager;
1898
1889
  this.persistence = persistence;
1899
1890
  this.broadcast = broadcast;
1891
+ this.activityStore = activityStore;
1900
1892
  this.previewPool = new PreviewPool(worktreeManager, broadcast, undefined, undefined, stashPortStart, stashPortEnd);
1901
1893
  }
1902
1894
  getActiveChatId() {
@@ -1930,7 +1922,13 @@ class StashService {
1930
1922
  "Reply with ONLY the file path relative to the project root."
1931
1923
  ].join(`
1932
1924
  `);
1933
- const aiProcess = startAiProcess("resolve-component", prompt, this.projectPath, undefined, "claude-haiku-4-5-20251001");
1925
+ const aiProcess = startAiProcess({
1926
+ id: "resolve-component",
1927
+ prompt,
1928
+ cwd: this.projectPath,
1929
+ model: "claude-haiku-4-5-20251001",
1930
+ tools: ["Read", "Grep", "Glob", "Bash"]
1931
+ });
1934
1932
  let resolvedPath = "";
1935
1933
  try {
1936
1934
  for await (const chunk of parseClaudeStream(aiProcess.process)) {
@@ -1993,7 +1991,7 @@ class StashService {
1993
1991
  if (filePath && filePath !== "auto-detect") {
1994
1992
  const sourceFile = join8(this.projectPath, filePath);
1995
1993
  if (existsSync8(sourceFile)) {
1996
- sourceCode = readFileSync4(sourceFile, "utf-8");
1994
+ sourceCode = readFileSync3(sourceFile, "utf-8");
1997
1995
  }
1998
1996
  }
1999
1997
  let stashContext = "";
@@ -2040,7 +2038,12 @@ ${sourceCode.substring(0, 3000)}
2040
2038
  ].filter(Boolean).join(`
2041
2039
  `);
2042
2040
  }
2043
- const aiProcess = startAiProcess("chat", chatPrompt, this.projectPath, existingSessionId);
2041
+ const aiProcess = startAiProcess({
2042
+ id: "chat",
2043
+ prompt: chatPrompt,
2044
+ cwd: this.projectPath,
2045
+ resumeSessionId: existingSessionId
2046
+ });
2044
2047
  let thinkingBuf = "";
2045
2048
  let textBuf = "";
2046
2049
  const now = new Date().toISOString();
@@ -2116,6 +2119,21 @@ ${sourceCode.substring(0, 3000)}
2116
2119
  }
2117
2120
  } else if (chunk.type === "tool_result") {
2118
2121
  this.stopStashPoll();
2122
+ let stashActivity;
2123
+ const toolNameForSnapshot = chunk.toolName ?? "";
2124
+ if (toolNameForSnapshot.includes("stashes_generate") || toolNameForSnapshot.includes("stashes_vary")) {
2125
+ const projectId2 = this.persistence.listProjects()[0]?.id ?? "";
2126
+ const allStashes = this.persistence.listStashes(projectId2);
2127
+ stashActivity = {};
2128
+ for (const s of allStashes) {
2129
+ if (this.activityStore.has(s.id)) {
2130
+ stashActivity[s.id] = this.activityStore.getSnapshot(s.id);
2131
+ this.activityStore.clear(s.id);
2132
+ }
2133
+ }
2134
+ if (Object.keys(stashActivity).length === 0)
2135
+ stashActivity = undefined;
2136
+ }
2119
2137
  let toolResult = chunk.content;
2120
2138
  let isError = false;
2121
2139
  try {
@@ -2123,13 +2141,16 @@ ${sourceCode.substring(0, 3000)}
2123
2141
  toolResult = parsed.result ?? chunk.content;
2124
2142
  isError = !!parsed.is_error;
2125
2143
  } catch {}
2144
+ const endToolName = chunk.toolName ?? "unknown";
2126
2145
  save({
2127
2146
  id: crypto.randomUUID(),
2128
2147
  role: "assistant",
2129
2148
  content: chunk.content,
2130
2149
  type: "tool_end",
2150
+ toolName: endToolName,
2131
2151
  toolStatus: isError ? "error" : "completed",
2132
2152
  toolResult: toolResult.substring(0, 300),
2153
+ stashActivity,
2133
2154
  createdAt: now
2134
2155
  });
2135
2156
  this.broadcast({
@@ -2137,6 +2158,7 @@ ${sourceCode.substring(0, 3000)}
2137
2158
  content: chunk.content,
2138
2159
  streamType: "tool_end",
2139
2160
  source: "chat",
2161
+ toolName: endToolName,
2140
2162
  toolStatus: isError ? "error" : "completed",
2141
2163
  toolResult: toolResult.substring(0, 300)
2142
2164
  });
@@ -2319,10 +2341,77 @@ ${refDescriptions.join(`
2319
2341
  }
2320
2342
  }
2321
2343
 
2344
+ // ../server/dist/services/activity-store.js
2345
+ import { appendFileSync as appendFileSync2, readFileSync as readFileSync4, existsSync as existsSync9, mkdirSync as mkdirSync6, rmSync as rmSync3 } from "fs";
2346
+ import { join as join9, dirname as dirname2 } from "path";
2347
+
2348
+ class ActivityStore {
2349
+ cache = new Map;
2350
+ projectPath;
2351
+ constructor(projectPath) {
2352
+ this.projectPath = projectPath;
2353
+ }
2354
+ jsonlPath(stashId) {
2355
+ return join9(this.projectPath, ".stashes", "activity", `${stashId}.jsonl`);
2356
+ }
2357
+ append(event) {
2358
+ const existing = this.cache.get(event.stashId) ?? [];
2359
+ existing.push(event);
2360
+ this.cache.set(event.stashId, existing);
2361
+ const filePath = this.jsonlPath(event.stashId);
2362
+ const dir = dirname2(filePath);
2363
+ if (!existsSync9(dir))
2364
+ mkdirSync6(dir, { recursive: true });
2365
+ appendFileSync2(filePath, JSON.stringify(event) + `
2366
+ `, "utf-8");
2367
+ }
2368
+ getEvents(stashId) {
2369
+ const cached = this.cache.get(stashId);
2370
+ if (cached && cached.length > 0)
2371
+ return cached;
2372
+ const filePath = this.jsonlPath(stashId);
2373
+ if (!existsSync9(filePath))
2374
+ return [];
2375
+ const lines = readFileSync4(filePath, "utf-8").trim().split(`
2376
+ `).filter(Boolean);
2377
+ const events = lines.map((line) => JSON.parse(line));
2378
+ this.cache.set(stashId, events);
2379
+ return events;
2380
+ }
2381
+ getSnapshot(stashId) {
2382
+ const actions = this.getEvents(stashId);
2383
+ const fileActions = actions.filter((a) => ["Read", "Write", "Edit", "Glob", "Grep", "Bash"].includes(a.action));
2384
+ const uniqueFiles = new Set(fileActions.filter((a) => a.file).map((a) => a.file));
2385
+ const timestamps = actions.map((a) => a.timestamp);
2386
+ const duration = timestamps.length > 1 ? Math.round((Math.max(...timestamps) - Math.min(...timestamps)) / 1000) : 0;
2387
+ return {
2388
+ actions: [...actions],
2389
+ stats: {
2390
+ filesChanged: uniqueFiles.size,
2391
+ duration,
2392
+ totalActions: actions.length
2393
+ }
2394
+ };
2395
+ }
2396
+ clear(stashId) {
2397
+ this.cache.delete(stashId);
2398
+ const filePath = this.jsonlPath(stashId);
2399
+ if (existsSync9(filePath)) {
2400
+ rmSync3(filePath);
2401
+ }
2402
+ }
2403
+ has(stashId) {
2404
+ if (this.cache.has(stashId) && (this.cache.get(stashId)?.length ?? 0) > 0)
2405
+ return true;
2406
+ return existsSync9(this.jsonlPath(stashId));
2407
+ }
2408
+ }
2409
+
2322
2410
  // ../server/dist/services/websocket.js
2323
2411
  var worktreeManager;
2324
2412
  var stashService;
2325
2413
  var persistence;
2414
+ var activityStore;
2326
2415
  var clients = new Set;
2327
2416
  function broadcast(event) {
2328
2417
  const data = JSON.stringify(event);
@@ -2333,10 +2422,14 @@ function broadcast(event) {
2333
2422
  function getPersistenceFromWs() {
2334
2423
  return persistence;
2335
2424
  }
2425
+ function getActivityStoreFromWs() {
2426
+ return activityStore;
2427
+ }
2336
2428
  function createWebSocketHandler(projectPath, userDevPort, appProxyPort, stashPortStart, stashPortEnd) {
2337
2429
  worktreeManager = new WorktreeManager(projectPath);
2338
2430
  persistence = new PersistenceService(projectPath);
2339
- stashService = new StashService(projectPath, worktreeManager, persistence, broadcast, stashPortStart, stashPortEnd);
2431
+ activityStore = new ActivityStore(projectPath);
2432
+ stashService = new StashService(projectPath, worktreeManager, persistence, broadcast, activityStore, stashPortStart, stashPortEnd);
2340
2433
  return {
2341
2434
  open(ws) {
2342
2435
  clients.add(ws);
@@ -2353,6 +2446,15 @@ function createWebSocketHandler(projectPath, userDevPort, appProxyPort, stashPor
2353
2446
  if (activeChatId) {
2354
2447
  ws.send(JSON.stringify({ type: "processing", chatId: activeChatId }));
2355
2448
  }
2449
+ const allStashes = persistence.listStashes(project.id);
2450
+ for (const stash of allStashes) {
2451
+ if (stash.status === "generating" && activityStore.has(stash.id)) {
2452
+ const events = activityStore.getEvents(stash.id);
2453
+ for (const event of events) {
2454
+ ws.send(JSON.stringify({ type: "stash:activity", stashId: stash.id, event }));
2455
+ }
2456
+ }
2457
+ }
2356
2458
  },
2357
2459
  async message(ws, message) {
2358
2460
  const raw = typeof message === "string" ? message : new TextDecoder().decode(message);
@@ -2432,6 +2534,156 @@ function createWebSocketHandler(projectPath, userDevPort, appProxyPort, stashPor
2432
2534
  };
2433
2535
  }
2434
2536
 
2537
+ // ../server/dist/routes/api.js
2538
+ var app = new Hono;
2539
+ app.get("/health", (c) => c.json({ status: "ok", service: "stashes" }));
2540
+ app.get("/projects", (c) => {
2541
+ const persistence2 = getPersistence();
2542
+ const projects = persistence2.listProjects();
2543
+ const projectsWithCounts = projects.map((p) => ({
2544
+ ...p,
2545
+ stashCount: persistence2.listStashes(p.id).length,
2546
+ recentScreenshots: persistence2.listStashes(p.id).filter((s) => s.screenshotUrl).slice(-4).map((s) => s.screenshotUrl)
2547
+ }));
2548
+ return c.json({ data: projectsWithCounts });
2549
+ });
2550
+ app.post("/projects", async (c) => {
2551
+ const { name, description } = await c.req.json();
2552
+ const project = {
2553
+ id: `proj_${crypto.randomUUID().substring(0, 8)}`,
2554
+ name,
2555
+ description,
2556
+ createdAt: new Date().toISOString(),
2557
+ updatedAt: new Date().toISOString()
2558
+ };
2559
+ getPersistence().saveProject(project);
2560
+ return c.json({ data: project }, 201);
2561
+ });
2562
+ app.get("/projects/:id", (c) => {
2563
+ const persistence2 = getPersistence();
2564
+ const project = persistence2.getProject(c.req.param("id"));
2565
+ if (!project)
2566
+ return c.json({ error: "Project not found" }, 404);
2567
+ const stashes = persistence2.listStashes(project.id);
2568
+ const chats = persistence2.listChats(project.id);
2569
+ return c.json({ data: { ...project, stashes, chats } });
2570
+ });
2571
+ app.delete("/projects/:id", (c) => {
2572
+ const id = c.req.param("id");
2573
+ getPersistence().deleteProject(id);
2574
+ return c.json({ data: { deleted: id } });
2575
+ });
2576
+ app.get("/chats", (c) => {
2577
+ const persistence2 = getPersistence();
2578
+ const project = ensureProject(persistence2);
2579
+ const chats = persistence2.listChats(project.id);
2580
+ const stashes = persistence2.listStashes(project.id);
2581
+ return c.json({ data: { project, chats, stashes } });
2582
+ });
2583
+ app.post("/chats", async (c) => {
2584
+ const persistence2 = getPersistence();
2585
+ const project = ensureProject(persistence2);
2586
+ const { title, referencedStashIds } = await c.req.json();
2587
+ const chatCount = persistence2.listChats(project.id).length;
2588
+ const chat = {
2589
+ id: `chat_${crypto.randomUUID().substring(0, 8)}`,
2590
+ projectId: project.id,
2591
+ title: title?.trim() || `Chat ${chatCount + 1}`,
2592
+ referencedStashIds: referencedStashIds ?? [],
2593
+ createdAt: new Date().toISOString(),
2594
+ updatedAt: new Date().toISOString()
2595
+ };
2596
+ persistence2.saveChat(chat);
2597
+ return c.json({ data: chat }, 201);
2598
+ });
2599
+ app.patch("/chats/:chatId", async (c) => {
2600
+ const persistence2 = getPersistence();
2601
+ const project = ensureProject(persistence2);
2602
+ const chatId = c.req.param("chatId");
2603
+ const chat = persistence2.getChat(project.id, chatId);
2604
+ if (!chat)
2605
+ return c.json({ error: "Chat not found" }, 404);
2606
+ const body = await c.req.json();
2607
+ const updated = {
2608
+ ...chat,
2609
+ ...body.referencedStashIds !== undefined ? { referencedStashIds: body.referencedStashIds } : {},
2610
+ updatedAt: new Date().toISOString()
2611
+ };
2612
+ persistence2.saveChat(updated);
2613
+ return c.json({ data: updated });
2614
+ });
2615
+ app.get("/chats/:chatId", (c) => {
2616
+ const persistence2 = getPersistence();
2617
+ const project = ensureProject(persistence2);
2618
+ const chatId = c.req.param("chatId");
2619
+ const chat = persistence2.getChat(project.id, chatId);
2620
+ if (!chat)
2621
+ return c.json({ error: "Chat not found" }, 404);
2622
+ const messages = persistence2.getChatMessages(project.id, chatId);
2623
+ const refIds = new Set(chat.referencedStashIds ?? []);
2624
+ const stashes = persistence2.listStashes(project.id).filter((s) => s.originChatId === chatId || refIds.has(s.id));
2625
+ return c.json({ data: { ...chat, messages, stashes } });
2626
+ });
2627
+ app.delete("/chats/:chatId", (c) => {
2628
+ const persistence2 = getPersistence();
2629
+ const project = ensureProject(persistence2);
2630
+ const chatId = c.req.param("chatId");
2631
+ persistence2.deleteChat(project.id, chatId);
2632
+ return c.json({ data: { deleted: chatId } });
2633
+ });
2634
+ app.get("/dev-server-status", async (c) => {
2635
+ const port = serverState.userDevPort;
2636
+ try {
2637
+ const res = await fetch(`http://localhost:${port}`, {
2638
+ method: "HEAD",
2639
+ signal: AbortSignal.timeout(2000)
2640
+ });
2641
+ return c.json({ up: res.status < 500, port });
2642
+ } catch {
2643
+ return c.json({ up: false, port });
2644
+ }
2645
+ });
2646
+ app.get("/screenshots/:filename", (c) => {
2647
+ const filename = c.req.param("filename");
2648
+ const filePath = join10(serverState.projectPath, ".stashes", "screenshots", filename);
2649
+ if (!existsSync10(filePath))
2650
+ return c.json({ error: "Not found" }, 404);
2651
+ const content = readFileSync5(filePath);
2652
+ return new Response(content, {
2653
+ headers: { "content-type": "image/png", "cache-control": "no-cache" }
2654
+ });
2655
+ });
2656
+ app.post("/stash-activity", async (c) => {
2657
+ const events = await c.req.json();
2658
+ const store = getActivityStoreFromWs();
2659
+ for (const event of events) {
2660
+ store.append(event);
2661
+ broadcast({ type: "stash:activity", stashId: event.stashId, event });
2662
+ }
2663
+ return c.json({ ok: true });
2664
+ });
2665
+ app.get("/stash-activity/:stashId", (c) => {
2666
+ const stashId = c.req.param("stashId");
2667
+ const store = getActivityStoreFromWs();
2668
+ const events = store.getEvents(stashId);
2669
+ return c.json({ data: events });
2670
+ });
2671
+ function ensureProject(persistence2) {
2672
+ const projects = persistence2.listProjects();
2673
+ if (projects.length > 0)
2674
+ return projects[0];
2675
+ const project = {
2676
+ id: `proj_${crypto.randomUUID().substring(0, 8)}`,
2677
+ name: basename(serverState.projectPath),
2678
+ createdAt: new Date().toISOString(),
2679
+ updatedAt: new Date().toISOString()
2680
+ };
2681
+ persistence2.saveProject(project);
2682
+ persistence2.migrateOldChat(project.id);
2683
+ return project;
2684
+ }
2685
+ var apiRoutes = app;
2686
+
2435
2687
  // ../server/dist/index.js
2436
2688
  var serverState = {
2437
2689
  projectPath: "",
@@ -2445,14 +2697,14 @@ app2.use("/*", cors());
2445
2697
  app2.route("/api", apiRoutes);
2446
2698
  app2.get("/*", async (c) => {
2447
2699
  const path = c.req.path;
2448
- const selfDir = dirname2(fileURLToPath(import.meta.url));
2449
- const bundledWebDir = join9(selfDir, "web");
2450
- const monorepoWebDir = join9(selfDir, "../../web/dist");
2451
- const webDistDir = existsSync9(join9(bundledWebDir, "index.html")) ? bundledWebDir : monorepoWebDir;
2700
+ const selfDir = dirname3(fileURLToPath(import.meta.url));
2701
+ const bundledWebDir = join11(selfDir, "web");
2702
+ const monorepoWebDir = join11(selfDir, "../../web/dist");
2703
+ const webDistDir = existsSync11(join11(bundledWebDir, "index.html")) ? bundledWebDir : monorepoWebDir;
2452
2704
  const requestPath = path === "/" ? "/index.html" : path;
2453
- const filePath = join9(webDistDir, requestPath);
2454
- if (existsSync9(filePath) && !filePath.includes("..")) {
2455
- const content = readFileSync5(filePath);
2705
+ const filePath = join11(webDistDir, requestPath);
2706
+ if (existsSync11(filePath) && !filePath.includes("..")) {
2707
+ const content = readFileSync6(filePath);
2456
2708
  const ext = filePath.split(".").pop() || "";
2457
2709
  const contentTypes = {
2458
2710
  html: "text/html; charset=utf-8",
@@ -2470,9 +2722,9 @@ app2.get("/*", async (c) => {
2470
2722
  headers: { "content-type": contentTypes[ext] || "application/octet-stream" }
2471
2723
  });
2472
2724
  }
2473
- const indexPath = join9(webDistDir, "index.html");
2474
- if (existsSync9(indexPath)) {
2475
- const html = readFileSync5(indexPath, "utf-8");
2725
+ const indexPath = join11(webDistDir, "index.html");
2726
+ if (existsSync11(indexPath)) {
2727
+ const html = readFileSync6(indexPath, "utf-8");
2476
2728
  return new Response(html, {
2477
2729
  headers: { "content-type": "text/html; charset=utf-8" }
2478
2730
  });
@@ -2532,11 +2784,11 @@ async function startServer(projectPath, userDevPort, requestedPort = STASHES_POR
2532
2784
  }
2533
2785
 
2534
2786
  // ../server/dist/services/detector.js
2535
- import { existsSync as existsSync10, readFileSync as readFileSync6 } from "fs";
2536
- import { join as join10 } from "path";
2787
+ import { existsSync as existsSync12, readFileSync as readFileSync7 } from "fs";
2788
+ import { join as join12 } from "path";
2537
2789
  function detectFramework(projectPath) {
2538
- const packageJsonPath = join10(projectPath, "package.json");
2539
- if (!existsSync10(packageJsonPath)) {
2790
+ const packageJsonPath = join12(projectPath, "package.json");
2791
+ if (!existsSync12(packageJsonPath)) {
2540
2792
  return {
2541
2793
  framework: "unknown",
2542
2794
  devCommand: "npm run dev",
@@ -2544,7 +2796,7 @@ function detectFramework(projectPath) {
2544
2796
  configFile: null
2545
2797
  };
2546
2798
  }
2547
- const packageJson = JSON.parse(readFileSync6(packageJsonPath, "utf-8"));
2799
+ const packageJson = JSON.parse(readFileSync7(packageJsonPath, "utf-8"));
2548
2800
  const deps = {
2549
2801
  ...packageJson.dependencies,
2550
2802
  ...packageJson.devDependencies
@@ -2598,7 +2850,7 @@ function getDevCommand(packageJson, fallback) {
2598
2850
  }
2599
2851
  function findConfig(projectPath, candidates) {
2600
2852
  for (const candidate of candidates) {
2601
- if (existsSync10(join10(projectPath, candidate))) {
2853
+ if (existsSync12(join12(projectPath, candidate))) {
2602
2854
  return candidate;
2603
2855
  }
2604
2856
  }
@@ -2790,8 +3042,8 @@ Cleaning up all stashes and worktrees...`);
2790
3042
  }
2791
3043
 
2792
3044
  // src/commands/setup.ts
2793
- import { existsSync as existsSync11, readFileSync as readFileSync7, writeFileSync as writeFileSync2, mkdirSync as mkdirSync5 } from "fs";
2794
- import { dirname as dirname3, join as join11 } from "path";
3045
+ import { existsSync as existsSync13, readFileSync as readFileSync8, writeFileSync as writeFileSync3, mkdirSync as mkdirSync7 } from "fs";
3046
+ import { dirname as dirname4, join as join13 } from "path";
2795
3047
  import { homedir } from "os";
2796
3048
  import * as p from "@clack/prompts";
2797
3049
  import pc from "picocolors";
@@ -2810,63 +3062,63 @@ var MCP_ENTRY_ZED = {
2810
3062
  };
2811
3063
  function buildToolDefinitions() {
2812
3064
  const home = homedir();
2813
- const appSupport = join11(home, "Library", "Application Support");
3065
+ const appSupport = join13(home, "Library", "Application Support");
2814
3066
  return [
2815
3067
  {
2816
3068
  id: "claude-code",
2817
3069
  name: "Claude Code",
2818
- configPath: join11(home, ".claude.json"),
3070
+ configPath: join13(home, ".claude.json"),
2819
3071
  serversKey: "mcpServers",
2820
3072
  format: "standard",
2821
- detect: () => existsSync11(join11(home, ".claude.json")) || existsSync11(join11(home, ".claude"))
3073
+ detect: () => existsSync13(join13(home, ".claude.json")) || existsSync13(join13(home, ".claude"))
2822
3074
  },
2823
3075
  {
2824
3076
  id: "claude-desktop",
2825
3077
  name: "Claude Desktop",
2826
- configPath: join11(appSupport, "Claude", "claude_desktop_config.json"),
3078
+ configPath: join13(appSupport, "Claude", "claude_desktop_config.json"),
2827
3079
  serversKey: "mcpServers",
2828
3080
  format: "standard",
2829
- detect: () => existsSync11(join11(appSupport, "Claude")) || existsSync11("/Applications/Claude.app")
3081
+ detect: () => existsSync13(join13(appSupport, "Claude")) || existsSync13("/Applications/Claude.app")
2830
3082
  },
2831
3083
  {
2832
3084
  id: "vscode",
2833
3085
  name: "VS Code",
2834
- configPath: join11(appSupport, "Code", "User", "mcp.json"),
3086
+ configPath: join13(appSupport, "Code", "User", "mcp.json"),
2835
3087
  serversKey: "servers",
2836
3088
  format: "standard",
2837
- detect: () => existsSync11(join11(appSupport, "Code", "User"))
3089
+ detect: () => existsSync13(join13(appSupport, "Code", "User"))
2838
3090
  },
2839
3091
  {
2840
3092
  id: "cursor",
2841
3093
  name: "Cursor",
2842
- configPath: join11(home, ".cursor", "mcp.json"),
3094
+ configPath: join13(home, ".cursor", "mcp.json"),
2843
3095
  serversKey: "mcpServers",
2844
3096
  format: "standard",
2845
- detect: () => existsSync11(join11(home, ".cursor"))
3097
+ detect: () => existsSync13(join13(home, ".cursor"))
2846
3098
  },
2847
3099
  {
2848
3100
  id: "windsurf",
2849
3101
  name: "Windsurf",
2850
- configPath: join11(home, ".codeium", "windsurf", "mcp_config.json"),
3102
+ configPath: join13(home, ".codeium", "windsurf", "mcp_config.json"),
2851
3103
  serversKey: "mcpServers",
2852
3104
  format: "standard",
2853
- detect: () => existsSync11(join11(home, ".codeium", "windsurf"))
3105
+ detect: () => existsSync13(join13(home, ".codeium", "windsurf"))
2854
3106
  },
2855
3107
  {
2856
3108
  id: "zed",
2857
3109
  name: "Zed",
2858
- configPath: join11(appSupport, "Zed", "settings.json"),
3110
+ configPath: join13(appSupport, "Zed", "settings.json"),
2859
3111
  serversKey: "context_servers",
2860
3112
  format: "zed",
2861
- detect: () => existsSync11(join11(appSupport, "Zed"))
3113
+ detect: () => existsSync13(join13(appSupport, "Zed"))
2862
3114
  }
2863
3115
  ];
2864
3116
  }
2865
3117
  function readJsonFile(path) {
2866
- if (!existsSync11(path))
3118
+ if (!existsSync13(path))
2867
3119
  return {};
2868
3120
  try {
2869
- const raw = readFileSync7(path, "utf-8").trim();
3121
+ const raw = readFileSync8(path, "utf-8").trim();
2870
3122
  if (!raw)
2871
3123
  return {};
2872
3124
  return JSON.parse(raw);
@@ -2875,8 +3127,8 @@ function readJsonFile(path) {
2875
3127
  }
2876
3128
  }
2877
3129
  function writeJsonFile(path, data) {
2878
- mkdirSync5(dirname3(path), { recursive: true });
2879
- writeFileSync2(path, JSON.stringify(data, null, 2) + `
3130
+ mkdirSync7(dirname4(path), { recursive: true });
3131
+ writeFileSync3(path, JSON.stringify(data, null, 2) + `
2880
3132
  `);
2881
3133
  }
2882
3134
  function isConfigured(tool) {
@@ -3014,16 +3266,16 @@ async function setupCommand(options) {
3014
3266
 
3015
3267
  // src/commands/update.ts
3016
3268
  import { execFileSync, execSync } from "child_process";
3017
- import { writeFileSync as writeFileSync3, unlinkSync, chmodSync, readFileSync as readFileSync8 } from "fs";
3018
- import { tmpdir } from "os";
3019
- import { join as join12, dirname as dirname4 } from "path";
3269
+ import { writeFileSync as writeFileSync4, unlinkSync, chmodSync, readFileSync as readFileSync9 } from "fs";
3270
+ import { tmpdir as tmpdir2 } from "os";
3271
+ import { join as join14, dirname as dirname5 } from "path";
3020
3272
  import { fileURLToPath as fileURLToPath2 } from "url";
3021
3273
  import * as p2 from "@clack/prompts";
3022
3274
  import pc2 from "picocolors";
3023
3275
  function getCurrentVersion() {
3024
- const selfDir = dirname4(fileURLToPath2(import.meta.url));
3025
- const pkgPath = join12(selfDir, "..", "package.json");
3026
- return JSON.parse(readFileSync8(pkgPath, "utf-8")).version;
3276
+ const selfDir = dirname5(fileURLToPath2(import.meta.url));
3277
+ const pkgPath = join14(selfDir, "..", "package.json");
3278
+ return JSON.parse(readFileSync9(pkgPath, "utf-8")).version;
3027
3279
  }
3028
3280
  function fetchLatestVersion() {
3029
3281
  try {
@@ -3099,8 +3351,8 @@ async function updateCommand() {
3099
3351
  }
3100
3352
  s.stop(`Removed from ${configuredTools.length} tool${configuredTools.length === 1 ? "" : "s"}`);
3101
3353
  }
3102
- const scriptPath = join12(tmpdir(), `stashes-update-${Date.now()}.sh`);
3103
- writeFileSync3(scriptPath, buildUpdateScript(), "utf-8");
3354
+ const scriptPath = join14(tmpdir2(), `stashes-update-${Date.now()}.sh`);
3355
+ writeFileSync4(scriptPath, buildUpdateScript(), "utf-8");
3104
3356
  chmodSync(scriptPath, 493);
3105
3357
  try {
3106
3358
  execFileSync("bash", [scriptPath], { stdio: "inherit" });
@@ -3117,9 +3369,9 @@ Update failed. Try manually:`);
3117
3369
  }
3118
3370
 
3119
3371
  // src/index.ts
3120
- var selfDir = dirname5(fileURLToPath3(import.meta.url));
3121
- var pkgPath = join13(selfDir, "..", "package.json");
3122
- var version = JSON.parse(readFileSync9(pkgPath, "utf-8")).version;
3372
+ var selfDir = dirname6(fileURLToPath3(import.meta.url));
3373
+ var pkgPath = join15(selfDir, "..", "package.json");
3374
+ var version = JSON.parse(readFileSync10(pkgPath, "utf-8")).version;
3123
3375
  var program = new Command;
3124
3376
  program.name("stashes").description("Generate AI-powered UI design explorations in your project").version(version, "-v, --version");
3125
3377
  program.command("browse", { isDefault: true }).description("Start the Stashes server and open the web UI").argument("[path]", "Project directory", ".").option("-p, --port <port>", "Stashes server port", "4000").option("-d, --dev-port <port>", "Your app dev server port (auto-detected)").option("-n, --stashes <count>", "Default stash count", "3").option("--no-open", "Do not open the browser").action(startCommand);