stashes 0.1.50 → 0.1.52
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 +537 -299
- package/dist/mcp.js +478 -212
- package/dist/web/assets/{index-B2HUtHbu.js → index-DjPE9klT.js} +40 -40
- package/dist/web/assets/index-mdSV-b1c.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-DXopYbWS.css +0 -1
package/dist/mcp.js
CHANGED
|
@@ -26,8 +26,8 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
26
26
|
import { z } from "zod";
|
|
27
27
|
|
|
28
28
|
// ../core/dist/generation.js
|
|
29
|
-
import { readFileSync as readFileSync2, existsSync as
|
|
30
|
-
import { join as
|
|
29
|
+
import { readFileSync as readFileSync2, existsSync as existsSync7 } from "fs";
|
|
30
|
+
import { join as join7 } from "path";
|
|
31
31
|
var {spawn: spawn3 } = globalThis.Bun;
|
|
32
32
|
import simpleGit3 from "simple-git";
|
|
33
33
|
// ../shared/dist/constants/index.js
|
|
@@ -529,37 +529,68 @@ class PersistenceService {
|
|
|
529
529
|
|
|
530
530
|
// ../core/dist/ai-process.js
|
|
531
531
|
var {spawn } = globalThis.Bun;
|
|
532
|
+
import { writeFileSync as writeFileSync2, existsSync as existsSync4, mkdirSync as mkdirSync3 } from "fs";
|
|
533
|
+
import { join as join4 } from "path";
|
|
534
|
+
import { tmpdir } from "os";
|
|
532
535
|
var CLAUDE_BIN = "/opt/homebrew/bin/claude";
|
|
536
|
+
var DEFAULT_TOOLS = ["Read", "Write", "Edit", "Glob", "Grep", "Bash"];
|
|
537
|
+
function getPlaywrightMcpConfigPath() {
|
|
538
|
+
const configDir = join4(tmpdir(), "stashes-mcp");
|
|
539
|
+
const configPath = join4(configDir, "playwright.json");
|
|
540
|
+
if (!existsSync4(configPath)) {
|
|
541
|
+
mkdirSync3(configDir, { recursive: true });
|
|
542
|
+
writeFileSync2(configPath, JSON.stringify({
|
|
543
|
+
mcpServers: {
|
|
544
|
+
playwright: { command: "npx", args: ["@playwright/mcp@latest"] }
|
|
545
|
+
}
|
|
546
|
+
}), "utf-8");
|
|
547
|
+
}
|
|
548
|
+
return configPath;
|
|
549
|
+
}
|
|
533
550
|
var processes = new Map;
|
|
534
|
-
function startAiProcess(
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
551
|
+
function startAiProcess(idOrOpts, prompt, cwd, resumeSessionId, model) {
|
|
552
|
+
const opts = typeof idOrOpts === "string" ? { id: idOrOpts, prompt, cwd, resumeSessionId, model } : idOrOpts;
|
|
553
|
+
const bare = opts.bare ?? true;
|
|
554
|
+
const tools = opts.tools ?? DEFAULT_TOOLS;
|
|
555
|
+
killAiProcess(opts.id);
|
|
556
|
+
logger.info("claude", `spawning process: ${opts.id}`, {
|
|
557
|
+
cwd: opts.cwd,
|
|
558
|
+
promptLength: opts.prompt.length,
|
|
559
|
+
promptPreview: opts.prompt.substring(0, 100),
|
|
560
|
+
resumeSessionId: opts.resumeSessionId,
|
|
561
|
+
model: opts.model,
|
|
562
|
+
bare,
|
|
563
|
+
tools: bare ? tools.join(",") : "all"
|
|
542
564
|
});
|
|
543
|
-
const cmd = [CLAUDE_BIN, "-p", prompt, "--output-format=stream-json", "--verbose", "--dangerously-skip-permissions"];
|
|
544
|
-
if (
|
|
545
|
-
cmd.push("--
|
|
565
|
+
const cmd = [CLAUDE_BIN, "-p", opts.prompt, "--output-format=stream-json", "--verbose", "--dangerously-skip-permissions"];
|
|
566
|
+
if (bare) {
|
|
567
|
+
cmd.push("--bare");
|
|
568
|
+
if (tools.length > 0) {
|
|
569
|
+
cmd.push("--tools", tools.join(","));
|
|
570
|
+
}
|
|
571
|
+
if (opts.mcpConfigPath) {
|
|
572
|
+
cmd.push("--mcp-config", opts.mcpConfigPath);
|
|
573
|
+
}
|
|
546
574
|
}
|
|
547
|
-
if (
|
|
548
|
-
cmd.push("--
|
|
575
|
+
if (opts.resumeSessionId) {
|
|
576
|
+
cmd.push("--resume", opts.resumeSessionId);
|
|
577
|
+
}
|
|
578
|
+
if (opts.model) {
|
|
579
|
+
cmd.push("--model", opts.model);
|
|
549
580
|
}
|
|
550
581
|
const proc = spawn({
|
|
551
582
|
cmd,
|
|
552
583
|
stdin: "ignore",
|
|
553
584
|
stdout: "pipe",
|
|
554
585
|
stderr: "pipe",
|
|
555
|
-
cwd,
|
|
586
|
+
cwd: opts.cwd,
|
|
556
587
|
env: { ...process.env }
|
|
557
588
|
});
|
|
558
589
|
proc.exited.then((code) => {
|
|
559
|
-
logger.info("claude", `process exited: ${id}`, { exitCode: code });
|
|
590
|
+
logger.info("claude", `process exited: ${opts.id}`, { exitCode: code });
|
|
560
591
|
});
|
|
561
|
-
const aiProcess = { process: proc, id };
|
|
562
|
-
processes.set(id, aiProcess);
|
|
592
|
+
const aiProcess = { process: proc, id: opts.id };
|
|
593
|
+
processes.set(opts.id, aiProcess);
|
|
563
594
|
return aiProcess;
|
|
564
595
|
}
|
|
565
596
|
function killAiProcess(id) {
|
|
@@ -574,8 +605,8 @@ function killAiProcess(id) {
|
|
|
574
605
|
}
|
|
575
606
|
return false;
|
|
576
607
|
}
|
|
577
|
-
var toolNameMap = new Map;
|
|
578
608
|
async function* parseClaudeStream(proc) {
|
|
609
|
+
const toolNameMap = new Map;
|
|
579
610
|
const stdout = proc.stdout;
|
|
580
611
|
if (!stdout || typeof stdout === "number") {
|
|
581
612
|
throw new Error("Process stdout is not a readable stream");
|
|
@@ -671,22 +702,22 @@ async function* parseClaudeStream(proc) {
|
|
|
671
702
|
}
|
|
672
703
|
|
|
673
704
|
// ../core/dist/smart-screenshot.js
|
|
674
|
-
import { join as
|
|
675
|
-
import { mkdirSync as
|
|
705
|
+
import { join as join6 } from "path";
|
|
706
|
+
import { mkdirSync as mkdirSync5, existsSync as existsSync6 } from "fs";
|
|
676
707
|
import simpleGit2 from "simple-git";
|
|
677
708
|
|
|
678
709
|
// ../core/dist/screenshot.js
|
|
679
710
|
var {spawn: spawn2 } = globalThis.Bun;
|
|
680
|
-
import { join as
|
|
681
|
-
import { mkdirSync as
|
|
711
|
+
import { join as join5 } from "path";
|
|
712
|
+
import { mkdirSync as mkdirSync4, existsSync as existsSync5 } from "fs";
|
|
682
713
|
var SCREENSHOTS_DIR = ".stashes/screenshots";
|
|
683
714
|
async function captureScreenshot(port, projectPath, stashId) {
|
|
684
|
-
const screenshotsDir =
|
|
685
|
-
if (!
|
|
686
|
-
|
|
715
|
+
const screenshotsDir = join5(projectPath, SCREENSHOTS_DIR);
|
|
716
|
+
if (!existsSync5(screenshotsDir)) {
|
|
717
|
+
mkdirSync4(screenshotsDir, { recursive: true });
|
|
687
718
|
}
|
|
688
719
|
const filename = `${stashId}.png`;
|
|
689
|
-
const outputPath =
|
|
720
|
+
const outputPath = join5(screenshotsDir, filename);
|
|
690
721
|
const playwrightScript = `
|
|
691
722
|
const { chromium } = require('playwright');
|
|
692
723
|
(async () => {
|
|
@@ -743,14 +774,8 @@ ${truncatedDiff}`;
|
|
|
743
774
|
}
|
|
744
775
|
function buildScreenshotPrompt(port, diff, screenshotDir, stashId) {
|
|
745
776
|
return [
|
|
746
|
-
"You are a screenshot assistant.
|
|
747
|
-
"",
|
|
748
|
-
"IMPORTANT RULES:",
|
|
749
|
-
"- ONLY use these Playwright MCP tools: mcp__playwright__browser_navigate, mcp__playwright__browser_take_screenshot, mcp__playwright__browser_click, mcp__playwright__browser_snapshot",
|
|
750
|
-
"- Do NOT use any other tools (no Bash, Read, Grep, Agent, ToolSearch, etc.)",
|
|
751
|
-
"- Do NOT start any sessions or call useai tools",
|
|
752
|
-
"- Do NOT read or analyze code files \u2014 the diff below tells you everything",
|
|
753
|
-
"- Be fast \u2014 you have a strict time limit",
|
|
777
|
+
"You are a screenshot assistant. Take screenshots of a running web app using Playwright MCP tools.",
|
|
778
|
+
"Be fast \u2014 you have a strict time limit.",
|
|
754
779
|
"",
|
|
755
780
|
`## The app is running at: http://localhost:${port}`,
|
|
756
781
|
"",
|
|
@@ -774,7 +799,7 @@ function buildScreenshotPrompt(port, diff, screenshotDir, stashId) {
|
|
|
774
799
|
"{",
|
|
775
800
|
' "screenshots": [',
|
|
776
801
|
" {",
|
|
777
|
-
` "path": "${
|
|
802
|
+
` "path": "${join6(screenshotDir, `${stashId}.png`)}",`,
|
|
778
803
|
' "label": "Short description of what is shown",',
|
|
779
804
|
' "route": "/the-url-path",',
|
|
780
805
|
' "isPrimary": true',
|
|
@@ -815,9 +840,9 @@ async function fallbackScreenshot(port, projectPath, stashId) {
|
|
|
815
840
|
}
|
|
816
841
|
async function captureSmartScreenshots(opts) {
|
|
817
842
|
const { projectPath, stashId, stashBranch, parentBranch, worktreePath, port, model = "sonnet", timeout = DEFAULT_TIMEOUT } = opts;
|
|
818
|
-
const screenshotDir =
|
|
819
|
-
if (!
|
|
820
|
-
|
|
843
|
+
const screenshotDir = join6(projectPath, SCREENSHOTS_DIR2);
|
|
844
|
+
if (!existsSync6(screenshotDir)) {
|
|
845
|
+
mkdirSync5(screenshotDir, { recursive: true });
|
|
821
846
|
}
|
|
822
847
|
const diff = await getStashDiff(worktreePath, parentBranch);
|
|
823
848
|
if (!diff) {
|
|
@@ -827,7 +852,15 @@ async function captureSmartScreenshots(opts) {
|
|
|
827
852
|
const processId = `screenshot-ai-${stashId}`;
|
|
828
853
|
const prompt = buildScreenshotPrompt(port, diff, screenshotDir, stashId);
|
|
829
854
|
const modelFlag = model === "sonnet" ? "sonnet" : "haiku";
|
|
830
|
-
const aiProcess = startAiProcess(
|
|
855
|
+
const aiProcess = startAiProcess({
|
|
856
|
+
id: processId,
|
|
857
|
+
prompt,
|
|
858
|
+
cwd: worktreePath,
|
|
859
|
+
model: modelFlag,
|
|
860
|
+
bare: true,
|
|
861
|
+
tools: [],
|
|
862
|
+
mcpConfigPath: getPlaywrightMcpConfigPath()
|
|
863
|
+
});
|
|
831
864
|
let textOutput = "";
|
|
832
865
|
let timedOut = false;
|
|
833
866
|
const timeoutId = setTimeout(() => {
|
|
@@ -859,7 +892,7 @@ async function captureSmartScreenshots(opts) {
|
|
|
859
892
|
let primaryUrl = "";
|
|
860
893
|
for (const shot of result.screenshots) {
|
|
861
894
|
const filename = shot.path.split("/").pop() || "";
|
|
862
|
-
if (!
|
|
895
|
+
if (!existsSync6(shot.path)) {
|
|
863
896
|
logger.warn("smart-screenshot", `Screenshot file not found: ${shot.path}`);
|
|
864
897
|
continue;
|
|
865
898
|
}
|
|
@@ -953,8 +986,8 @@ async function generate(opts) {
|
|
|
953
986
|
const selectedDirectives = directives.slice(0, count);
|
|
954
987
|
let sourceCode = "";
|
|
955
988
|
if (component?.filePath) {
|
|
956
|
-
const sourceFile =
|
|
957
|
-
if (
|
|
989
|
+
const sourceFile = join7(projectPath, component.filePath);
|
|
990
|
+
if (existsSync7(sourceFile)) {
|
|
958
991
|
sourceCode = readFileSync2(sourceFile, "utf-8");
|
|
959
992
|
}
|
|
960
993
|
}
|
|
@@ -990,7 +1023,12 @@ async function generate(opts) {
|
|
|
990
1023
|
} else {
|
|
991
1024
|
stashPrompt = buildFreeformStashPrompt(prompt, directive);
|
|
992
1025
|
}
|
|
993
|
-
const aiProcess = startAiProcess(
|
|
1026
|
+
const aiProcess = startAiProcess({
|
|
1027
|
+
id: stashId,
|
|
1028
|
+
prompt: stashPrompt,
|
|
1029
|
+
cwd: worktree.path,
|
|
1030
|
+
bare: false
|
|
1031
|
+
});
|
|
994
1032
|
try {
|
|
995
1033
|
for await (const chunk of parseClaudeStream(aiProcess.process)) {
|
|
996
1034
|
emit(onProgress, {
|
|
@@ -999,14 +1037,63 @@ async function generate(opts) {
|
|
|
999
1037
|
content: chunk.content,
|
|
1000
1038
|
streamType: chunk.type
|
|
1001
1039
|
});
|
|
1040
|
+
if (chunk.type === "tool_use" && chunk.toolName) {
|
|
1041
|
+
const knownTools = ["Read", "Write", "Edit", "Glob", "Grep", "Bash"];
|
|
1042
|
+
if (knownTools.includes(chunk.toolName)) {
|
|
1043
|
+
const filePath = chunk.toolInput?.file_path ?? chunk.toolInput?.path ?? chunk.toolInput?.command ?? undefined;
|
|
1044
|
+
const lines = chunk.toolInput?.content ? chunk.toolInput.content.split(`
|
|
1045
|
+
`).length : undefined;
|
|
1046
|
+
emit(onProgress, {
|
|
1047
|
+
type: "activity",
|
|
1048
|
+
stashId,
|
|
1049
|
+
action: chunk.toolName,
|
|
1050
|
+
file: filePath,
|
|
1051
|
+
lines,
|
|
1052
|
+
timestamp: Date.now()
|
|
1053
|
+
});
|
|
1054
|
+
}
|
|
1055
|
+
} else if (chunk.type === "thinking") {
|
|
1056
|
+
emit(onProgress, {
|
|
1057
|
+
type: "activity",
|
|
1058
|
+
stashId,
|
|
1059
|
+
action: "thinking",
|
|
1060
|
+
content: chunk.content.substring(0, 200),
|
|
1061
|
+
timestamp: Date.now()
|
|
1062
|
+
});
|
|
1063
|
+
} else if (chunk.type === "text") {
|
|
1064
|
+
emit(onProgress, {
|
|
1065
|
+
type: "activity",
|
|
1066
|
+
stashId,
|
|
1067
|
+
action: "text",
|
|
1068
|
+
content: chunk.content.substring(0, 200),
|
|
1069
|
+
timestamp: Date.now()
|
|
1070
|
+
});
|
|
1071
|
+
}
|
|
1002
1072
|
}
|
|
1003
1073
|
await aiProcess.process.exited;
|
|
1004
1074
|
const wtGit = simpleGit3(worktree.path);
|
|
1075
|
+
let hasChanges = false;
|
|
1005
1076
|
try {
|
|
1006
1077
|
await wtGit.add("-A");
|
|
1007
|
-
await wtGit.
|
|
1008
|
-
|
|
1078
|
+
const status = await wtGit.status();
|
|
1079
|
+
if (status.staged.length > 0) {
|
|
1080
|
+
await wtGit.commit(`stashes: stash ${stashId}`);
|
|
1081
|
+
hasChanges = true;
|
|
1082
|
+
logger.info("generation", `committed changes for ${stashId}`, { files: status.staged.length });
|
|
1083
|
+
} else {
|
|
1084
|
+
logger.warn("generation", `AI produced no file changes for ${stashId} \u2014 skipping`);
|
|
1085
|
+
}
|
|
1086
|
+
} catch (commitErr) {
|
|
1087
|
+
logger.warn("generation", `commit failed for ${stashId}`, {
|
|
1088
|
+
error: commitErr instanceof Error ? commitErr.message : String(commitErr)
|
|
1089
|
+
});
|
|
1090
|
+
}
|
|
1009
1091
|
await worktreeManager.removeGeneration(stashId);
|
|
1092
|
+
if (!hasChanges) {
|
|
1093
|
+
persistence.saveStash({ ...stash, status: "error", error: "AI generation produced no file changes" });
|
|
1094
|
+
emit(onProgress, { type: "error", stashId, error: "AI generation produced no file changes" });
|
|
1095
|
+
return;
|
|
1096
|
+
}
|
|
1010
1097
|
const generatedStash = { ...stash, status: "screenshotting" };
|
|
1011
1098
|
completedStashes.push(generatedStash);
|
|
1012
1099
|
persistence.saveStash(generatedStash);
|
|
@@ -1137,7 +1224,12 @@ async function vary(opts) {
|
|
|
1137
1224
|
persistence.saveStash(stash);
|
|
1138
1225
|
emit2(onProgress, { type: "generating", stashId, number: stashNumber });
|
|
1139
1226
|
const varyPrompt = `The user wants to vary the current UI. Apply this change: ${prompt}`;
|
|
1140
|
-
const aiProcess = startAiProcess(
|
|
1227
|
+
const aiProcess = startAiProcess({
|
|
1228
|
+
id: stashId,
|
|
1229
|
+
prompt: varyPrompt,
|
|
1230
|
+
cwd: worktree.path,
|
|
1231
|
+
bare: false
|
|
1232
|
+
});
|
|
1141
1233
|
try {
|
|
1142
1234
|
for await (const chunk of parseClaudeStream(aiProcess.process)) {
|
|
1143
1235
|
emit2(onProgress, {
|
|
@@ -1149,11 +1241,29 @@ async function vary(opts) {
|
|
|
1149
1241
|
}
|
|
1150
1242
|
await aiProcess.process.exited;
|
|
1151
1243
|
const wtGit = simpleGit4(worktree.path);
|
|
1244
|
+
let hasChanges = false;
|
|
1152
1245
|
try {
|
|
1153
1246
|
await wtGit.add("-A");
|
|
1154
|
-
await wtGit.
|
|
1155
|
-
|
|
1247
|
+
const status = await wtGit.status();
|
|
1248
|
+
if (status.staged.length > 0) {
|
|
1249
|
+
await wtGit.commit(`stashes: vary ${stashId} from ${sourceStashId}`);
|
|
1250
|
+
hasChanges = true;
|
|
1251
|
+
logger.info("vary", `committed changes for ${stashId}`, { files: status.staged.length });
|
|
1252
|
+
} else {
|
|
1253
|
+
logger.warn("vary", `AI produced no file changes for ${stashId}`);
|
|
1254
|
+
}
|
|
1255
|
+
} catch (commitErr) {
|
|
1256
|
+
logger.warn("vary", `commit failed for ${stashId}`, {
|
|
1257
|
+
error: commitErr instanceof Error ? commitErr.message : String(commitErr)
|
|
1258
|
+
});
|
|
1259
|
+
}
|
|
1156
1260
|
await worktreeManager.removeGeneration(stashId);
|
|
1261
|
+
if (!hasChanges) {
|
|
1262
|
+
const errorStash = { ...stash, status: "error", error: "AI generation produced no file changes" };
|
|
1263
|
+
persistence.saveStash(errorStash);
|
|
1264
|
+
emit2(onProgress, { type: "error", stashId, error: "AI generation produced no file changes" });
|
|
1265
|
+
return errorStash;
|
|
1266
|
+
}
|
|
1157
1267
|
persistence.saveStash({ ...stash, status: "screenshotting" });
|
|
1158
1268
|
emit2(onProgress, { type: "screenshotting", stashId });
|
|
1159
1269
|
let screenshotPath = "";
|
|
@@ -1274,13 +1384,14 @@ async function show(projectPath, stashId) {
|
|
|
1274
1384
|
return { stash: found, diff, files };
|
|
1275
1385
|
}
|
|
1276
1386
|
// ../mcp/src/tools/generate.ts
|
|
1387
|
+
var HONO_PORT = process.env.STASHES_PORT ?? "4000";
|
|
1277
1388
|
var generateParams = {
|
|
1278
1389
|
prompt: z.string().describe('What UI changes to generate (e.g. "make the hero section bolder")'),
|
|
1279
1390
|
count: z.number().min(1).max(5).optional().describe("Number of stashes to generate (1-5, default 3)"),
|
|
1280
1391
|
filePath: z.string().optional().describe('Optional: scope changes to a specific file (e.g. "src/components/Hero.tsx")'),
|
|
1281
1392
|
exportName: z.string().optional().describe("Optional: specific component export name within the file")
|
|
1282
1393
|
};
|
|
1283
|
-
async function handleGenerate(args, projectPath) {
|
|
1394
|
+
async function handleGenerate(args, projectPath, server) {
|
|
1284
1395
|
const { prompt, filePath, exportName } = args;
|
|
1285
1396
|
const count = args.count ?? 3;
|
|
1286
1397
|
initLogFile(projectPath);
|
|
@@ -1298,14 +1409,41 @@ async function handleGenerate(args, projectPath) {
|
|
|
1298
1409
|
};
|
|
1299
1410
|
persistence.saveProject(project);
|
|
1300
1411
|
}
|
|
1412
|
+
let pendingEvents = [];
|
|
1413
|
+
let flushTimer = null;
|
|
1414
|
+
function flushPending() {
|
|
1415
|
+
if (pendingEvents.length === 0)
|
|
1416
|
+
return;
|
|
1417
|
+
const batch = pendingEvents.splice(0);
|
|
1418
|
+
flushTimer = null;
|
|
1419
|
+
fetch(`http://localhost:${HONO_PORT}/api/stash-activity`, {
|
|
1420
|
+
method: "POST",
|
|
1421
|
+
headers: { "Content-Type": "application/json" },
|
|
1422
|
+
body: JSON.stringify(batch.filter((e) => e.type === "activity"))
|
|
1423
|
+
}).catch(() => {});
|
|
1424
|
+
}
|
|
1301
1425
|
const stashes = await generate({
|
|
1302
1426
|
projectPath,
|
|
1303
1427
|
projectId: project.id,
|
|
1304
1428
|
prompt,
|
|
1305
1429
|
count,
|
|
1306
1430
|
component: filePath ? { filePath, exportName } : undefined,
|
|
1307
|
-
onProgress: () => {
|
|
1431
|
+
onProgress: (event) => {
|
|
1432
|
+
if (event.type === "activity") {
|
|
1433
|
+
pendingEvents.push(event);
|
|
1434
|
+
if (!flushTimer) {
|
|
1435
|
+
flushTimer = setTimeout(flushPending, 100);
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
server.server.sendLoggingMessage({
|
|
1439
|
+
level: "info",
|
|
1440
|
+
data: event
|
|
1441
|
+
}).catch(() => {});
|
|
1442
|
+
}
|
|
1308
1443
|
});
|
|
1444
|
+
if (flushTimer)
|
|
1445
|
+
clearTimeout(flushTimer);
|
|
1446
|
+
flushPending();
|
|
1309
1447
|
return {
|
|
1310
1448
|
content: [{
|
|
1311
1449
|
type: "text",
|
|
@@ -1452,150 +1590,17 @@ async function handleRemove(args, projectPath) {
|
|
|
1452
1590
|
// ../server/dist/index.js
|
|
1453
1591
|
import { Hono as Hono2 } from "hono";
|
|
1454
1592
|
import { cors } from "hono/cors";
|
|
1455
|
-
import { join as
|
|
1593
|
+
import { join as join11, dirname as dirname3 } from "path";
|
|
1456
1594
|
import { fileURLToPath } from "url";
|
|
1457
|
-
import { existsSync as
|
|
1595
|
+
import { existsSync as existsSync11, readFileSync as readFileSync6 } from "fs";
|
|
1458
1596
|
|
|
1459
1597
|
// ../server/dist/routes/api.js
|
|
1460
1598
|
import { Hono } from "hono";
|
|
1461
|
-
import { join as
|
|
1462
|
-
import { existsSync as
|
|
1463
|
-
var app = new Hono;
|
|
1464
|
-
app.get("/health", (c) => c.json({ status: "ok", service: "stashes" }));
|
|
1465
|
-
app.get("/projects", (c) => {
|
|
1466
|
-
const persistence = getPersistence();
|
|
1467
|
-
const projects = persistence.listProjects();
|
|
1468
|
-
const projectsWithCounts = projects.map((p) => ({
|
|
1469
|
-
...p,
|
|
1470
|
-
stashCount: persistence.listStashes(p.id).length,
|
|
1471
|
-
recentScreenshots: persistence.listStashes(p.id).filter((s) => s.screenshotUrl).slice(-4).map((s) => s.screenshotUrl)
|
|
1472
|
-
}));
|
|
1473
|
-
return c.json({ data: projectsWithCounts });
|
|
1474
|
-
});
|
|
1475
|
-
app.post("/projects", async (c) => {
|
|
1476
|
-
const { name, description } = await c.req.json();
|
|
1477
|
-
const project = {
|
|
1478
|
-
id: `proj_${crypto.randomUUID().substring(0, 8)}`,
|
|
1479
|
-
name,
|
|
1480
|
-
description,
|
|
1481
|
-
createdAt: new Date().toISOString(),
|
|
1482
|
-
updatedAt: new Date().toISOString()
|
|
1483
|
-
};
|
|
1484
|
-
getPersistence().saveProject(project);
|
|
1485
|
-
return c.json({ data: project }, 201);
|
|
1486
|
-
});
|
|
1487
|
-
app.get("/projects/:id", (c) => {
|
|
1488
|
-
const persistence = getPersistence();
|
|
1489
|
-
const project = persistence.getProject(c.req.param("id"));
|
|
1490
|
-
if (!project)
|
|
1491
|
-
return c.json({ error: "Project not found" }, 404);
|
|
1492
|
-
const stashes = persistence.listStashes(project.id);
|
|
1493
|
-
const chats = persistence.listChats(project.id);
|
|
1494
|
-
return c.json({ data: { ...project, stashes, chats } });
|
|
1495
|
-
});
|
|
1496
|
-
app.delete("/projects/:id", (c) => {
|
|
1497
|
-
const id = c.req.param("id");
|
|
1498
|
-
getPersistence().deleteProject(id);
|
|
1499
|
-
return c.json({ data: { deleted: id } });
|
|
1500
|
-
});
|
|
1501
|
-
app.get("/chats", (c) => {
|
|
1502
|
-
const persistence = getPersistence();
|
|
1503
|
-
const project = ensureProject(persistence);
|
|
1504
|
-
const chats = persistence.listChats(project.id);
|
|
1505
|
-
const stashes = persistence.listStashes(project.id);
|
|
1506
|
-
return c.json({ data: { project, chats, stashes } });
|
|
1507
|
-
});
|
|
1508
|
-
app.post("/chats", async (c) => {
|
|
1509
|
-
const persistence = getPersistence();
|
|
1510
|
-
const project = ensureProject(persistence);
|
|
1511
|
-
const { title, referencedStashIds } = await c.req.json();
|
|
1512
|
-
const chatCount = persistence.listChats(project.id).length;
|
|
1513
|
-
const chat = {
|
|
1514
|
-
id: `chat_${crypto.randomUUID().substring(0, 8)}`,
|
|
1515
|
-
projectId: project.id,
|
|
1516
|
-
title: title?.trim() || `Chat ${chatCount + 1}`,
|
|
1517
|
-
referencedStashIds: referencedStashIds ?? [],
|
|
1518
|
-
createdAt: new Date().toISOString(),
|
|
1519
|
-
updatedAt: new Date().toISOString()
|
|
1520
|
-
};
|
|
1521
|
-
persistence.saveChat(chat);
|
|
1522
|
-
return c.json({ data: chat }, 201);
|
|
1523
|
-
});
|
|
1524
|
-
app.patch("/chats/:chatId", async (c) => {
|
|
1525
|
-
const persistence = getPersistence();
|
|
1526
|
-
const project = ensureProject(persistence);
|
|
1527
|
-
const chatId = c.req.param("chatId");
|
|
1528
|
-
const chat = persistence.getChat(project.id, chatId);
|
|
1529
|
-
if (!chat)
|
|
1530
|
-
return c.json({ error: "Chat not found" }, 404);
|
|
1531
|
-
const body = await c.req.json();
|
|
1532
|
-
const updated = {
|
|
1533
|
-
...chat,
|
|
1534
|
-
...body.referencedStashIds !== undefined ? { referencedStashIds: body.referencedStashIds } : {},
|
|
1535
|
-
updatedAt: new Date().toISOString()
|
|
1536
|
-
};
|
|
1537
|
-
persistence.saveChat(updated);
|
|
1538
|
-
return c.json({ data: updated });
|
|
1539
|
-
});
|
|
1540
|
-
app.get("/chats/:chatId", (c) => {
|
|
1541
|
-
const persistence = getPersistence();
|
|
1542
|
-
const project = ensureProject(persistence);
|
|
1543
|
-
const chatId = c.req.param("chatId");
|
|
1544
|
-
const chat = persistence.getChat(project.id, chatId);
|
|
1545
|
-
if (!chat)
|
|
1546
|
-
return c.json({ error: "Chat not found" }, 404);
|
|
1547
|
-
const messages = persistence.getChatMessages(project.id, chatId);
|
|
1548
|
-
const refIds = new Set(chat.referencedStashIds ?? []);
|
|
1549
|
-
const stashes = persistence.listStashes(project.id).filter((s) => s.originChatId === chatId || refIds.has(s.id));
|
|
1550
|
-
return c.json({ data: { ...chat, messages, stashes } });
|
|
1551
|
-
});
|
|
1552
|
-
app.delete("/chats/:chatId", (c) => {
|
|
1553
|
-
const persistence = getPersistence();
|
|
1554
|
-
const project = ensureProject(persistence);
|
|
1555
|
-
const chatId = c.req.param("chatId");
|
|
1556
|
-
persistence.deleteChat(project.id, chatId);
|
|
1557
|
-
return c.json({ data: { deleted: chatId } });
|
|
1558
|
-
});
|
|
1559
|
-
app.get("/dev-server-status", async (c) => {
|
|
1560
|
-
const port = serverState.userDevPort;
|
|
1561
|
-
try {
|
|
1562
|
-
const res = await fetch(`http://localhost:${port}`, {
|
|
1563
|
-
method: "HEAD",
|
|
1564
|
-
signal: AbortSignal.timeout(2000)
|
|
1565
|
-
});
|
|
1566
|
-
return c.json({ up: res.status < 500, port });
|
|
1567
|
-
} catch {
|
|
1568
|
-
return c.json({ up: false, port });
|
|
1569
|
-
}
|
|
1570
|
-
});
|
|
1571
|
-
app.get("/screenshots/:filename", (c) => {
|
|
1572
|
-
const filename = c.req.param("filename");
|
|
1573
|
-
const filePath = join7(serverState.projectPath, ".stashes", "screenshots", filename);
|
|
1574
|
-
if (!existsSync7(filePath))
|
|
1575
|
-
return c.json({ error: "Not found" }, 404);
|
|
1576
|
-
const content = readFileSync3(filePath);
|
|
1577
|
-
return new Response(content, {
|
|
1578
|
-
headers: { "content-type": "image/png", "cache-control": "no-cache" }
|
|
1579
|
-
});
|
|
1580
|
-
});
|
|
1581
|
-
function ensureProject(persistence) {
|
|
1582
|
-
const projects = persistence.listProjects();
|
|
1583
|
-
if (projects.length > 0)
|
|
1584
|
-
return projects[0];
|
|
1585
|
-
const project = {
|
|
1586
|
-
id: `proj_${crypto.randomUUID().substring(0, 8)}`,
|
|
1587
|
-
name: basename(serverState.projectPath),
|
|
1588
|
-
createdAt: new Date().toISOString(),
|
|
1589
|
-
updatedAt: new Date().toISOString()
|
|
1590
|
-
};
|
|
1591
|
-
persistence.saveProject(project);
|
|
1592
|
-
persistence.migrateOldChat(project.id);
|
|
1593
|
-
return project;
|
|
1594
|
-
}
|
|
1595
|
-
var apiRoutes = app;
|
|
1599
|
+
import { join as join10, basename } from "path";
|
|
1600
|
+
import { existsSync as existsSync10, readFileSync as readFileSync5 } from "fs";
|
|
1596
1601
|
|
|
1597
1602
|
// ../server/dist/services/stash-service.js
|
|
1598
|
-
import { readFileSync as
|
|
1603
|
+
import { readFileSync as readFileSync3, existsSync as existsSync8 } from "fs";
|
|
1599
1604
|
import { join as join8 } from "path";
|
|
1600
1605
|
|
|
1601
1606
|
// ../server/dist/services/app-proxy.js
|
|
@@ -2079,6 +2084,7 @@ class StashService {
|
|
|
2079
2084
|
worktreeManager;
|
|
2080
2085
|
persistence;
|
|
2081
2086
|
broadcast;
|
|
2087
|
+
activityStore;
|
|
2082
2088
|
previewPool;
|
|
2083
2089
|
selectedComponent = null;
|
|
2084
2090
|
messageQueue = [];
|
|
@@ -2088,11 +2094,12 @@ class StashService {
|
|
|
2088
2094
|
stashPollTimer = null;
|
|
2089
2095
|
knownStashIds = new Set;
|
|
2090
2096
|
pendingComponentResolve = null;
|
|
2091
|
-
constructor(projectPath, worktreeManager, persistence, broadcast, stashPortStart, stashPortEnd) {
|
|
2097
|
+
constructor(projectPath, worktreeManager, persistence, broadcast, activityStore, stashPortStart, stashPortEnd) {
|
|
2092
2098
|
this.projectPath = projectPath;
|
|
2093
2099
|
this.worktreeManager = worktreeManager;
|
|
2094
2100
|
this.persistence = persistence;
|
|
2095
2101
|
this.broadcast = broadcast;
|
|
2102
|
+
this.activityStore = activityStore;
|
|
2096
2103
|
this.previewPool = new PreviewPool(worktreeManager, broadcast, undefined, undefined, stashPortStart, stashPortEnd);
|
|
2097
2104
|
}
|
|
2098
2105
|
getActiveChatId() {
|
|
@@ -2126,7 +2133,14 @@ class StashService {
|
|
|
2126
2133
|
"Reply with ONLY the file path relative to the project root."
|
|
2127
2134
|
].join(`
|
|
2128
2135
|
`);
|
|
2129
|
-
const aiProcess = startAiProcess(
|
|
2136
|
+
const aiProcess = startAiProcess({
|
|
2137
|
+
id: "resolve-component",
|
|
2138
|
+
prompt,
|
|
2139
|
+
cwd: this.projectPath,
|
|
2140
|
+
model: "claude-haiku-4-5-20251001",
|
|
2141
|
+
bare: true,
|
|
2142
|
+
tools: ["Read", "Grep", "Glob", "Bash"]
|
|
2143
|
+
});
|
|
2130
2144
|
let resolvedPath = "";
|
|
2131
2145
|
try {
|
|
2132
2146
|
for await (const chunk of parseClaudeStream(aiProcess.process)) {
|
|
@@ -2189,7 +2203,7 @@ class StashService {
|
|
|
2189
2203
|
if (filePath && filePath !== "auto-detect") {
|
|
2190
2204
|
const sourceFile = join8(this.projectPath, filePath);
|
|
2191
2205
|
if (existsSync8(sourceFile)) {
|
|
2192
|
-
sourceCode =
|
|
2206
|
+
sourceCode = readFileSync3(sourceFile, "utf-8");
|
|
2193
2207
|
}
|
|
2194
2208
|
}
|
|
2195
2209
|
let stashContext = "";
|
|
@@ -2236,7 +2250,13 @@ ${sourceCode.substring(0, 3000)}
|
|
|
2236
2250
|
].filter(Boolean).join(`
|
|
2237
2251
|
`);
|
|
2238
2252
|
}
|
|
2239
|
-
const aiProcess = startAiProcess(
|
|
2253
|
+
const aiProcess = startAiProcess({
|
|
2254
|
+
id: "chat",
|
|
2255
|
+
prompt: chatPrompt,
|
|
2256
|
+
cwd: this.projectPath,
|
|
2257
|
+
resumeSessionId: existingSessionId,
|
|
2258
|
+
bare: false
|
|
2259
|
+
});
|
|
2240
2260
|
let thinkingBuf = "";
|
|
2241
2261
|
let textBuf = "";
|
|
2242
2262
|
const now = new Date().toISOString();
|
|
@@ -2312,6 +2332,21 @@ ${sourceCode.substring(0, 3000)}
|
|
|
2312
2332
|
}
|
|
2313
2333
|
} else if (chunk.type === "tool_result") {
|
|
2314
2334
|
this.stopStashPoll();
|
|
2335
|
+
let stashActivity;
|
|
2336
|
+
const toolNameForSnapshot = chunk.toolName ?? "";
|
|
2337
|
+
if (toolNameForSnapshot.includes("stashes_generate") || toolNameForSnapshot.includes("stashes_vary")) {
|
|
2338
|
+
const projectId2 = this.persistence.listProjects()[0]?.id ?? "";
|
|
2339
|
+
const allStashes = this.persistence.listStashes(projectId2);
|
|
2340
|
+
stashActivity = {};
|
|
2341
|
+
for (const s of allStashes) {
|
|
2342
|
+
if (this.activityStore.has(s.id)) {
|
|
2343
|
+
stashActivity[s.id] = this.activityStore.getSnapshot(s.id);
|
|
2344
|
+
this.activityStore.clear(s.id);
|
|
2345
|
+
}
|
|
2346
|
+
}
|
|
2347
|
+
if (Object.keys(stashActivity).length === 0)
|
|
2348
|
+
stashActivity = undefined;
|
|
2349
|
+
}
|
|
2315
2350
|
let toolResult = chunk.content;
|
|
2316
2351
|
let isError = false;
|
|
2317
2352
|
try {
|
|
@@ -2326,6 +2361,7 @@ ${sourceCode.substring(0, 3000)}
|
|
|
2326
2361
|
type: "tool_end",
|
|
2327
2362
|
toolStatus: isError ? "error" : "completed",
|
|
2328
2363
|
toolResult: toolResult.substring(0, 300),
|
|
2364
|
+
stashActivity,
|
|
2329
2365
|
createdAt: now
|
|
2330
2366
|
});
|
|
2331
2367
|
this.broadcast({
|
|
@@ -2515,10 +2551,77 @@ ${refDescriptions.join(`
|
|
|
2515
2551
|
}
|
|
2516
2552
|
}
|
|
2517
2553
|
|
|
2554
|
+
// ../server/dist/services/activity-store.js
|
|
2555
|
+
import { appendFileSync as appendFileSync2, readFileSync as readFileSync4, existsSync as existsSync9, mkdirSync as mkdirSync6, rmSync as rmSync3 } from "fs";
|
|
2556
|
+
import { join as join9, dirname as dirname2 } from "path";
|
|
2557
|
+
|
|
2558
|
+
class ActivityStore {
|
|
2559
|
+
cache = new Map;
|
|
2560
|
+
projectPath;
|
|
2561
|
+
constructor(projectPath) {
|
|
2562
|
+
this.projectPath = projectPath;
|
|
2563
|
+
}
|
|
2564
|
+
jsonlPath(stashId) {
|
|
2565
|
+
return join9(this.projectPath, ".stashes", "activity", `${stashId}.jsonl`);
|
|
2566
|
+
}
|
|
2567
|
+
append(event) {
|
|
2568
|
+
const existing = this.cache.get(event.stashId) ?? [];
|
|
2569
|
+
existing.push(event);
|
|
2570
|
+
this.cache.set(event.stashId, existing);
|
|
2571
|
+
const filePath = this.jsonlPath(event.stashId);
|
|
2572
|
+
const dir = dirname2(filePath);
|
|
2573
|
+
if (!existsSync9(dir))
|
|
2574
|
+
mkdirSync6(dir, { recursive: true });
|
|
2575
|
+
appendFileSync2(filePath, JSON.stringify(event) + `
|
|
2576
|
+
`, "utf-8");
|
|
2577
|
+
}
|
|
2578
|
+
getEvents(stashId) {
|
|
2579
|
+
const cached = this.cache.get(stashId);
|
|
2580
|
+
if (cached && cached.length > 0)
|
|
2581
|
+
return cached;
|
|
2582
|
+
const filePath = this.jsonlPath(stashId);
|
|
2583
|
+
if (!existsSync9(filePath))
|
|
2584
|
+
return [];
|
|
2585
|
+
const lines = readFileSync4(filePath, "utf-8").trim().split(`
|
|
2586
|
+
`).filter(Boolean);
|
|
2587
|
+
const events = lines.map((line) => JSON.parse(line));
|
|
2588
|
+
this.cache.set(stashId, events);
|
|
2589
|
+
return events;
|
|
2590
|
+
}
|
|
2591
|
+
getSnapshot(stashId) {
|
|
2592
|
+
const actions = this.getEvents(stashId);
|
|
2593
|
+
const fileActions = actions.filter((a) => ["Read", "Write", "Edit", "Glob", "Grep", "Bash"].includes(a.action));
|
|
2594
|
+
const uniqueFiles = new Set(fileActions.filter((a) => a.file).map((a) => a.file));
|
|
2595
|
+
const timestamps = actions.map((a) => a.timestamp);
|
|
2596
|
+
const duration = timestamps.length > 1 ? Math.round((Math.max(...timestamps) - Math.min(...timestamps)) / 1000) : 0;
|
|
2597
|
+
return {
|
|
2598
|
+
actions: [...actions],
|
|
2599
|
+
stats: {
|
|
2600
|
+
filesChanged: uniqueFiles.size,
|
|
2601
|
+
duration,
|
|
2602
|
+
totalActions: actions.length
|
|
2603
|
+
}
|
|
2604
|
+
};
|
|
2605
|
+
}
|
|
2606
|
+
clear(stashId) {
|
|
2607
|
+
this.cache.delete(stashId);
|
|
2608
|
+
const filePath = this.jsonlPath(stashId);
|
|
2609
|
+
if (existsSync9(filePath)) {
|
|
2610
|
+
rmSync3(filePath);
|
|
2611
|
+
}
|
|
2612
|
+
}
|
|
2613
|
+
has(stashId) {
|
|
2614
|
+
if (this.cache.has(stashId) && (this.cache.get(stashId)?.length ?? 0) > 0)
|
|
2615
|
+
return true;
|
|
2616
|
+
return existsSync9(this.jsonlPath(stashId));
|
|
2617
|
+
}
|
|
2618
|
+
}
|
|
2619
|
+
|
|
2518
2620
|
// ../server/dist/services/websocket.js
|
|
2519
2621
|
var worktreeManager;
|
|
2520
2622
|
var stashService;
|
|
2521
2623
|
var persistence;
|
|
2624
|
+
var activityStore;
|
|
2522
2625
|
var clients = new Set;
|
|
2523
2626
|
function broadcast(event) {
|
|
2524
2627
|
const data = JSON.stringify(event);
|
|
@@ -2529,10 +2632,14 @@ function broadcast(event) {
|
|
|
2529
2632
|
function getPersistenceFromWs() {
|
|
2530
2633
|
return persistence;
|
|
2531
2634
|
}
|
|
2635
|
+
function getActivityStoreFromWs() {
|
|
2636
|
+
return activityStore;
|
|
2637
|
+
}
|
|
2532
2638
|
function createWebSocketHandler(projectPath, userDevPort, appProxyPort, stashPortStart, stashPortEnd) {
|
|
2533
2639
|
worktreeManager = new WorktreeManager(projectPath);
|
|
2534
2640
|
persistence = new PersistenceService(projectPath);
|
|
2535
|
-
|
|
2641
|
+
activityStore = new ActivityStore(projectPath);
|
|
2642
|
+
stashService = new StashService(projectPath, worktreeManager, persistence, broadcast, activityStore, stashPortStart, stashPortEnd);
|
|
2536
2643
|
return {
|
|
2537
2644
|
open(ws) {
|
|
2538
2645
|
clients.add(ws);
|
|
@@ -2549,6 +2656,15 @@ function createWebSocketHandler(projectPath, userDevPort, appProxyPort, stashPor
|
|
|
2549
2656
|
if (activeChatId) {
|
|
2550
2657
|
ws.send(JSON.stringify({ type: "processing", chatId: activeChatId }));
|
|
2551
2658
|
}
|
|
2659
|
+
const allStashes = persistence.listStashes(project.id);
|
|
2660
|
+
for (const stash of allStashes) {
|
|
2661
|
+
if (stash.status === "generating" && activityStore.has(stash.id)) {
|
|
2662
|
+
const events = activityStore.getEvents(stash.id);
|
|
2663
|
+
for (const event of events) {
|
|
2664
|
+
ws.send(JSON.stringify({ type: "stash:activity", stashId: stash.id, event }));
|
|
2665
|
+
}
|
|
2666
|
+
}
|
|
2667
|
+
}
|
|
2552
2668
|
},
|
|
2553
2669
|
async message(ws, message) {
|
|
2554
2670
|
const raw = typeof message === "string" ? message : new TextDecoder().decode(message);
|
|
@@ -2628,6 +2744,156 @@ function createWebSocketHandler(projectPath, userDevPort, appProxyPort, stashPor
|
|
|
2628
2744
|
};
|
|
2629
2745
|
}
|
|
2630
2746
|
|
|
2747
|
+
// ../server/dist/routes/api.js
|
|
2748
|
+
var app = new Hono;
|
|
2749
|
+
app.get("/health", (c) => c.json({ status: "ok", service: "stashes" }));
|
|
2750
|
+
app.get("/projects", (c) => {
|
|
2751
|
+
const persistence2 = getPersistence();
|
|
2752
|
+
const projects = persistence2.listProjects();
|
|
2753
|
+
const projectsWithCounts = projects.map((p) => ({
|
|
2754
|
+
...p,
|
|
2755
|
+
stashCount: persistence2.listStashes(p.id).length,
|
|
2756
|
+
recentScreenshots: persistence2.listStashes(p.id).filter((s) => s.screenshotUrl).slice(-4).map((s) => s.screenshotUrl)
|
|
2757
|
+
}));
|
|
2758
|
+
return c.json({ data: projectsWithCounts });
|
|
2759
|
+
});
|
|
2760
|
+
app.post("/projects", async (c) => {
|
|
2761
|
+
const { name, description } = await c.req.json();
|
|
2762
|
+
const project = {
|
|
2763
|
+
id: `proj_${crypto.randomUUID().substring(0, 8)}`,
|
|
2764
|
+
name,
|
|
2765
|
+
description,
|
|
2766
|
+
createdAt: new Date().toISOString(),
|
|
2767
|
+
updatedAt: new Date().toISOString()
|
|
2768
|
+
};
|
|
2769
|
+
getPersistence().saveProject(project);
|
|
2770
|
+
return c.json({ data: project }, 201);
|
|
2771
|
+
});
|
|
2772
|
+
app.get("/projects/:id", (c) => {
|
|
2773
|
+
const persistence2 = getPersistence();
|
|
2774
|
+
const project = persistence2.getProject(c.req.param("id"));
|
|
2775
|
+
if (!project)
|
|
2776
|
+
return c.json({ error: "Project not found" }, 404);
|
|
2777
|
+
const stashes = persistence2.listStashes(project.id);
|
|
2778
|
+
const chats = persistence2.listChats(project.id);
|
|
2779
|
+
return c.json({ data: { ...project, stashes, chats } });
|
|
2780
|
+
});
|
|
2781
|
+
app.delete("/projects/:id", (c) => {
|
|
2782
|
+
const id = c.req.param("id");
|
|
2783
|
+
getPersistence().deleteProject(id);
|
|
2784
|
+
return c.json({ data: { deleted: id } });
|
|
2785
|
+
});
|
|
2786
|
+
app.get("/chats", (c) => {
|
|
2787
|
+
const persistence2 = getPersistence();
|
|
2788
|
+
const project = ensureProject(persistence2);
|
|
2789
|
+
const chats = persistence2.listChats(project.id);
|
|
2790
|
+
const stashes = persistence2.listStashes(project.id);
|
|
2791
|
+
return c.json({ data: { project, chats, stashes } });
|
|
2792
|
+
});
|
|
2793
|
+
app.post("/chats", async (c) => {
|
|
2794
|
+
const persistence2 = getPersistence();
|
|
2795
|
+
const project = ensureProject(persistence2);
|
|
2796
|
+
const { title, referencedStashIds } = await c.req.json();
|
|
2797
|
+
const chatCount = persistence2.listChats(project.id).length;
|
|
2798
|
+
const chat = {
|
|
2799
|
+
id: `chat_${crypto.randomUUID().substring(0, 8)}`,
|
|
2800
|
+
projectId: project.id,
|
|
2801
|
+
title: title?.trim() || `Chat ${chatCount + 1}`,
|
|
2802
|
+
referencedStashIds: referencedStashIds ?? [],
|
|
2803
|
+
createdAt: new Date().toISOString(),
|
|
2804
|
+
updatedAt: new Date().toISOString()
|
|
2805
|
+
};
|
|
2806
|
+
persistence2.saveChat(chat);
|
|
2807
|
+
return c.json({ data: chat }, 201);
|
|
2808
|
+
});
|
|
2809
|
+
app.patch("/chats/:chatId", async (c) => {
|
|
2810
|
+
const persistence2 = getPersistence();
|
|
2811
|
+
const project = ensureProject(persistence2);
|
|
2812
|
+
const chatId = c.req.param("chatId");
|
|
2813
|
+
const chat = persistence2.getChat(project.id, chatId);
|
|
2814
|
+
if (!chat)
|
|
2815
|
+
return c.json({ error: "Chat not found" }, 404);
|
|
2816
|
+
const body = await c.req.json();
|
|
2817
|
+
const updated = {
|
|
2818
|
+
...chat,
|
|
2819
|
+
...body.referencedStashIds !== undefined ? { referencedStashIds: body.referencedStashIds } : {},
|
|
2820
|
+
updatedAt: new Date().toISOString()
|
|
2821
|
+
};
|
|
2822
|
+
persistence2.saveChat(updated);
|
|
2823
|
+
return c.json({ data: updated });
|
|
2824
|
+
});
|
|
2825
|
+
app.get("/chats/:chatId", (c) => {
|
|
2826
|
+
const persistence2 = getPersistence();
|
|
2827
|
+
const project = ensureProject(persistence2);
|
|
2828
|
+
const chatId = c.req.param("chatId");
|
|
2829
|
+
const chat = persistence2.getChat(project.id, chatId);
|
|
2830
|
+
if (!chat)
|
|
2831
|
+
return c.json({ error: "Chat not found" }, 404);
|
|
2832
|
+
const messages = persistence2.getChatMessages(project.id, chatId);
|
|
2833
|
+
const refIds = new Set(chat.referencedStashIds ?? []);
|
|
2834
|
+
const stashes = persistence2.listStashes(project.id).filter((s) => s.originChatId === chatId || refIds.has(s.id));
|
|
2835
|
+
return c.json({ data: { ...chat, messages, stashes } });
|
|
2836
|
+
});
|
|
2837
|
+
app.delete("/chats/:chatId", (c) => {
|
|
2838
|
+
const persistence2 = getPersistence();
|
|
2839
|
+
const project = ensureProject(persistence2);
|
|
2840
|
+
const chatId = c.req.param("chatId");
|
|
2841
|
+
persistence2.deleteChat(project.id, chatId);
|
|
2842
|
+
return c.json({ data: { deleted: chatId } });
|
|
2843
|
+
});
|
|
2844
|
+
app.get("/dev-server-status", async (c) => {
|
|
2845
|
+
const port = serverState.userDevPort;
|
|
2846
|
+
try {
|
|
2847
|
+
const res = await fetch(`http://localhost:${port}`, {
|
|
2848
|
+
method: "HEAD",
|
|
2849
|
+
signal: AbortSignal.timeout(2000)
|
|
2850
|
+
});
|
|
2851
|
+
return c.json({ up: res.status < 500, port });
|
|
2852
|
+
} catch {
|
|
2853
|
+
return c.json({ up: false, port });
|
|
2854
|
+
}
|
|
2855
|
+
});
|
|
2856
|
+
app.get("/screenshots/:filename", (c) => {
|
|
2857
|
+
const filename = c.req.param("filename");
|
|
2858
|
+
const filePath = join10(serverState.projectPath, ".stashes", "screenshots", filename);
|
|
2859
|
+
if (!existsSync10(filePath))
|
|
2860
|
+
return c.json({ error: "Not found" }, 404);
|
|
2861
|
+
const content = readFileSync5(filePath);
|
|
2862
|
+
return new Response(content, {
|
|
2863
|
+
headers: { "content-type": "image/png", "cache-control": "no-cache" }
|
|
2864
|
+
});
|
|
2865
|
+
});
|
|
2866
|
+
app.post("/stash-activity", async (c) => {
|
|
2867
|
+
const events = await c.req.json();
|
|
2868
|
+
const store = getActivityStoreFromWs();
|
|
2869
|
+
for (const event of events) {
|
|
2870
|
+
store.append(event);
|
|
2871
|
+
broadcast({ type: "stash:activity", stashId: event.stashId, event });
|
|
2872
|
+
}
|
|
2873
|
+
return c.json({ ok: true });
|
|
2874
|
+
});
|
|
2875
|
+
app.get("/stash-activity/:stashId", (c) => {
|
|
2876
|
+
const stashId = c.req.param("stashId");
|
|
2877
|
+
const store = getActivityStoreFromWs();
|
|
2878
|
+
const events = store.getEvents(stashId);
|
|
2879
|
+
return c.json({ data: events });
|
|
2880
|
+
});
|
|
2881
|
+
function ensureProject(persistence2) {
|
|
2882
|
+
const projects = persistence2.listProjects();
|
|
2883
|
+
if (projects.length > 0)
|
|
2884
|
+
return projects[0];
|
|
2885
|
+
const project = {
|
|
2886
|
+
id: `proj_${crypto.randomUUID().substring(0, 8)}`,
|
|
2887
|
+
name: basename(serverState.projectPath),
|
|
2888
|
+
createdAt: new Date().toISOString(),
|
|
2889
|
+
updatedAt: new Date().toISOString()
|
|
2890
|
+
};
|
|
2891
|
+
persistence2.saveProject(project);
|
|
2892
|
+
persistence2.migrateOldChat(project.id);
|
|
2893
|
+
return project;
|
|
2894
|
+
}
|
|
2895
|
+
var apiRoutes = app;
|
|
2896
|
+
|
|
2631
2897
|
// ../server/dist/index.js
|
|
2632
2898
|
var serverState = {
|
|
2633
2899
|
projectPath: "",
|
|
@@ -2641,14 +2907,14 @@ app2.use("/*", cors());
|
|
|
2641
2907
|
app2.route("/api", apiRoutes);
|
|
2642
2908
|
app2.get("/*", async (c) => {
|
|
2643
2909
|
const path = c.req.path;
|
|
2644
|
-
const selfDir =
|
|
2645
|
-
const bundledWebDir =
|
|
2646
|
-
const monorepoWebDir =
|
|
2647
|
-
const webDistDir =
|
|
2910
|
+
const selfDir = dirname3(fileURLToPath(import.meta.url));
|
|
2911
|
+
const bundledWebDir = join11(selfDir, "web");
|
|
2912
|
+
const monorepoWebDir = join11(selfDir, "../../web/dist");
|
|
2913
|
+
const webDistDir = existsSync11(join11(bundledWebDir, "index.html")) ? bundledWebDir : monorepoWebDir;
|
|
2648
2914
|
const requestPath = path === "/" ? "/index.html" : path;
|
|
2649
|
-
const filePath =
|
|
2650
|
-
if (
|
|
2651
|
-
const content =
|
|
2915
|
+
const filePath = join11(webDistDir, requestPath);
|
|
2916
|
+
if (existsSync11(filePath) && !filePath.includes("..")) {
|
|
2917
|
+
const content = readFileSync6(filePath);
|
|
2652
2918
|
const ext = filePath.split(".").pop() || "";
|
|
2653
2919
|
const contentTypes = {
|
|
2654
2920
|
html: "text/html; charset=utf-8",
|
|
@@ -2666,9 +2932,9 @@ app2.get("/*", async (c) => {
|
|
|
2666
2932
|
headers: { "content-type": contentTypes[ext] || "application/octet-stream" }
|
|
2667
2933
|
});
|
|
2668
2934
|
}
|
|
2669
|
-
const indexPath =
|
|
2670
|
-
if (
|
|
2671
|
-
const html =
|
|
2935
|
+
const indexPath = join11(webDistDir, "index.html");
|
|
2936
|
+
if (existsSync11(indexPath)) {
|
|
2937
|
+
const html = readFileSync6(indexPath, "utf-8");
|
|
2672
2938
|
return new Response(html, {
|
|
2673
2939
|
headers: { "content-type": "text/html; charset=utf-8" }
|
|
2674
2940
|
});
|
|
@@ -2731,11 +2997,11 @@ async function startServer(projectPath, userDevPort, requestedPort = STASHES_POR
|
|
|
2731
2997
|
import open from "open";
|
|
2732
2998
|
|
|
2733
2999
|
// ../server/dist/services/detector.js
|
|
2734
|
-
import { existsSync as
|
|
2735
|
-
import { join as
|
|
3000
|
+
import { existsSync as existsSync12, readFileSync as readFileSync7 } from "fs";
|
|
3001
|
+
import { join as join12 } from "path";
|
|
2736
3002
|
function detectFramework(projectPath) {
|
|
2737
|
-
const packageJsonPath =
|
|
2738
|
-
if (!
|
|
3003
|
+
const packageJsonPath = join12(projectPath, "package.json");
|
|
3004
|
+
if (!existsSync12(packageJsonPath)) {
|
|
2739
3005
|
return {
|
|
2740
3006
|
framework: "unknown",
|
|
2741
3007
|
devCommand: "npm run dev",
|
|
@@ -2743,7 +3009,7 @@ function detectFramework(projectPath) {
|
|
|
2743
3009
|
configFile: null
|
|
2744
3010
|
};
|
|
2745
3011
|
}
|
|
2746
|
-
const packageJson = JSON.parse(
|
|
3012
|
+
const packageJson = JSON.parse(readFileSync7(packageJsonPath, "utf-8"));
|
|
2747
3013
|
const deps = {
|
|
2748
3014
|
...packageJson.dependencies,
|
|
2749
3015
|
...packageJson.devDependencies
|
|
@@ -2797,7 +3063,7 @@ function getDevCommand(packageJson, fallback) {
|
|
|
2797
3063
|
}
|
|
2798
3064
|
function findConfig(projectPath, candidates) {
|
|
2799
3065
|
for (const candidate of candidates) {
|
|
2800
|
-
if (
|
|
3066
|
+
if (existsSync12(join12(projectPath, candidate))) {
|
|
2801
3067
|
return candidate;
|
|
2802
3068
|
}
|
|
2803
3069
|
}
|
|
@@ -2825,7 +3091,7 @@ var server = new McpServer({
|
|
|
2825
3091
|
name: "stashes",
|
|
2826
3092
|
version: "0.1.0"
|
|
2827
3093
|
});
|
|
2828
|
-
server.tool("stashes_generate", "Generate multiple AI-powered UI design explorations (stashes) for a given prompt. Each stash applies a different creative direction.", generateParams, async (args) => handleGenerate(args, projectPath));
|
|
3094
|
+
server.tool("stashes_generate", "Generate multiple AI-powered UI design explorations (stashes) for a given prompt. Each stash applies a different creative direction.", generateParams, async (args) => handleGenerate(args, projectPath, server));
|
|
2829
3095
|
server.tool("stashes_list", "List all existing stashes in the current project. Shows ID, prompt, status, branch, and screenshot path.", listParams, async (args) => handleList(args, projectPath));
|
|
2830
3096
|
server.tool("stashes_show", "Show detailed information about a specific stash including its git diff, changed files, prompt, and metadata. Use this to inspect what a stash changed without applying it.", showParams, async (args) => handleShow(args, projectPath));
|
|
2831
3097
|
server.tool("stashes_apply", "Merge a stash branch into the current git branch. Applies the AI-generated UI changes and cleans up all worktrees. ONLY use when the user explicitly asks to apply or merge.", applyParams, async (args) => handleApply(args, projectPath));
|