@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.
- package/AGENTS.md +1 -0
- package/package.json +1 -1
- package/{SKILL.md → skills/markcut/SKILL.md} +8 -2
- package/{docs → skills/markcut/docs}/system-prompt-edit.md +5 -2
- package/skills/markcut/docs/template.md +213 -0
- package/{templates → skills/markcut/docs/templates}/courseware/TEMPLATE.md +220 -10
- package/{templates → skills/markcut/docs/templates}/courseware/prompts/outline.md +5 -0
- package/{templates → skills/markcut/docs/templates}/courseware/prompts/scene.md +6 -0
- package/skills/markcut/docs/templates/deep-dive/TEMPLATE.md +355 -0
- package/skills/markcut/docs/templates/deep-dive/agents/researcher.md +69 -0
- package/skills/markcut/docs/templates/deep-dive/agents/reviewer.md +108 -0
- package/skills/markcut/docs/templates/deep-dive/prompts/fix.md +28 -0
- package/skills/markcut/docs/templates/deep-dive/prompts/outline.md +102 -0
- package/skills/markcut/docs/templates/deep-dive/prompts/script.md +64 -0
- package/skills/markcut/docs/templates/illustrated-book/TEMPLATE.md +360 -0
- package/skills/markcut/docs/templates/illustrated-book/agents/reviewer.md +115 -0
- package/skills/markcut/docs/templates/illustrated-book/prompts/fix.md +29 -0
- package/skills/markcut/docs/templates/illustrated-book/prompts/illustration.md +35 -0
- package/skills/markcut/docs/templates/illustrated-book/prompts/story.md +69 -0
- package/skills/markcut/docs/templates/short-film/TEMPLATE.md +387 -0
- package/skills/markcut/docs/templates/short-film/agents/reviewer.md +115 -0
- package/skills/markcut/docs/templates/short-film/prompts/fix.md +29 -0
- package/skills/markcut/docs/templates/short-film/prompts/screenplay.md +101 -0
- package/skills/markcut/docs/templates/short-film/prompts/storyboard.md +54 -0
- package/skills/markcut/docs/templates/short-video/TEMPLATE.md +302 -0
- package/skills/markcut/docs/templates/short-video/agents/reviewer.md +94 -0
- package/skills/markcut/docs/templates/short-video/prompts/fix.md +27 -0
- package/skills/markcut/docs/templates/short-video/prompts/script.md +65 -0
- package/skills/markcut/docs/templates/vlog/TEMPLATE.md +405 -0
- package/skills/markcut/docs/templates/vlog/agents/reviewer.md +89 -0
- package/skills/markcut/docs/templates/vlog/agents/story-writer.md +100 -0
- package/skills/markcut/docs/templates/vlog/prompts/bgm-select.md +38 -0
- package/skills/markcut/docs/templates/vlog/prompts/group-clips.md +93 -0
- package/skills/markcut/docs/templates/vlog/prompts/local-context.md +59 -0
- package/skills/markcut/docs/templates/vlog/prompts/outline.md +56 -0
- package/skills/markcut/docs/templates/vlog/prompts/review-fix.md +27 -0
- package/skills/markcut/docs/templates/vlog/prompts/storyboard.md +41 -0
- package/src/config.mjs +11 -3
- package/src/player/bundle/player.js +5 -24
- package/src/player/components/EditControls.tsx +1 -1
- package/src/player/components/SceneThumbnails.tsx +6 -33
- package/src/player/server.mjs +217 -129
- /package/{docs → skills/markcut/docs}/edit-mode.md +0 -0
- /package/{docs → skills/markcut/docs}/json-descriptive.md +0 -0
- /package/{docs → skills/markcut/docs}/label-mode.md +0 -0
- /package/{docs → skills/markcut/docs}/markdown-descriptive.md +0 -0
- /package/{templates → skills/markcut/docs/templates}/courseware/agents/reviewer.md +0 -0
- /package/{templates → skills/markcut/docs/templates}/courseware/prompts/fix.md +0 -0
package/src/player/server.mjs
CHANGED
|
@@ -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 {
|
|
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
|
-
.
|
|
622
|
-
.
|
|
623
|
-
.
|
|
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 —
|
|
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
|
-
//
|
|
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
|
-
|
|
860
|
-
|
|
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
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
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
|
-
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|