@lalalic/markcut 2.0.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/AGENTS.md +1 -0
  2. package/package.json +1 -1
  3. package/{SKILL.md → skills/markcut/SKILL.md} +8 -2
  4. package/{docs → skills/markcut/docs}/system-prompt-edit.md +5 -2
  5. package/skills/markcut/docs/template.md +213 -0
  6. package/{templates → skills/markcut/docs/templates}/courseware/TEMPLATE.md +220 -10
  7. package/{templates → skills/markcut/docs/templates}/courseware/prompts/outline.md +5 -0
  8. package/{templates → skills/markcut/docs/templates}/courseware/prompts/scene.md +6 -0
  9. package/skills/markcut/docs/templates/deep-dive/TEMPLATE.md +355 -0
  10. package/skills/markcut/docs/templates/deep-dive/agents/researcher.md +69 -0
  11. package/skills/markcut/docs/templates/deep-dive/agents/reviewer.md +108 -0
  12. package/skills/markcut/docs/templates/deep-dive/prompts/fix.md +28 -0
  13. package/skills/markcut/docs/templates/deep-dive/prompts/outline.md +102 -0
  14. package/skills/markcut/docs/templates/deep-dive/prompts/script.md +64 -0
  15. package/skills/markcut/docs/templates/illustrated-book/TEMPLATE.md +360 -0
  16. package/skills/markcut/docs/templates/illustrated-book/agents/reviewer.md +115 -0
  17. package/skills/markcut/docs/templates/illustrated-book/prompts/fix.md +29 -0
  18. package/skills/markcut/docs/templates/illustrated-book/prompts/illustration.md +35 -0
  19. package/skills/markcut/docs/templates/illustrated-book/prompts/story.md +69 -0
  20. package/skills/markcut/docs/templates/short-film/TEMPLATE.md +387 -0
  21. package/skills/markcut/docs/templates/short-film/agents/reviewer.md +115 -0
  22. package/skills/markcut/docs/templates/short-film/prompts/fix.md +29 -0
  23. package/skills/markcut/docs/templates/short-film/prompts/screenplay.md +101 -0
  24. package/skills/markcut/docs/templates/short-film/prompts/storyboard.md +54 -0
  25. package/skills/markcut/docs/templates/short-video/TEMPLATE.md +302 -0
  26. package/skills/markcut/docs/templates/short-video/agents/reviewer.md +94 -0
  27. package/skills/markcut/docs/templates/short-video/prompts/fix.md +27 -0
  28. package/skills/markcut/docs/templates/short-video/prompts/script.md +65 -0
  29. package/skills/markcut/docs/templates/vlog/TEMPLATE.md +405 -0
  30. package/skills/markcut/docs/templates/vlog/agents/reviewer.md +89 -0
  31. package/skills/markcut/docs/templates/vlog/agents/story-writer.md +100 -0
  32. package/skills/markcut/docs/templates/vlog/prompts/bgm-select.md +38 -0
  33. package/skills/markcut/docs/templates/vlog/prompts/group-clips.md +93 -0
  34. package/skills/markcut/docs/templates/vlog/prompts/local-context.md +59 -0
  35. package/skills/markcut/docs/templates/vlog/prompts/outline.md +56 -0
  36. package/skills/markcut/docs/templates/vlog/prompts/review-fix.md +27 -0
  37. package/skills/markcut/docs/templates/vlog/prompts/storyboard.md +41 -0
  38. package/src/config.mjs +11 -3
  39. package/src/player/bundle/player.js +5 -24
  40. package/src/player/components/EditControls.tsx +1 -1
  41. package/src/player/components/SceneThumbnails.tsx +6 -33
  42. package/src/player/server.mjs +217 -129
  43. /package/{docs → skills/markcut/docs}/edit-mode.md +0 -0
  44. /package/{docs → skills/markcut/docs}/json-descriptive.md +0 -0
  45. /package/{docs → skills/markcut/docs}/label-mode.md +0 -0
  46. /package/{docs → skills/markcut/docs}/markdown-descriptive.md +0 -0
  47. /package/{templates → skills/markcut/docs/templates}/courseware/agents/reviewer.md +0 -0
  48. /package/{templates → skills/markcut/docs/templates}/courseware/prompts/fix.md +0 -0
@@ -17,7 +17,7 @@ import { fileURLToPath } from "node:url";
17
17
  import { isDescriptiveRoot, resolveAndCompile, resolveAndCompileMarkdown, parseImportsBlock, extractDependencySpecs } from "./pipeline.mjs";
18
18
  import { bundleFromEntries } from "./bundler.mjs";
19
19
  import { extractScenes, MIME, serveFile, handleShutdown } from "./server-shared.mjs";
20
- import { DEFAULT_AGENT_CLI } from "../config.mjs";
20
+ import { DEFAULT_EDIT_CLI } from "../config.mjs";
21
21
 
22
22
 
23
23
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -78,6 +78,180 @@ let shutdownTimer = null;
78
78
  // ─── Label store for label mode ───────────────────────────────────────────
79
79
  let labels = [];
80
80
 
81
+ // ─── Agent process (rpc mode) for --edit mode ─────────────────────────────
82
+ // A persistent `pi --mode rpc` subprocess: ONE cold start, MANY edit turns.
83
+ // The agent keeps conversation memory across edits (fast, no re-spawn).
84
+ //
85
+ // JSON-lines protocol over stdio:
86
+ // request: {"id":N,"type":"prompt","message":"..."}
87
+ // response: {"id":N,"type":"response","command":"prompt","success":true} (accepted)
88
+ // events: streamed; a turn ENDS at {"type":"agent_settled"}
89
+ // We collect assistant text from {"type":"message_end", message:{role:"assistant"}}
90
+ // events and resolve the pending /api/edit call on agent_settled.
91
+ let agentProcess = null;
92
+ let agentBusy = false;
93
+ let agentLineBuf = ""; // partial stdout line buffer
94
+ let agentTurnText = ""; // accumulated assistant text for the current turn
95
+ let agentTurnTimer = null;
96
+ /** Resolve/reject for the pending edit API call */
97
+ let pendingEditResolver = null;
98
+ const AGENT_TURN_TIMEOUT = 300000; // 5min safety net per turn
99
+
100
+ /** Pull text out of an assistant message_end event's content blocks. */
101
+ function extractAssistantText(message) {
102
+ if (!message || message.role !== "assistant") return "";
103
+ const content = Array.isArray(message.content) ? message.content : [];
104
+ return content
105
+ .filter((b) => b && b.type === "text" && typeof b.text === "string")
106
+ .map((b) => b.text)
107
+ .join("");
108
+ }
109
+
110
+ /** Resolve the pending edit call and reset per-turn state. */
111
+ function finishAgentTurn(result) {
112
+ if (agentTurnTimer) { clearTimeout(agentTurnTimer); agentTurnTimer = null; }
113
+ agentBusy = false;
114
+ const turnText = agentTurnText;
115
+ agentTurnText = "";
116
+ const resolver = pendingEditResolver;
117
+ pendingEditResolver = null;
118
+ if (resolver) resolver({ ...result, output: result.output != null ? result.output : turnText });
119
+ }
120
+
121
+ /** Dispatch one parsed JSON line emitted on the agent's stdout. */
122
+ function handleAgentLine(obj) {
123
+ if (!obj || typeof obj !== "object") return;
124
+ const t = obj.type;
125
+ if (t === "message_end") {
126
+ const txt = extractAssistantText(obj.message);
127
+ if (txt) agentTurnText += (agentTurnText ? "\n" : "") + txt;
128
+ return;
129
+ }
130
+ if (t === "agent_settled" && pendingEditResolver) {
131
+ finishAgentTurn({ ok: true });
132
+ return;
133
+ }
134
+ if (t === "response" && obj.command === "prompt" && pendingEditResolver) {
135
+ // Preflight result: success means "accepted, events incoming"; failure aborts.
136
+ if (obj.success === false) finishAgentTurn({ ok: false, error: obj.error || "prompt rejected" });
137
+ return;
138
+ }
139
+ if (t === "extension_error" && obj.error) {
140
+ console.warn(` ⚠ agent extension error: ${String(obj.error).substring(0, 120)}`);
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Start the persistent agent subprocess in rpc mode.
146
+ * The system prompt is fixed for the server lifetime (it only references the
147
+ * constant file path); per-edit context rides on each prompt's message.
148
+ */
149
+ function startAgentProcess() {
150
+ if (!MODE_EDIT) return;
151
+
152
+ const agentCli = process.env.MARKCUT_EDIT_CLI || DEFAULT_EDIT_CLI;
153
+
154
+ const fileLabel = VIDEO_JSON.split("/").pop();
155
+ const sessionId = VIDEO_JSON.replace(/[^a-zA-Z0-9\-_.]/g, "_").replace(/^[^a-zA-Z0-9]+/, "").replace(/[^a-zA-Z0-9]+$/, "");
156
+ const promptTemplate = readFileSync(join(ROOT, "docs", "system-prompt-edit.md"), "utf-8");
157
+ const systemPrompt = promptTemplate
158
+ .replace(/@\{([^}]+)\}/g, (_, refPath) => {
159
+ try { return readFileSync(resolve(ROOT, refPath.trim()), "utf-8"); }
160
+ catch { console.warn(` ⚠ could not read @{${refPath}}`); return `[Missing: ${refPath}]`; }
161
+ })
162
+ .replace(/\$\{fileName\}/g, fileLabel)
163
+ .replace(/\$\{filePath\}/g, VIDEO_JSON);
164
+
165
+ // Build the CLI command — strip -p/--prompt (rpc takes prompts via stdin) and
166
+ // force --mode rpc. e.g.
167
+ // "npx pi --session-id {sessionid} --system-prompt {systemprompt} -p {prompt}"
168
+ // → "npx pi --session-id SID --system-prompt SP --mode rpc"
169
+ const parts = agentCli.split(/\s+/).filter(Boolean);
170
+ const resolvedParts = [];
171
+ for (let i = 0; i < parts.length; i++) {
172
+ const p = parts[i];
173
+ if (p === "-p" || p === "--prompt") { i++; continue; } // drop prompt flag + value
174
+ const resolved = p
175
+ .replace(/\{sessionid\}/g, sessionId)
176
+ .replace(/\{systemprompt\}/g, systemPrompt)
177
+ .replace(/\{prompt\}/g, "");
178
+ if (resolved) resolvedParts.push(resolved);
179
+ }
180
+ if (!resolvedParts.includes("--mode")) resolvedParts.push("--mode", "rpc");
181
+ const cmd = resolvedParts[0];
182
+ const cmdArgs = resolvedParts.slice(1);
183
+
184
+ console.log(` 🎬 starting persistent agent (rpc): ${cmd} ${cmdArgs.slice(0, -1).join(" ")} --system-prompt <${systemPrompt.length} chars>`);
185
+
186
+ const child = spawn(cmd, cmdArgs, { cwd: ROOT, stdio: ["pipe", "pipe", "pipe"] });
187
+ agentProcess = child;
188
+ agentLineBuf = "";
189
+
190
+ // stdout is pure JSON-lines in rpc mode (it takes over stdout). Parse line by line.
191
+ child.stdout.on("data", (chunk) => {
192
+ agentLineBuf += chunk.toString();
193
+ let nl;
194
+ while ((nl = agentLineBuf.indexOf("\n")) >= 0) {
195
+ const line = agentLineBuf.slice(0, nl).trim();
196
+ agentLineBuf = agentLineBuf.slice(nl + 1);
197
+ if (!line) continue;
198
+ let obj;
199
+ try { obj = JSON.parse(line); } catch { continue; } // not JSON — ignore
200
+ handleAgentLine(obj);
201
+ }
202
+ });
203
+
204
+ child.stderr.on("data", (chunk) => { process.stderr.write(chunk); });
205
+
206
+ child.on("exit", (code) => {
207
+ console.log(` ⚫ agent process exited (code ${code})`);
208
+ agentProcess = null;
209
+ if (pendingEditResolver) finishAgentTurn({ ok: false, error: `agent exited (code ${code})` });
210
+ agentBusy = false;
211
+ // Keep the session warm: restart on any exit while in edit mode.
212
+ // The --session-id resumes conversation history from disk.
213
+ if (MODE_EDIT) {
214
+ console.log(` 🔄 restarting agent in 1s...`);
215
+ setTimeout(() => startAgentProcess(), 1000);
216
+ }
217
+ });
218
+ }
219
+
220
+ /**
221
+ * Send an edit prompt to the persistent rpc agent and resolve when the turn
222
+ * settles. Completion is signalled authoritatively by the `agent_settled`
223
+ * event — no idle-timeout guessing.
224
+ * @param {string} prompt
225
+ * @returns {Promise<{ok: boolean, output: string, error?: string}>}
226
+ */
227
+ function sendToAgent(prompt) {
228
+ return new Promise((resolve) => {
229
+ if (!agentProcess || agentProcess.killed) {
230
+ resolve({ ok: false, output: "", error: "agent process not running" });
231
+ return;
232
+ }
233
+ if (agentBusy) {
234
+ resolve({ ok: false, output: "", error: "agent is busy" });
235
+ return;
236
+ }
237
+
238
+ agentBusy = true;
239
+ agentTurnText = "";
240
+ pendingEditResolver = resolve;
241
+
242
+ // Safety net — the agent_settled event is the real completion signal.
243
+ agentTurnTimer = setTimeout(() => {
244
+ console.warn(` ⏰ agent turn timed out after ${AGENT_TURN_TIMEOUT / 1000}s`);
245
+ finishAgentTurn({ ok: false, output: agentTurnText + "\n[TIMEOUT]", error: "agent turn timed out" });
246
+ }, AGENT_TURN_TIMEOUT);
247
+
248
+ const id = Date.now();
249
+ const req = JSON.stringify({ id, type: "prompt", message: prompt }) + "\n";
250
+ console.log(` 📤 rpc prompt (id=${id}): ${prompt.substring(0, 80).replace(/\n/g, " ")}`);
251
+ agentProcess.stdin.write(req);
252
+ });
253
+ }
254
+
81
255
  // ─── Edit history for context ─────────────────────────────────────────────
82
256
  let editHistory = [];
83
257
 
@@ -618,13 +792,9 @@ function getHtml(variantLabel) {
618
792
  #scene-thumbnails { display: flex; gap: 6px; width: 100%; max-width: 500px; padding: 4px 12px; flex-shrink: 0; overflow-x: auto; scrollbar-width: thin; }
619
793
  #scene-thumbnails::-webkit-scrollbar { height: 4px; }
620
794
  #scene-thumbnails::-webkit-scrollbar-thumb { background: rgba(255,255,255,.15); border-radius: 2px; }
621
- .sthumb-item { flex-shrink: 0; width: 72px; cursor: pointer; border-radius: 6px; overflow: hidden; border: 2px solid transparent; transition: all .15s; background: rgba(255,255,255,.05); display: flex; flex-direction: column; }
622
- .sthumb-item:hover { border-color: rgba(74,158,255,.4); }
623
- .sthumb-item.active { border-color: #4a9eff; box-shadow: 0 0 8px rgba(74,158,255,.3); }
624
- .sthumb-media { width: 100%; height: 48px; overflow: hidden; position: relative; }
625
- .sthumb-media img, .sthumb-media video { width: 100%; height: 100%; object-fit: cover; }
626
- .sthumb-fallback { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; background: rgba(255,255,255,.08); color: rgba(255,255,255,.3); font-size: 16px; font-weight: 600; }
627
- .sthumb-name { font-size: 9px; color: rgba(255,255,255,.45); text-align: center; padding: 2px 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
795
+ .scene-pill { flex-shrink: 0; padding: 4px 12px; border-radius: 12px; cursor: pointer; border: 1px solid transparent; transition: all .15s; background: rgba(255,255,255,.06); color: rgba(255,255,255,.45); font-size: 11px; white-space: nowrap; }
796
+ .scene-pill:hover { border-color: rgba(74,158,255,.4); color: rgba(255,255,255,.7); }
797
+ .scene-pill.active { background: rgba(74,158,255,.2); border-color: #4a9eff; color: #4a9eff; }
628
798
  </style>
629
799
  </head>
630
800
  <body>
@@ -759,145 +929,58 @@ const server = createServer(async (req, res) => {
759
929
  return;
760
930
  }
761
931
 
762
- // API: Edit — call configured agent CLI to edit the .md source file
763
- // Only supports markdown-descriptive (.md) files — the primary authoring format
764
- // Uses DEFAULT_AGENT_CLI with {sessionid} replaced by sanitized file path for continuity
765
- // System prompt: role "video director" + markcut skill + @{docs/markdown-descriptive.md}
766
- // User prompt: current timestamp + active scene + edit request
932
+ // API: Edit — send prompt to the persistent rpc agent (startAgentProcess)
767
933
  if (path === "/api/edit" && req.method === "POST") {
768
934
  let body = "";
769
935
  req.on("data", c => body += c);
770
- req.on("end", () => {
936
+ req.on("end", async () => {
771
937
  try {
772
938
  const { text, currentTime, activeScene } = JSON.parse(body);
773
939
  if (!text) { res.writeHead(400); res.end(JSON.stringify({ error: "empty text" })); return; }
774
940
 
775
941
  editHistory.push(text);
776
942
 
777
- // Read the current file content for the agent to edit
778
- const rawContent = readFileSync(VIDEO_JSON, "utf-8");
779
- const fileLabel = VIDEO_JSON.split("/").pop();
780
- // Sanitize file path to a valid session ID (alphanumeric, -, _, .)
781
- const sessionId = VIDEO_JSON.replace(/[^a-zA-Z0-9\-_.]/g, "_").replace(/^[^a-zA-Z0-9]+/, "").replace(/[^a-zA-Z0-9]+$/, "");
782
-
783
- // Build tree structure description for context
784
- let treeInfo = "";
785
- try {
786
- const parsed = JSON.parse(rawContent);
787
- const root = parsed.root || parsed;
788
-
789
- function describeNode(node, depth) {
790
- const indent = " ".repeat(depth);
791
- const id = node.id || "";
792
- const name = node.name || "";
793
- const label = name || id;
794
- const type = node.type || "unknown";
795
- const dur = node.durationInSeconds !== undefined ? `, ${node.durationInSeconds}s` : "";
796
- let line = `${indent}${type} "${label}"`;
797
- if (type === "root") line += ` (${node.width}x${node.height}, ${node.fps}fps${node.isSeries ? ", series" : ""}${node.transition ? `, transition:${node.transition}` : ""}${node.theme ? `, theme:${node.theme}` : ""})`;
798
- else if (type === "folder") line += ` (${node.isSeries ? "series" : "parallel"}${node.transition ? `, transition:${node.transition}` : ""}${dur})`;
799
- else if (type === "component") {
800
- const props = node.props ? JSON.stringify(Object.fromEntries(Object.entries(node.props).filter(([k]) => !k.startsWith("_")))) : "{}";
801
- line += ` ${node.componentName}(${props.slice(0, 100)})${dur}`;
802
- }
803
- else if (type === "subtitle") { const txt = (node.src || "").slice(0, 60); line += ` "${txt}"${dur}`; }
804
- else if (type === "video" || type === "audio" || type === "image") { const src = (node.src || "").slice(0, 50); line += ` "${src}"${dur}`; }
805
- else if (type === "effect") line += ` animation:${node.animation || "custom"}${dur}`;
806
- else if (type === "rhythm") line += ` src:"${(node.src || "").slice(0, 40)}"${dur}`;
807
- else if (type === "map") line += ` waypoints:${(node.waypoints || []).length}${dur}`;
808
- else if (type === "include") line += ` src:"${(node.src || "").slice(0, 50)}"${dur}`;
809
- if (typeof node.start === "number" || typeof node.end === "number") {
810
- const sStart = node.start ?? 0; const sEnd = node.end ?? sStart;
811
- line += ` [${sStart}→${sEnd}s${node.isBackground ? ", bg" : ""}${node.volume !== undefined ? `, vol:${node.volume}` : ""}${node.loop ? `, loop:${node.loop}` : ""}]`;
812
- } else if (node.isBackground) line += ` [bg]`;
813
- const extras = [];
814
- if (node.componentName) extras.push(node.componentName);
815
- if (node.fit) extras.push(`fit:${node.fit}`);
816
- if (node.fontSize) extras.push(`fontSize:${node.fontSize}`);
817
- if (node.volume !== undefined && type !== "root") extras.push(`vol:${node.volume}`);
818
- if (node.playbackRate) extras.push(`rate:${node.playbackRate}`);
819
- if (node.transitionTime !== undefined) extras.push(`transTime:${node.transitionTime}`);
820
- if (node.visible === false) extras.push("hidden");
821
- if (extras.length > 0) line += ` {${extras.join(", ")}}`;
822
- return line;
823
- }
824
- function walkTree(node, depth = 0) {
825
- const lines = [describeNode(node, depth)];
826
- if (node.children && node.children.length > 0) {
827
- for (const child of node.children.filter(c => c.visible !== false || c.visible === undefined)) lines.push(...walkTree(child, depth + 1));
828
- }
829
- return lines;
830
- }
831
- treeInfo = walkTree(root).join("\n");
832
- } catch {}
833
-
834
- const historyStr = editHistory.length > 1
835
- ? "Previous edits (in order):\n" + editHistory.slice(0, -1).map((e, i) => `${i+1}. ${e}`).join("\n") + "\n"
836
- : "";
837
-
838
- // Build system prompt from template file (resolves @{path} references)
839
- // System prompt is stable — it contains the role, skills, and format docs.
840
- // Tree info and history change per-edit, so they go in the user prompt.
841
- const promptTemplate = readFileSync(join(ROOT, "docs", "system-prompt-edit.md"), "utf-8");
842
- const systemPrompt = promptTemplate
843
- .replace(/@\{([^}]+)\}/g, (_, refPath) => {
844
- try {
845
- return readFileSync(resolve(ROOT, refPath.trim()), "utf-8");
846
- } catch {
847
- console.warn(` ⚠ could not read @{${refPath}}`);
848
- return `[Missing: ${refPath}]`;
849
- }
850
- })
851
- .replace(/\$\{fileName\}/g, fileLabel)
852
- .replace(/\$\{filePath\}/g, VIDEO_JSON);
853
-
854
- // Build user prompt with current context (changes every edit)
943
+ // Build user prompt with context (system prompt is fixed at agent startup)
855
944
  const timeStr = currentTime !== undefined ? `Current playback time: ${Number(currentTime).toFixed(1)}s` : "";
856
945
  const sceneStr = activeScene ? `Active scene: ${activeScene}` : "";
857
946
  const contextBlock = [timeStr, sceneStr].filter(Boolean).join("\n");
858
-
859
- const userPrompt = `--- Current stream tree (for reference) ---
860
- ${treeInfo || "(compiled tree)"}
947
+ const historyStr = editHistory.length > 1
948
+ ? "Previous edits (in order):\n" + editHistory.slice(0, -1).map((e, i) => `${i+1}. ${e}`).join("\n") + "\n"
949
+ : "";
950
+ const userPrompt = `${contextBlock}
861
951
  ${historyStr}
862
- ${contextBlock}
863
952
  Edit request: ${text}`;
864
953
 
865
- // Resolve agent CLI template with all placeholders
866
- // Each placeholder is its own argument in the template (no shell quotes needed)
867
- const agentCli = process.env.MARKCUT_AGENT_CLI || DEFAULT_AGENT_CLI;
868
- const parts = agentCli.split(/\s+/);
869
- const resolvedParts = parts.map(p =>
870
- p.replace(/\{sessionid\}/g, sessionId)
871
- .replace(/\{systemprompt\}/g, systemPrompt)
872
- .replace(/\{prompt\}/g, userPrompt)
873
- );
874
- const cmd = resolvedParts[0];
875
- const cmdArgs = resolvedParts.slice(1);
876
-
877
954
  console.log(` 🤖 edit: ${text} (${currentTime !== undefined ? currentTime.toFixed(1) + "s" : ""} ${activeScene || ""})`);
878
- console.log(` 🎬 session: ${sessionId}`);
879
- console.log(` 📋 cmd: ${agentCli.split(/\s+/)[0]} ${agentCli.split(/\s+/).slice(1).join(" ").replace(/\{prompt\}/g, "...").substring(0, 120)}`);
880
-
881
- const child = spawn(cmd, cmdArgs, {
882
- cwd: ROOT,
883
- stdio: ["ignore", "pipe", "pipe"],
884
- });
885
-
886
- let output = "";
887
- child.stdout.on("data", d => output += d);
888
- child.stderr.on("data", d => output += d);
889
-
890
- child.on("exit", (code) => {
891
- if (code === 0) {
892
- console.log(` ✅ edit complete`);
893
- res.writeHead(200, { "Content-Type": "application/json" });
894
- res.end(JSON.stringify({ done: true, output: output.trim() }));
955
+
956
+ // Send to the persistent rpc agent (one cold start; conversation kept in memory)
957
+ const result = await sendToAgent(userPrompt);
958
+
959
+ if (result.ok) {
960
+ // Parse JSON summary from the agent's final assistant text
961
+ let summary = "";
962
+ let fileChanged = false;
963
+ const raw = (result.output || "").trim();
964
+ const jsonMatch = raw.match(/\{[\s\S]*"summary"[\s\S]*\}/);
965
+ if (jsonMatch) {
966
+ try {
967
+ const parsed = JSON.parse(jsonMatch[0]);
968
+ summary = parsed.summary || raw.substring(0, 120);
969
+ fileChanged = parsed.fileChanged === true;
970
+ } catch { summary = raw.substring(0, 120); }
895
971
  } else {
896
- console.error(` ❌ edit failed (exit ${code}): ${output.trim()}`);
897
- res.writeHead(500, { "Content-Type": "application/json" });
898
- res.end(JSON.stringify({ error: `agent exited with code ${code}`, output: output.trim() }));
972
+ summary = raw.substring(0, 120) || "done";
899
973
  }
900
- });
974
+ console.log(` ✅ ${fileChanged ? "edited" : "no change"}: ${summary}`);
975
+ console.log(` 📤 response summary="${summary}" fileChanged=${fileChanged}`);
976
+ res.writeHead(200, { "Content-Type": "application/json" });
977
+ res.end(JSON.stringify({ done: true, summary, fileChanged, output: "" }));
978
+ } else {
979
+ const msg = result.error || "failed";
980
+ console.error(` ❌ edit ${msg}: ${(result.output || "").trim().substring(0, 100)}`);
981
+ res.writeHead(500, { "Content-Type": "application/json" });
982
+ res.end(JSON.stringify({ error: msg, output: (result.output || "").trim().substring(0, 200) }));
983
+ }
901
984
  } catch (e) {
902
985
  res.writeHead(500, { "Content-Type": "application/json" });
903
986
  res.end(JSON.stringify({ error: e.message }));
@@ -1000,6 +1083,11 @@ server.listen(PORT, async () => {
1000
1083
  // Pipeline failed — still announce ready so the player can show an error state
1001
1084
  }
1002
1085
 
1086
+ // Start persistent agent process for edit mode (after initial compilation)
1087
+ if (MODE_EDIT) {
1088
+ startAgentProcess();
1089
+ }
1090
+
1003
1091
  const mode = MODE_LABEL ? " --label" : MODE_EDIT ? " --edit" : "";
1004
1092
  console.log(`\n🎬 Player ready at http://localhost:${PORT}${mode}`);
1005
1093
  if (VARIANT_CONFIGS.length > 1) {
File without changes
File without changes