@popmelt.com/core 0.3.0 → 0.4.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/dist/server.mjs CHANGED
@@ -1,1814 +1,101 @@
1
- var __defProp = Object.defineProperty;
2
- var __defProps = Object.defineProperties;
3
- var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
4
- var __getOwnPropSymbols = Object.getOwnPropertySymbols;
5
- var __hasOwnProp = Object.prototype.hasOwnProperty;
6
- var __propIsEnum = Object.prototype.propertyIsEnumerable;
7
- var __knownSymbol = (name, symbol) => (symbol = Symbol[name]) ? symbol : /* @__PURE__ */ Symbol.for("Symbol." + name);
8
- var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
9
- var __spreadValues = (a, b) => {
10
- for (var prop in b || (b = {}))
11
- if (__hasOwnProp.call(b, prop))
12
- __defNormalProp(a, prop, b[prop]);
13
- if (__getOwnPropSymbols)
14
- for (var prop of __getOwnPropSymbols(b)) {
15
- if (__propIsEnum.call(b, prop))
16
- __defNormalProp(a, prop, b[prop]);
17
- }
18
- return a;
19
- };
20
- var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
21
- var __objRest = (source, exclude) => {
22
- var target = {};
23
- for (var prop in source)
24
- if (__hasOwnProp.call(source, prop) && exclude.indexOf(prop) < 0)
25
- target[prop] = source[prop];
26
- if (source != null && __getOwnPropSymbols)
27
- for (var prop of __getOwnPropSymbols(source)) {
28
- if (exclude.indexOf(prop) < 0 && __propIsEnum.call(source, prop))
29
- target[prop] = source[prop];
30
- }
31
- return target;
32
- };
33
- var __forAwait = (obj, it, method) => (it = obj[__knownSymbol("asyncIterator")]) ? it.call(obj) : (obj = obj[__knownSymbol("iterator")](), it = {}, method = (key, fn) => (fn = obj[key]) && (it[key] = (arg) => new Promise((yes, no, done) => (arg = fn.call(obj, arg), done = arg.done, Promise.resolve(arg.value).then((value) => yes({ value, done }), no)))), method("next"), method("return"), it);
1
+ var Et=Object.defineProperty,Dt=Object.defineProperties;var At=Object.getOwnPropertyDescriptors;var Te=Object.getOwnPropertySymbols;var Xe=Object.prototype.hasOwnProperty,Ze=Object.prototype.propertyIsEnumerable;var Ve=(r,t)=>(t=Symbol[r])?t:Symbol.for("Symbol."+r);var Ke=(r,t,n)=>t in r?Et(r,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):r[t]=n,Z=(r,t)=>{for(var n in t||(t={}))Xe.call(t,n)&&Ke(r,n,t[n]);if(Te)for(var n of Te(t))Ze.call(t,n)&&Ke(r,n,t[n]);return r},ae=(r,t)=>Dt(r,At(t));var et=(r,t)=>{var n={};for(var e in r)Xe.call(r,e)&&t.indexOf(e)<0&&(n[e]=r[e]);if(r!=null&&Te)for(var e of Te(r))t.indexOf(e)<0&&Ze.call(r,e)&&(n[e]=r[e]);return n};var ce=(r,t,n)=>(t=r[Ve("asyncIterator")])?t.call(r):(r=r[Ve("iterator")](),t={},n=(e,s)=>(s=r[e])&&(t[e]=a=>new Promise((i,c,p)=>(a=s.call(r,a),p=a.done,Promise.resolve(a.value).then(u=>i({value:u,done:p}),c)))),n("next"),n("return"),t);import{execFileSync as rn}from"child_process";import{randomUUID as pe}from"crypto";import{mkdir as an,readdir as cn,stat as ln,unlink as dn,writeFile as ge}from"fs/promises";import{createServer as un}from"http";import{tmpdir as pn}from"os";import{join as le}from"path";import{spawn as Jt}from"child_process";import{createInterface as Nt}from"readline";function Re(r,t){let{prompt:n,projectRoot:e,maxTurns:s=10,maxBudgetUsd:a=1,allowedTools:i=["Read","Edit","Write","Glob","Grep","Bash"],claudePath:c="claude",resumeSessionId:p,model:u,onEvent:v}=t,G=[];p?G.push("--resume",p,"-p",n):G.push("-p",n),G.push("--output-format","stream-json","--verbose","--max-turns",String(s),"--max-budget-usd",String(a)),u&&G.push("--model",u);for(let S of i)G.push("--allowedTools",S);let y=Jt(c,G,{cwd:e,stdio:["ignore","pipe","pipe"],env:ae(Z({},process.env),{ANTHROPIC_API_KEY:void 0})}),T=new Promise(S=>{var A;let F,b=[],z=[],V=!1,ee="",q=Nt({input:y.stdout}),B=new Set;q.on("line",j=>{var P,Y,H,X,xe,ke,Ie,be,Pe;if(j.trim())try{let Q=JSON.parse(j);Q.session_id&&!F&&(F=Q.session_id);let De=(Y=Q.type)!=null?Y:(P=Q.event)!=null&&P.type?`event.${Q.event.type}`:"unknown";if(B.add(De),Q.type==="result"&&Q.result&&b.length===0){let D=typeof Q.result=="string"?Q.result:"";D&&(b.push(D),v==null||v({type:"delta",jobId:r,text:D},r))}if(Q.type==="assistant"&&Array.isArray((H=Q.message)==null?void 0:H.content))for(let D of Q.message.content){if(D.type==="text"&&D.text&&(b.push(D.text),v==null||v({type:"delta",jobId:r,text:D.text},r)),D.type==="tool_use"&&D.name){let ye=((X=D.input)==null?void 0:X.file_path)||((xe=D.input)==null?void 0:xe.path)||void 0;v==null||v(Z({type:"tool_use",jobId:r,tool:D.name},ye?{file:ye}:{}),r),D.name==="Edit"&&((ke=D.input)!=null&&ke.file_path)?z.push({tool:"Edit",file_path:D.input.file_path,old_string:D.input.old_string,new_string:D.input.new_string,replace_all:D.input.replace_all}):D.name==="Write"&&((Ie=D.input)!=null&&Ie.file_path)&&z.push({tool:"Write",file_path:D.input.file_path,content:D.input.content})}D.type==="thinking"&&D.thinking&&(v==null||v({type:"thinking",jobId:r,text:D.thinking},r))}Q.type==="user"&&((Pe=(be=Q.tool_use_result)==null?void 0:be.file)!=null&&Pe.filePath)&&(v==null||v({type:"tool_use",jobId:r,tool:"Read",file:Q.tool_use_result.file.filePath},r))}catch(Q){}});let te=[];(A=y.stderr)==null||A.on("data",j=>{te.push(j.toString())}),y.on("close",j=>{q.close(),j!==0&&j!==null&&(V=!0,ee=te.join("")||`Claude process exited with code ${j}`),S({sessionId:F,text:b.join(""),success:!V,error:V?ee:void 0,fileEdits:z.length>0?z:void 0})}),y.on("error",j=>{V=!0,ee=j.message,S({sessionId:F,text:b.join(""),success:!1,error:ee,fileEdits:z.length>0?z:void 0})})});return{process:y,result:T}}import{spawn as _t}from"child_process";import{createInterface as Bt}from"readline";function tt(r,t){let{prompt:n,projectRoot:e,screenshotPath:s,resumeSessionId:a,model:i,onEvent:c}=t,p=[];a?(p.push("exec","resume",a),i&&p.push("-m",i),p.push("--json","--full-auto",n),s&&p.push("--image",s)):(p.push("exec","--json","--full-auto"),i&&p.push("-m",i),p.push(n),s&&p.push("--image",s));let u=_t("codex",p,{cwd:e,stdio:["ignore","pipe","pipe"],env:Z({},process.env)}),v=new Promise(G=>{var ee;let y,T=[],S=!1,F="",b=Bt({input:u.stdout}),z=new Set;b.on("line",q=>{var B,te,A,j;if(q.trim())try{let P=JSON.parse(q),Y=(B=P.type)!=null?B:"unknown";if(z.add(Y),Y==="thread.started"&&P.thread_id&&!y&&(y=P.thread_id),Y==="item/agentMessage/delta"&&((te=P.delta)!=null&&te.text)&&(T.push(P.delta.text),c==null||c({type:"delta",jobId:r,text:P.delta.text},r)),Y==="item/reasoning/delta"&&((A=P.delta)!=null&&A.text)&&(c==null||c({type:"thinking",jobId:r,text:P.delta.text},r)),Y==="item/started"&&P.item){let H=P.item.type;if(H==="command_execution")c==null||c({type:"tool_use",jobId:r,tool:"Bash"},r);else if(H==="file_change"){let X=P.item.filename||P.item.path;c==null||c(Z({type:"tool_use",jobId:r,tool:"Edit"},X?{file:X}:{}),r)}else if(H==="file_read"){let X=P.item.filename||P.item.path;c==null||c(Z({type:"tool_use",jobId:r,tool:"Read"},X?{file:X}:{}),r)}else if(H==="web_search")c==null||c({type:"tool_use",jobId:r,tool:"WebSearch"},r);else if(H==="mcp_tool_call"){let X=P.item.tool_name||P.item.name||"MCP";c==null||c({type:"tool_use",jobId:r,tool:X},r)}}if(Y==="item/completed"&&P.item){if(P.item.type==="agent_message"){let H=P.item.text;typeof H=="string"&&H&&T.push(H)}else if(P.item.type==="reasoning"){let H=P.item.text;typeof H=="string"&&H&&(c==null||c({type:"thinking",jobId:r,text:H},r))}}Y==="turn.failed"&&(S=!0,F=((j=P.error)==null?void 0:j.message)||P.message||"Turn failed")}catch(P){}});let V=[];(ee=u.stderr)==null||ee.on("data",q=>{V.push(q.toString())}),u.on("close",q=>{b.close(),q!==0&&q!==null&&(S=!0,F=V.join("")||`Codex process exited with code ${q}`),G({sessionId:y,text:T.join(""),success:!S,error:S?F:void 0})}),u.on("error",q=>{S=!0,F=q.message,G({sessionId:y,text:T.join(""),success:!1,error:F})})});return{process:u,result:v}}import{execFile as Ut}from"child_process";import{copyFile as nt,mkdir as st,readdir as Ft,readFile as zt,writeFile as jt}from"fs/promises";import{join as ue}from"path";var $e=class{constructor(t){this.projectRoot=t;let n=ue(t,".popmelt");this.decisionsDir=ue(n,"decisions"),this.screenshotsDir=ue(n,"screenshots")}async persist(t,n,e){try{await st(this.decisionsDir,{recursive:!0}),await st(this.screenshotsDir,{recursive:!0});try{await nt(n,ue(this.screenshotsDir,`s-${t.id}.png`))}catch(s){}for(let s=0;s<e.length;s++)try{let a=t.pastedImagePaths[s];a&&await nt(e[s],ue(this.screenshotsDir,a.replace("screenshots/","")))}catch(a){}await jt(ue(this.decisionsDir,`d-${t.id}.json`),JSON.stringify(t,null,2))}catch(s){console.error("[DecisionStore] Failed to persist decision record:",s)}}async listDecisionIds(){try{return(await Ft(this.decisionsDir)).filter(n=>n.startsWith("d-")&&n.endsWith(".json")).map(n=>n.slice(2,-5))}catch(t){return[]}}async loadDecision(t){try{let n=await zt(ue(this.decisionsDir,`d-${t}.json`),"utf-8");return JSON.parse(n)}catch(n){return null}}async loadDecisions(t){return(await Promise.all(t.map(e=>this.loadDecision(e)))).filter(e=>e!==null)}captureGitDiff(t){return new Promise(n=>{Ut("git",["diff","HEAD"],{cwd:t,timeout:5e3,maxBuffer:1024*1024},(e,s)=>{if(e){n(null);return}n(s||null)})})}};import{readFile as ot,writeFile as he}from"fs/promises";import{join as Je}from"path";var oe="[Materializer]",Ht={version:1,materializedIds:[],lastRunAt:null,lastRunDecisionIds:[],lastRunError:null},Me=class{constructor(t,n,e={}){this.projectRoot=t;this.decisionStore=n;this.options=e;this.cachedIndex=null;this.running=!1;let s=Je(t,".popmelt");this.indexPath=Je(s,"materialized.json"),this.modelPath=Je(s,"model.json")}get isRunning(){return this.running}async loadModel(){try{let t=await ot(this.modelPath,"utf-8");return JSON.parse(t)}catch(t){return null}}async addComponent(t){let n=await this.loadModel();n||(n={tokens:{},components:{},rules:[]}),(!n.components||typeof n.components!="object")&&(n.components={});let e=n.components;return e[t]?{added:!1,alreadyExists:!0}:(e[t]={description:""},await he(this.modelPath,JSON.stringify(n,null,2)),console.log(`${oe} Added component "${t}" to model`),{added:!0,alreadyExists:!1})}async updateToken(t,n){let e=await this.loadModel();e||(e={tokens:{},components:{},rules:[]});let s=t.split("."),a=e;for(let p=0;p<s.length-1;p++){let u=s[p];(!a[u]||typeof a[u]!="object")&&(a[u]={}),a=a[u]}let i=s[s.length-1],c;try{c=JSON.parse(n)}catch(p){c=null}if(c&&typeof c=="object"&&c!==null&&"value"in c)a[i]=c;else{let p=a[i];p&&typeof p=="object"&&p!==null&&"value"in p?p.value=n:a[i]=n}return await he(this.modelPath,JSON.stringify(e,null,2)),console.log(`${oe} Updated token "${t}" \u2192 "${n.slice(0,80)}"`),{updated:!0}}async removeToken(t){let n=await this.loadModel();if(!n)return{removed:!1};let e=t.split("."),s=n;for(let i=0;i<e.length-1;i++){let c=e[i];if(!s[c]||typeof s[c]!="object")return{removed:!1};s=s[c]}let a=e[e.length-1];return a in s?(delete s[a],await he(this.modelPath,JSON.stringify(n,null,2)),console.log(`${oe} Removed token "${t}" from model`),{removed:!0}):{removed:!1}}async removeComponent(t){let n=await this.loadModel();if(!n)return{removed:!1};let e=n.components;return!e||!e[t]?{removed:!1}:(delete e[t],await he(this.modelPath,JSON.stringify(n,null,2)),console.log(`${oe} Removed component "${t}" from model`),{removed:!0})}async getUnmaterializedPatternDecisions(){let t=await this.loadIndex(),n=new Set(t.materializedIds),s=(await this.decisionStore.listDecisionIds()).filter(i=>!n.has(i));return s.length===0?[]:(await this.decisionStore.loadDecisions(s)).filter(i=>i.resolutions.some(c=>{var u;let p=(u=c.finalScope)!=null?u:c.inferredScope;return(p==null?void 0:p.breadth)==="pattern"}))}async run(){var t,n,e,s,a,i,c;if(this.running)return{processedIds:[],success:!0,error:"Already running"};this.running=!0;try{let p=await this.getUnmaterializedPatternDecisions();if(p.length===0)return{processedIds:[],success:!0};let u=p.map(b=>b.id);console.log(`${oe} Processing ${u.length} pattern-scoped decision(s): ${u.join(", ")}`),(n=(t=this.options).onEvent)==null||n.call(t,{type:"materialize_started",decisionIds:u});let v=await this.loadModel(),G=Wt(p,v),y=!0,T;try{let{result:b}=Re(`mat-${Date.now()}`,{prompt:G,projectRoot:this.projectRoot,maxTurns:(e=this.options.maxTurns)!=null?e:5,maxBudgetUsd:(s=this.options.maxBudgetUsd)!=null?s:.5,allowedTools:["Read"],claudePath:(a=this.options.claudePath)!=null?a:"claude"}),z=await b;if(!z.success)y=!1,T=z.error,console.error(`${oe} Claude spawn error:`,T);else{let V=Lt(z.text);V?(await he(this.modelPath,JSON.stringify(V,null,2)),console.log(`${oe} Successfully materialized ${u.length} decision(s) \u2192 ${this.modelPath}`)):(y=!1,T="No <model> block found in response",console.error(`${oe} ${T}`))}}catch(b){y=!1,T=b instanceof Error?b.message:String(b),console.error(`${oe} Error:`,T)}let S=await this.loadIndex(),F=new Set(S.materializedIds);for(let b of u)F.add(b);return S.materializedIds=[...F],S.lastRunAt=Date.now(),S.lastRunDecisionIds=u,S.lastRunError=T!=null?T:null,await this.persistIndex(S),(c=(i=this.options).onEvent)==null||c.call(i,{type:"materialize_done",decisionIds:u,success:y,error:T}),{processedIds:u,success:y,error:T}}finally{this.running=!1}}async loadIndex(){if(this.cachedIndex)return this.cachedIndex;try{let t=await ot(this.indexPath,"utf-8"),n=JSON.parse(t);return this.cachedIndex=n,n}catch(t){return this.cachedIndex=ae(Z({},Ht),{materializedIds:[],lastRunDecisionIds:[]}),this.cachedIndex}}async persistIndex(t){this.cachedIndex=t;try{await he(this.indexPath,JSON.stringify(t,null,2))}catch(n){console.error(`${oe} Failed to write index:`,n)}}};function Lt(r){let t=r.match(/<model>\s*([\s\S]*?)\s*<\/model>/);if(!(t!=null&&t[1]))return null;try{let n=JSON.parse(t[1]);return typeof n!="object"||n===null||Array.isArray(n)?null:n}catch(n){return null}}function Wt(r,t){let n=r.map(s=>{let i=s.resolutions.filter(u=>{var G;let v=(G=u.finalScope)!=null?G:u.inferredScope;return(v==null?void 0:v.breadth)==="pattern"}).map(u=>{var T,S,F,b;let v=(T=u.finalScope)!=null?T:u.inferredScope,G=(S=v==null?void 0:v.target)!=null?S:"unknown",y=(b=(F=u.filesModified)==null?void 0:F.join(", "))!=null?b:"none";return`- **${u.summary}** [scope: pattern/${G}]
2
+ Files modified: ${y}`}).join(`
3
+ `),c=s.annotations.map(u=>u.instruction).filter(Boolean).join(`
4
+ `),p=s.gitDiff?`
5
+ \`\`\`diff
6
+ ${s.gitDiff.slice(0,2e3)}
7
+ \`\`\``:"";return`### Decision ${s.id} (${new Date(s.createdAt).toISOString()})
8
+ Page: ${s.url}
9
+ ${i}
10
+ ${p}
11
+ ${c?`
12
+ Original instructions:
13
+ ${c}`:""}`}).join(`
34
14
 
35
- // src/server/bridge-server.ts
36
- import { execFileSync } from "child_process";
37
- import { randomUUID } from "crypto";
38
- import { mkdir as mkdir2, readdir, stat, unlink, writeFile as writeFile2 } from "fs/promises";
39
- import { createServer } from "http";
40
- import { tmpdir } from "os";
41
- import { join as join2 } from "path";
15
+ `);return`You are extracting a local design model from accumulated design decisions.
42
16
 
43
- // src/server/claude-spawner.ts
44
- import { spawn } from "child_process";
45
- import { createInterface } from "readline";
46
- function spawnClaude(jobId, options) {
47
- const {
48
- prompt,
49
- projectRoot,
50
- maxTurns = 10,
51
- maxBudgetUsd = 1,
52
- allowedTools = ["Read", "Edit", "Write", "Glob", "Grep", "Bash"],
53
- claudePath = "claude",
54
- resumeSessionId,
55
- model,
56
- onEvent
57
- } = options;
58
- const args = [];
59
- if (resumeSessionId) {
60
- args.push("--resume", resumeSessionId, "-p", prompt);
61
- } else {
62
- args.push("-p", prompt);
63
- }
64
- args.push(
65
- "--output-format",
66
- "stream-json",
67
- "--verbose",
68
- "--max-turns",
69
- String(maxTurns),
70
- "--max-budget-usd",
71
- String(maxBudgetUsd)
72
- );
73
- if (model) {
74
- args.push("--model", model);
75
- }
76
- for (const tool of allowedTools) {
77
- args.push("--allowedTools", tool);
78
- }
79
- const child = spawn(claudePath, args, {
80
- cwd: projectRoot,
81
- stdio: ["ignore", "pipe", "pipe"],
82
- env: __spreadProps(__spreadValues({}, process.env), { ANTHROPIC_API_KEY: void 0 })
83
- });
84
- const result = new Promise((resolve) => {
85
- var _a;
86
- let capturedSessionId;
87
- const textChunks = [];
88
- const fileEdits = [];
89
- let hadError = false;
90
- let errorMessage = "";
91
- const rl = createInterface({ input: child.stdout });
92
- const seenEventTypes = /* @__PURE__ */ new Set();
93
- rl.on("line", (line) => {
94
- var _a2, _b, _c, _d, _e, _f, _g, _h, _i;
95
- if (!line.trim()) return;
96
- try {
97
- const parsed = JSON.parse(line);
98
- if (parsed.session_id && !capturedSessionId) {
99
- capturedSessionId = parsed.session_id;
100
- }
101
- const topType = (_b = parsed.type) != null ? _b : ((_a2 = parsed.event) == null ? void 0 : _a2.type) ? `event.${parsed.event.type}` : "unknown";
102
- seenEventTypes.add(topType);
103
- if (parsed.type === "result" && parsed.result && textChunks.length === 0) {
104
- const resultText = typeof parsed.result === "string" ? parsed.result : "";
105
- if (resultText) {
106
- textChunks.push(resultText);
107
- onEvent == null ? void 0 : onEvent({ type: "delta", jobId, text: resultText }, jobId);
108
- }
109
- }
110
- if (parsed.type === "assistant" && Array.isArray((_c = parsed.message) == null ? void 0 : _c.content)) {
111
- for (const block of parsed.message.content) {
112
- if (block.type === "text" && block.text) {
113
- textChunks.push(block.text);
114
- onEvent == null ? void 0 : onEvent({ type: "delta", jobId, text: block.text }, jobId);
115
- }
116
- if (block.type === "tool_use" && block.name) {
117
- const file = ((_d = block.input) == null ? void 0 : _d.file_path) || ((_e = block.input) == null ? void 0 : _e.path) || void 0;
118
- onEvent == null ? void 0 : onEvent(__spreadValues({ type: "tool_use", jobId, tool: block.name }, file ? { file } : {}), jobId);
119
- if (block.name === "Edit" && ((_f = block.input) == null ? void 0 : _f.file_path)) {
120
- fileEdits.push({
121
- tool: "Edit",
122
- file_path: block.input.file_path,
123
- old_string: block.input.old_string,
124
- new_string: block.input.new_string,
125
- replace_all: block.input.replace_all
126
- });
127
- } else if (block.name === "Write" && ((_g = block.input) == null ? void 0 : _g.file_path)) {
128
- fileEdits.push({
129
- tool: "Write",
130
- file_path: block.input.file_path,
131
- content: block.input.content
132
- });
133
- }
134
- }
135
- if (block.type === "thinking" && block.thinking) {
136
- onEvent == null ? void 0 : onEvent({ type: "thinking", jobId, text: block.thinking }, jobId);
137
- }
138
- }
139
- }
140
- if (parsed.type === "user" && ((_i = (_h = parsed.tool_use_result) == null ? void 0 : _h.file) == null ? void 0 : _i.filePath)) {
141
- onEvent == null ? void 0 : onEvent({ type: "tool_use", jobId, tool: "Read", file: parsed.tool_use_result.file.filePath }, jobId);
142
- }
143
- } catch (e) {
144
- }
145
- });
146
- const stderrChunks = [];
147
- (_a = child.stderr) == null ? void 0 : _a.on("data", (chunk) => {
148
- stderrChunks.push(chunk.toString());
149
- });
150
- child.on("close", (code) => {
151
- rl.close();
152
- if (code !== 0 && code !== null) {
153
- hadError = true;
154
- errorMessage = stderrChunks.join("") || `Claude process exited with code ${code}`;
155
- }
156
- resolve({
157
- sessionId: capturedSessionId,
158
- text: textChunks.join(""),
159
- success: !hadError,
160
- error: hadError ? errorMessage : void 0,
161
- fileEdits: fileEdits.length > 0 ? fileEdits : void 0
162
- });
163
- });
164
- child.on("error", (err) => {
165
- hadError = true;
166
- errorMessage = err.message;
167
- resolve({
168
- sessionId: capturedSessionId,
169
- text: textChunks.join(""),
170
- success: false,
171
- error: errorMessage,
172
- fileEdits: fileEdits.length > 0 ? fileEdits : void 0
173
- });
174
- });
175
- });
176
- return { process: child, result };
177
- }
17
+ ## Instructions
18
+ 1. Review the current model (if any) and the new decisions below.
19
+ 2. Determine what design tokens, component patterns, and rules to add or update.
20
+ 3. Output the complete updated model as a JSON object inside <model> tags.
178
21
 
179
- // src/server/codex-spawner.ts
180
- import { spawn as spawn2 } from "child_process";
181
- import { createInterface as createInterface2 } from "readline";
182
- function spawnCodex(jobId, options) {
183
- const {
184
- prompt,
185
- projectRoot,
186
- screenshotPath,
187
- resumeSessionId,
188
- model,
189
- onEvent
190
- } = options;
191
- const args = [];
192
- if (resumeSessionId) {
193
- args.push("exec", "resume", resumeSessionId);
194
- if (model) args.push("-m", model);
195
- args.push("--json", "--full-auto", prompt);
196
- if (screenshotPath) {
197
- args.push("--image", screenshotPath);
198
- }
199
- } else {
200
- args.push("exec", "--json", "--full-auto");
201
- if (model) args.push("-m", model);
202
- args.push(prompt);
203
- if (screenshotPath) {
204
- args.push("--image", screenshotPath);
205
- }
206
- }
207
- const child = spawn2("codex", args, {
208
- cwd: projectRoot,
209
- stdio: ["ignore", "pipe", "pipe"],
210
- env: __spreadValues({}, process.env)
211
- });
212
- const result = new Promise((resolve) => {
213
- var _a;
214
- let capturedSessionId;
215
- const textChunks = [];
216
- let hadError = false;
217
- let errorMessage = "";
218
- const rl = createInterface2({ input: child.stdout });
219
- const seenEventTypes = /* @__PURE__ */ new Set();
220
- rl.on("line", (line) => {
221
- var _a2, _b, _c, _d;
222
- if (!line.trim()) return;
223
- try {
224
- const parsed = JSON.parse(line);
225
- const eventType = (_a2 = parsed.type) != null ? _a2 : "unknown";
226
- seenEventTypes.add(eventType);
227
- if (eventType === "thread.started" && parsed.thread_id && !capturedSessionId) {
228
- capturedSessionId = parsed.thread_id;
229
- }
230
- if (eventType === "item/agentMessage/delta" && ((_b = parsed.delta) == null ? void 0 : _b.text)) {
231
- textChunks.push(parsed.delta.text);
232
- onEvent == null ? void 0 : onEvent({ type: "delta", jobId, text: parsed.delta.text }, jobId);
233
- }
234
- if (eventType === "item/reasoning/delta" && ((_c = parsed.delta) == null ? void 0 : _c.text)) {
235
- onEvent == null ? void 0 : onEvent({ type: "thinking", jobId, text: parsed.delta.text }, jobId);
236
- }
237
- if (eventType === "item/started" && parsed.item) {
238
- const itemType = parsed.item.type;
239
- if (itemType === "command_execution") {
240
- onEvent == null ? void 0 : onEvent({ type: "tool_use", jobId, tool: "Bash" }, jobId);
241
- } else if (itemType === "file_change") {
242
- const file = parsed.item.filename || parsed.item.path;
243
- onEvent == null ? void 0 : onEvent(__spreadValues({ type: "tool_use", jobId, tool: "Edit" }, file ? { file } : {}), jobId);
244
- } else if (itemType === "file_read") {
245
- const file = parsed.item.filename || parsed.item.path;
246
- onEvent == null ? void 0 : onEvent(__spreadValues({ type: "tool_use", jobId, tool: "Read" }, file ? { file } : {}), jobId);
247
- } else if (itemType === "web_search") {
248
- onEvent == null ? void 0 : onEvent({ type: "tool_use", jobId, tool: "WebSearch" }, jobId);
249
- } else if (itemType === "mcp_tool_call") {
250
- const toolName = parsed.item.tool_name || parsed.item.name || "MCP";
251
- onEvent == null ? void 0 : onEvent({ type: "tool_use", jobId, tool: toolName }, jobId);
252
- }
253
- }
254
- if (eventType === "item/completed" && parsed.item) {
255
- if (parsed.item.type === "agent_message") {
256
- const itemText = parsed.item.text;
257
- if (typeof itemText === "string" && itemText) {
258
- textChunks.push(itemText);
259
- }
260
- } else if (parsed.item.type === "reasoning") {
261
- const reasoningText = parsed.item.text;
262
- if (typeof reasoningText === "string" && reasoningText) {
263
- onEvent == null ? void 0 : onEvent({ type: "thinking", jobId, text: reasoningText }, jobId);
264
- }
265
- }
266
- }
267
- if (eventType === "turn.failed") {
268
- hadError = true;
269
- errorMessage = ((_d = parsed.error) == null ? void 0 : _d.message) || parsed.message || "Turn failed";
270
- }
271
- } catch (e) {
272
- }
273
- });
274
- const stderrChunks = [];
275
- (_a = child.stderr) == null ? void 0 : _a.on("data", (chunk) => {
276
- stderrChunks.push(chunk.toString());
277
- });
278
- child.on("close", (code) => {
279
- rl.close();
280
- if (code !== 0 && code !== null) {
281
- hadError = true;
282
- errorMessage = stderrChunks.join("") || `Codex process exited with code ${code}`;
283
- }
284
- resolve({
285
- sessionId: capturedSessionId,
286
- text: textChunks.join(""),
287
- success: !hadError,
288
- error: hadError ? errorMessage : void 0
289
- });
290
- });
291
- child.on("error", (err) => {
292
- hadError = true;
293
- errorMessage = err.message;
294
- resolve({
295
- sessionId: capturedSessionId,
296
- text: textChunks.join(""),
297
- success: false,
298
- error: errorMessage
299
- });
300
- });
301
- });
302
- return { process: child, result };
303
- }
22
+ ${t?`## Current Model
23
+ \`\`\`json
24
+ ${JSON.stringify(t,null,2)}
25
+ \`\`\`
304
26
 
305
- // src/server/multipart.ts
306
- async function parseMultipart(req) {
307
- const contentType = req.headers["content-type"] || "";
308
- const boundaryMatch = contentType.match(/boundary=(?:"([^"]+)"|([^\s;]+))/);
309
- if (!boundaryMatch) {
310
- throw new Error("Missing multipart boundary");
311
- }
312
- const boundary = boundaryMatch[1] || boundaryMatch[2];
313
- const body = await readBody(req);
314
- const delimiter = Buffer.from(`--${boundary}`);
315
- const endDelimiter = Buffer.from(`--${boundary}--`);
316
- let screenshot;
317
- let feedback;
318
- let color;
319
- let provider;
320
- let model;
321
- let goal;
322
- let pageUrl;
323
- let viewport;
324
- let planId;
325
- let manifest;
326
- let tasks;
327
- const pastedImages = [];
328
- let offset = 0;
329
- const parts = [];
330
- while (offset < body.length) {
331
- const delimStart = body.indexOf(delimiter, offset);
332
- if (delimStart === -1) break;
333
- const afterDelim = delimStart + delimiter.length;
334
- if (body.slice(delimStart, delimStart + endDelimiter.length).equals(endDelimiter)) {
335
- break;
336
- }
337
- let headerStart = afterDelim;
338
- if (body[headerStart] === 13 && body[headerStart + 1] === 10) {
339
- headerStart += 2;
340
- }
341
- const headerEnd = body.indexOf("\r\n\r\n", headerStart);
342
- if (headerEnd === -1) break;
343
- const headers = body.slice(headerStart, headerEnd).toString("utf-8");
344
- const bodyStart = headerEnd + 4;
345
- const nextDelim = body.indexOf(delimiter, bodyStart);
346
- const bodyEnd = nextDelim !== -1 ? nextDelim - 2 : body.length;
347
- parts.push({
348
- headers,
349
- body: body.slice(bodyStart, bodyEnd)
350
- });
351
- offset = nextDelim !== -1 ? nextDelim : body.length;
352
- }
353
- for (const part of parts) {
354
- const nameMatch = part.headers.match(/name="([^"]+)"/);
355
- if (!nameMatch) continue;
356
- const name = nameMatch[1];
357
- if (name === "screenshot") {
358
- screenshot = part.body;
359
- } else if (name === "feedback") {
360
- feedback = part.body.toString("utf-8");
361
- } else if (name === "color") {
362
- color = part.body.toString("utf-8");
363
- } else if (name === "provider") {
364
- provider = part.body.toString("utf-8");
365
- } else if (name === "model") {
366
- model = part.body.toString("utf-8");
367
- } else if (name === "goal") {
368
- goal = part.body.toString("utf-8");
369
- } else if (name === "pageUrl") {
370
- pageUrl = part.body.toString("utf-8");
371
- } else if (name === "viewport") {
372
- viewport = part.body.toString("utf-8");
373
- } else if (name === "planId") {
374
- planId = part.body.toString("utf-8");
375
- } else if (name === "manifest") {
376
- manifest = part.body.toString("utf-8");
377
- } else if (name === "tasks") {
378
- tasks = part.body.toString("utf-8");
379
- } else if (name.startsWith("image-")) {
380
- const segments = name.split("-");
381
- const idx = parseInt(segments[segments.length - 1], 10);
382
- const annotationId = segments.slice(1, -1).join("-");
383
- if (annotationId && !isNaN(idx)) {
384
- pastedImages.push({ annotationId, index: idx, data: part.body });
385
- }
386
- }
387
- }
388
- if (!screenshot) throw new Error("Missing screenshot field");
389
- if (!feedback) feedback = "";
390
- return { screenshot, feedback, color, provider, model, goal, pageUrl, viewport, planId, manifest, tasks, pastedImages };
391
- }
392
- function readBody(req) {
393
- return new Promise((resolve, reject) => {
394
- const chunks = [];
395
- req.on("data", (chunk) => chunks.push(chunk));
396
- req.on("end", () => resolve(Buffer.concat(chunks)));
397
- req.on("error", reject);
398
- });
399
- }
27
+ Merge the new decisions into the existing model. Preserve existing entries that are not contradicted by new decisions.`:"No model exists yet. Create one from scratch based on the decisions below."}
400
28
 
401
- // src/server/prompt-builder.ts
402
- function formatFeedbackContext(feedback, imagePaths) {
403
- var _a;
404
- const lines = [];
405
- if (feedback.annotations.length > 0) {
406
- lines.push("## Annotations");
407
- for (const ann of feedback.annotations) {
408
- const elementsDesc = ann.elements.map((el) => {
409
- const parts = [el.selector];
410
- if (el.reactComponent) parts.push(`(${el.reactComponent})`);
411
- return parts.join(" ");
412
- }).join(", ");
413
- const instruction = ann.instruction || "No text";
414
- lines.push(`- id=${ann.id} [${ann.type}] ${instruction} \u2192 Elements: ${elementsDesc || "none"}`);
415
- const annImages = imagePaths == null ? void 0 : imagePaths[ann.id];
416
- if (annImages && annImages.length > 0) {
417
- for (const imgPath of annImages) {
418
- lines.push(` Attached image: use the Read tool to view ${imgPath}`);
419
- }
420
- }
421
- }
422
- }
423
- if (feedback.styleModifications.length > 0) {
424
- lines.push("");
425
- lines.push("## Style Changes (make permanent in source)");
426
- lines.push("The developer previewed these CSS changes via inline style overrides. Find the corresponding styles in the source files and update them so the changes persist:");
427
- for (const mod of feedback.styleModifications) {
428
- const elementDesc = ((_a = mod.element) == null ? void 0 : _a.reactComponent) ? `(${mod.element.reactComponent})` : "";
429
- for (const change of mod.changes) {
430
- lines.push(
431
- `- ${mod.selector} ${elementDesc}: ${change.property} ${change.original} \u2192 ${change.modified}`
432
- );
433
- }
434
- }
435
- }
436
- if (feedback.inspectedElement) {
437
- const el = feedback.inspectedElement;
438
- lines.push("");
439
- lines.push("## Inspected Element");
440
- lines.push("The developer has this element selected in the inspector:");
441
- const parts = [el.selector];
442
- if (el.reactComponent) parts.push(`(${el.reactComponent})`);
443
- if (el.context) parts.push(`in ${el.context}`);
444
- if (el.textContent) parts.push(`"${el.textContent.slice(0, 80)}"`);
445
- lines.push(`- ${parts.join(" ")}`);
446
- }
447
- return lines.join("\n");
448
- }
449
- function buildPrompt(screenshotPath, feedback, options) {
450
- const lines = [];
451
- lines.push("You are reviewing a UI screenshot with developer annotations.");
452
- lines.push("");
453
- if ((options == null ? void 0 : options.provider) !== "codex") {
454
- lines.push(`IMPORTANT: First, use the Read tool to view the screenshot at: ${screenshotPath}`);
455
- lines.push("");
456
- }
457
- lines.push(
458
- `The developer annotated their running app at ${feedback.url} (${feedback.viewport.width}x${feedback.viewport.height}).`
459
- );
460
- if ((options == null ? void 0 : options.threadHistory) && options.threadHistory.length > 0) {
461
- lines.push("");
462
- lines.push("## Previous Conversation");
463
- let roundNum = 0;
464
- for (const msg of options.threadHistory) {
465
- if (msg.role === "human") {
466
- roundNum++;
467
- if (msg.replyToQuestion) {
468
- lines.push(`### Round ${roundNum} (human) \u2014 reply`);
469
- lines.push(`"${msg.replyToQuestion}"`);
470
- } else {
471
- lines.push(`### Round ${roundNum} (human)`);
472
- if (msg.feedbackSummary) {
473
- lines.push(`Annotations: ${msg.feedbackSummary}`);
474
- }
475
- if (msg.annotationIds && msg.annotationIds.length > 0) {
476
- lines.push(`Annotation IDs: ${msg.annotationIds.join(", ")}`);
477
- }
478
- }
479
- } else {
480
- if (msg.question) {
481
- lines.push(`### Round ${roundNum} (assistant) \u2014 question`);
482
- lines.push(`"${msg.question}"`);
483
- } else {
484
- lines.push(`### Round ${roundNum} (assistant)`);
485
- if (msg.responseText) {
486
- lines.push(`Response: ${msg.responseText}`);
487
- }
488
- if (msg.resolutions && msg.resolutions.length > 0) {
489
- for (const r of msg.resolutions) {
490
- lines.push(`- ${r.annotationId}: ${r.status} \u2014 ${r.summary}`);
491
- if (r.filesModified && r.filesModified.length > 0) {
492
- lines.push(` Files: ${r.filesModified.join(", ")}`);
493
- }
494
- }
495
- }
496
- if (msg.toolsUsed && msg.toolsUsed.length > 0) {
497
- lines.push(`Tools used: ${msg.toolsUsed.join(", ")}`);
498
- }
499
- }
500
- }
501
- }
502
- lines.push("");
503
- lines.push("The current round is shown in full below.");
504
- }
505
- const feedbackContext = formatFeedbackContext(feedback, options == null ? void 0 : options.imagePaths);
506
- if (feedbackContext) {
507
- lines.push("");
508
- lines.push(feedbackContext);
509
- }
510
- lines.push("");
511
- lines.push(
512
- "Follow the developer's instructions. If they ask for changes, apply them to the source files \u2014 the dev server has HMR so changes appear immediately. If they ask a question or request analysis, respond in text without modifying code."
513
- );
514
- lines.push("");
515
- lines.push(
516
- "IMPORTANT: If any elements you modify have a `data-pm` attribute, preserve it in the source. This attribute tracks annotation positions."
517
- );
518
- lines.push("");
519
- lines.push("## Resolution");
520
- lines.push("After completing all work, output a resolution block listing what you did for each annotation:");
521
- lines.push("<resolution>");
522
- lines.push('[{"annotationId":"<id>","status":"resolved","summary":"<what you did>","filesModified":["<file>"]}]');
523
- lines.push("</resolution>");
524
- lines.push(`Use status "resolved" when the change is complete, or "needs_review" if you're unsure about the result.`);
525
- lines.push("");
526
- lines.push("## Questions");
527
- lines.push("If the annotation text is unclear, ambiguous, gibberish, or you are unsure what the developer wants, output a question:");
528
- lines.push('<question>What do you mean by "..."?</question>');
529
- lines.push("Do NOT guess what unclear instructions mean \u2014 ask instead.");
530
- lines.push("You may output BOTH a <resolution> for clear annotations AND a <question> for unclear ones in the same response.");
531
- return lines.join("\n");
532
- }
533
- function parseQuestion(responseText) {
534
- var _a;
535
- const match = responseText.match(/<question>\s*([\s\S]*?)\s*<\/question>/);
536
- return (_a = match == null ? void 0 : match[1]) != null ? _a : null;
537
- }
538
- function buildReplyPrompt(screenshotPath, threadHistory, provider, imagePaths) {
539
- const lines = [];
540
- lines.push("You are continuing work on a UI based on the developer's reply to your question.");
541
- lines.push("");
542
- if (provider !== "codex") {
543
- lines.push(`IMPORTANT: First, use the Read tool to view the screenshot at: ${screenshotPath}`);
544
- }
545
- const firstHuman = threadHistory.find((m) => m.role === "human" && m.feedbackContext);
546
- if (firstHuman == null ? void 0 : firstHuman.feedbackContext) {
547
- lines.push("");
548
- lines.push(firstHuman.feedbackContext);
549
- }
550
- if (threadHistory.length > 0) {
551
- lines.push("");
552
- lines.push("## Conversation History");
553
- let roundNum = 0;
554
- for (const msg of threadHistory) {
555
- if (msg.role === "human") {
556
- roundNum++;
557
- if (msg.replyToQuestion) {
558
- lines.push(`### Round ${roundNum} (human) \u2014 reply`);
559
- lines.push(`"${msg.replyToQuestion}"`);
560
- } else {
561
- lines.push(`### Round ${roundNum} (human)`);
562
- if (msg.feedbackSummary) {
563
- lines.push(`Annotations: ${msg.feedbackSummary}`);
564
- }
565
- }
566
- } else {
567
- if (msg.question) {
568
- lines.push(`### Round ${roundNum} (assistant) \u2014 question`);
569
- lines.push(`"${msg.question}"`);
570
- } else {
571
- lines.push(`### Round ${roundNum} (assistant)`);
572
- if (msg.responseText) {
573
- lines.push(`Response: ${msg.responseText}`);
574
- }
575
- }
576
- }
577
- }
578
- }
579
- lines.push("");
580
- lines.push("The developer answered your question. Continue working based on their reply.");
581
- lines.push("Follow their instructions \u2014 apply code changes only if requested. The dev server has HMR so changes appear immediately.");
582
- lines.push("");
583
- lines.push("IMPORTANT: If any elements you modify have a `data-pm` attribute, preserve it in the source. This attribute tracks annotation positions.");
584
- if (imagePaths && imagePaths.length > 0) {
585
- lines.push("");
586
- lines.push("## Attached Images");
587
- lines.push("The developer attached reference images with their reply:");
588
- for (const imgPath of imagePaths) {
589
- lines.push(`Attached image: use the Read tool to view the image at: ${imgPath}`);
590
- }
591
- }
592
- lines.push("");
593
- lines.push("## Resolution");
594
- lines.push("After completing all work, output a resolution block listing what you did for each annotation:");
595
- lines.push("<resolution>");
596
- lines.push('[{"annotationId":"<id>","status":"resolved","summary":"<what you did>","filesModified":["<file>"]}]');
597
- lines.push("</resolution>");
598
- lines.push(`Use status "resolved" when the change is complete, or "needs_review" if you're unsure about the result.`);
599
- lines.push("");
600
- lines.push("## Questions");
601
- lines.push("If you still need clarification, output:");
602
- lines.push("<question>Your question here</question>");
603
- lines.push("You may output BOTH a <resolution> and a <question> in the same response.");
604
- return lines.join("\n");
605
- }
606
- function isValidResolution(r) {
607
- return typeof r === "object" && r !== null && typeof r.annotationId === "string" && (r.status === "resolved" || r.status === "needs_review") && typeof r.summary === "string";
608
- }
609
- function parseResolutions(responseText) {
610
- const match = responseText.match(/<resolution>\s*([\s\S]*?)\s*<\/resolution>/);
611
- if (!match || !match[1]) return [];
612
- try {
613
- const parsed = JSON.parse(match[1]);
614
- if (!Array.isArray(parsed)) return [];
615
- return parsed.filter(isValidResolution);
616
- } catch (e) {
617
- return [];
618
- }
619
- }
620
- function parseAllResolutions(responseText) {
621
- const results = [];
622
- const regex = /<resolution>\s*([\s\S]*?)\s*<\/resolution>/g;
623
- let match;
624
- while ((match = regex.exec(responseText)) !== null) {
625
- if (!match[1]) continue;
626
- try {
627
- const parsed = JSON.parse(match[1]);
628
- if (Array.isArray(parsed)) {
629
- results.push(...parsed.filter(isValidResolution));
630
- }
631
- } catch (e) {
632
- }
633
- }
634
- return results;
635
- }
636
- function buildPlannerPrompt(screenshotPath, goal, pageUrl, viewport, manifestJson, feedbackContext) {
637
- const lines = [];
638
- lines.push("You are a UI design planner. You are looking at a full-page screenshot of a web application.");
639
- lines.push("");
640
- lines.push(`IMPORTANT: First, use the Read tool to view the screenshot at: ${screenshotPath}`);
641
- lines.push("");
642
- lines.push(`Page: ${pageUrl}`);
643
- lines.push(`Viewport: ${viewport.width}x${viewport.height}`);
644
- if (manifestJson) {
645
- lines.push("");
646
- lines.push("## Page Elements (ground truth)");
647
- lines.push("Below is a structured inventory of actual DOM elements on this page. Cross-reference");
648
- lines.push("against this list \u2014 do NOT reference elements that aren't listed here.");
649
- lines.push("");
650
- lines.push("<manifest>");
651
- lines.push(manifestJson);
652
- lines.push("</manifest>");
653
- }
654
- if (feedbackContext) {
655
- lines.push("");
656
- lines.push("## Developer Context");
657
- lines.push("The developer has the following annotations and style changes on their canvas. Factor these into your plan:");
658
- lines.push(feedbackContext);
659
- }
660
- lines.push("");
661
- lines.push("## Goal");
662
- lines.push(goal);
663
- lines.push("");
664
- lines.push("## Your Task");
665
- lines.push("Analyze the screenshot and decompose the goal into specific, element-level tasks.");
666
- lines.push("Each task targets a specific region of the page and gives a clear instruction for a worker agent.");
667
- lines.push("");
668
- lines.push("Output your plan as a JSON array inside a <plan> tag. Each task has:");
669
- lines.push('- `id`: A short unique identifier (e.g., "t1", "t2")');
670
- lines.push("- `instruction`: Clear, specific instruction for a worker agent (what to change and how)");
671
- lines.push("- `region`: Bounding box in page coordinates `{x, y, width, height}` \u2014 where (x,y) is top-left corner");
672
- lines.push("- `priority`: Optional 1-5 (1=highest). Tasks with no dependency can share a priority level.");
673
- lines.push("");
674
- lines.push("Example:");
675
- lines.push("<plan>");
676
- lines.push("[");
677
- lines.push(' {"id":"t1","instruction":"Increase heading font-size to 48px and change font-weight to 700","region":{"x":100,"y":50,"width":600,"height":80},"priority":1},');
678
- lines.push(' {"id":"t2","instruction":"Add a subtle box-shadow to the card container","region":{"x":80,"y":200,"width":640,"height":300},"priority":2}');
679
- lines.push("]");
680
- lines.push("</plan>");
681
- lines.push("");
682
- lines.push("Guidelines:");
683
- lines.push("- CRITICAL: Cross-check all element references against the <manifest>. Only reference elements that actually exist. Use the manifest's text content, component names, and bounding rects for precise instructions.");
684
- lines.push('- Be specific about values (colors, sizes, spacing) rather than vague ("make it look better")');
685
- lines.push("- Each task should be independently actionable by a worker that can only see its region");
686
- lines.push("- Regions should tightly bound the relevant UI element(s)");
687
- lines.push("- Keep tasks atomic \u2014 one change per task, not multiple unrelated changes");
688
- lines.push("- Order by priority: structural changes first, then visual polish");
689
- lines.push("- If the goal can be accomplished as a single change, return a plan with just one task. Only decompose when the goal genuinely requires multiple independent changes.");
690
- lines.push("- If the goal is unclear or you need more context, output a question instead:");
691
- lines.push("<question>Your question here</question>");
692
- lines.push("");
693
- lines.push("Do NOT modify any files. You are a planner only \u2014 output a <plan> or <question>, nothing else.");
694
- return lines.join("\n");
695
- }
696
- function parsePlan(responseText) {
697
- const match = responseText.match(/<plan>\s*([\s\S]*?)\s*<\/plan>/);
698
- if (!(match == null ? void 0 : match[1])) return null;
699
- try {
700
- const parsed = JSON.parse(match[1]);
701
- if (!Array.isArray(parsed)) return null;
702
- return parsed.filter(
703
- (t) => {
704
- if (typeof t !== "object" || t === null) return false;
705
- const obj = t;
706
- if (typeof obj.id !== "string" || typeof obj.instruction !== "string") return false;
707
- if (typeof obj.region !== "object" || obj.region === null) return false;
708
- const r = obj.region;
709
- return typeof r.x === "number" && typeof r.y === "number" && typeof r.width === "number" && typeof r.height === "number";
710
- }
711
- );
712
- } catch (e) {
713
- return null;
714
- }
715
- }
716
- function buildReviewerPrompt(screenshotPath, goal, completedTasks) {
717
- const lines = [];
718
- lines.push("You are reviewing whether a series of UI changes achieved the original design goal.");
719
- lines.push("");
720
- lines.push(`IMPORTANT: First, use the Read tool to view the screenshot at: ${screenshotPath}`);
721
- lines.push("");
722
- lines.push("## Original Goal");
723
- lines.push(goal);
724
- lines.push("");
725
- lines.push("## Completed Tasks");
726
- for (const task of completedTasks) {
727
- lines.push(`- [${task.id}] ${task.instruction} \u2192 ${task.summary}`);
728
- }
729
- lines.push("");
730
- lines.push("## Your Task");
731
- lines.push("Look at the current screenshot and determine if the goal has been achieved.");
732
- lines.push("Output your verdict inside a <review> tag:");
733
- lines.push("<review>");
734
- lines.push('{"verdict":"pass","summary":"The changes look good..."}');
735
- lines.push("</review>");
736
- lines.push("");
737
- lines.push("Or if issues remain:");
738
- lines.push("<review>");
739
- lines.push('{"verdict":"fail","summary":"Some issues remain...","issues":["Issue 1","Issue 2"]}');
740
- lines.push("</review>");
741
- lines.push("");
742
- lines.push("Do NOT modify any files. Output only a <review> block.");
743
- return lines.join("\n");
744
- }
745
- function buildPlanExecutorPrompt(screenshotPath, tasks, pageUrl, viewport, provider) {
746
- const lines = [];
747
- lines.push("You are implementing a series of UI changes on a web application.");
748
- lines.push("");
749
- if (provider !== "codex") {
750
- lines.push(`IMPORTANT: First, use the Read tool to view the screenshot at: ${screenshotPath}`);
751
- lines.push("");
752
- }
753
- lines.push(`Page: ${pageUrl} (${viewport.width}x${viewport.height})`);
754
- lines.push("");
755
- lines.push("## Tasks");
756
- lines.push("Each task targets a specific region of the page. Complete them in order.");
757
- lines.push("");
758
- for (const task of tasks) {
759
- lines.push(`### Task ${task.planTaskId} (annotationId: ${task.annotationId})`);
760
- lines.push(`Instruction: ${task.instruction}`);
761
- lines.push(`Region: (${task.region.x}, ${task.region.y}) ${task.region.width}x${task.region.height}`);
762
- if (task.linkedSelector) {
763
- lines.push(`Target element: ${task.linkedSelector}`);
764
- }
765
- if (task.elements && task.elements.length > 0) {
766
- const elemDesc = task.elements.map((el) => {
767
- const parts = [el.selector];
768
- if (el.reactComponent) parts.push(`(${el.reactComponent})`);
769
- return parts.join(" ");
770
- }).join(", ");
771
- lines.push(`Elements: ${elemDesc}`);
772
- }
773
- lines.push("");
774
- }
775
- lines.push("## Instructions");
776
- lines.push("- Apply each change to the source files \u2014 the dev server has HMR so changes appear immediately.");
777
- lines.push("- IMPORTANT: If any elements you modify have a `data-pm` attribute, preserve it in the source.");
778
- lines.push("- You may use parallel subagents (Task tool) for independent changes, or work serially \u2014 use your judgment.");
779
- lines.push("");
780
- lines.push("## Resolution");
781
- lines.push("CRITICAL: After completing EACH task, immediately output a <resolution> block for that task.");
782
- lines.push("Do NOT wait until all tasks are done \u2014 output each resolution as soon as that task is finished.");
783
- lines.push("<resolution>");
784
- lines.push('[{"annotationId":"<annotationId>","status":"resolved","summary":"<what you did>","filesModified":["<file>"]}]');
785
- lines.push("</resolution>");
786
- lines.push(`Use status "resolved" when the change is complete, or "needs_review" if you're unsure about the result.`);
787
- return lines.join("\n");
788
- }
789
- function parseReview(responseText) {
790
- const match = responseText.match(/<review>\s*([\s\S]*?)\s*<\/review>/);
791
- if (!(match == null ? void 0 : match[1])) return null;
792
- try {
793
- const parsed = JSON.parse(match[1]);
794
- if (typeof parsed !== "object" || parsed === null) return null;
795
- if (parsed.verdict !== "pass" && parsed.verdict !== "fail") return null;
796
- if (typeof parsed.summary !== "string") return null;
797
- return {
798
- verdict: parsed.verdict,
799
- summary: parsed.summary,
800
- issues: Array.isArray(parsed.issues) ? parsed.issues.filter((i) => typeof i === "string") : void 0
801
- };
802
- } catch (e) {
803
- return null;
804
- }
805
- }
29
+ ## Design Decisions to Materialize
30
+ ${n}
806
31
 
807
- // src/server/queue.ts
808
- var JobQueue = class {
809
- constructor(maxConcurrency = 5) {
810
- this.queue = [];
811
- this.activeJobs = /* @__PURE__ */ new Map();
812
- this.activeProcesses = /* @__PURE__ */ new Map();
813
- this.listeners = /* @__PURE__ */ new Set();
814
- this.processor = null;
815
- this.maxConcurrency = maxConcurrency;
816
- }
817
- setProcessor(fn) {
818
- this.processor = fn;
819
- }
820
- /** First active job (backward compat for status endpoint) */
821
- get active() {
822
- const first = this.activeJobs.values().next();
823
- return first.done ? null : first.value;
824
- }
825
- get allActive() {
826
- return Array.from(this.activeJobs.values());
827
- }
828
- get activeCount() {
829
- return this.activeJobs.size;
830
- }
831
- get depth() {
832
- return this.queue.length;
833
- }
834
- get isRunning() {
835
- return this.activeJobs.size > 0;
836
- }
837
- setActiveProcess(jobId, proc) {
838
- if (proc) {
839
- this.activeProcesses.set(jobId, proc);
840
- } else {
841
- this.activeProcesses.delete(jobId);
842
- }
843
- }
844
- enqueue(job) {
845
- this.queue.push(job);
846
- this.processNext();
847
- return this.queue.length + this.activeJobs.size;
848
- }
849
- addListener(listener) {
850
- this.listeners.add(listener);
851
- return () => this.listeners.delete(listener);
852
- }
853
- broadcast(event, jobId) {
854
- for (const listener of this.listeners) {
855
- listener(event, jobId);
856
- }
857
- }
858
- cancelJob(jobId) {
859
- const proc = this.activeProcesses.get(jobId);
860
- const job = this.activeJobs.get(jobId);
861
- if (!proc || !job) return false;
862
- proc.kill("SIGTERM");
863
- this.activeProcesses.delete(jobId);
864
- this.activeJobs.delete(jobId);
865
- job.status = "error";
866
- job.error = "Cancelled by user";
867
- this.broadcast(
868
- { type: "error", jobId: job.id, message: "Cancelled by user" },
869
- job.id
870
- );
871
- this.processNext();
872
- return true;
873
- }
874
- cancelActive() {
875
- if (this.activeJobs.size === 0) return false;
876
- const jobIds = Array.from(this.activeJobs.keys());
877
- for (const jobId of jobIds) {
878
- this.cancelJob(jobId);
879
- }
880
- return true;
881
- }
882
- destroy() {
883
- for (const proc of this.activeProcesses.values()) {
884
- proc.kill("SIGTERM");
885
- }
886
- this.activeProcesses.clear();
887
- this.activeJobs.clear();
888
- this.queue = [];
889
- this.listeners.clear();
890
- }
891
- processNext() {
892
- while (this.activeJobs.size < this.maxConcurrency && this.queue.length > 0 && this.processor) {
893
- const job = this.queue.shift();
894
- this.activeJobs.set(job.id, job);
895
- job.status = "running";
896
- this.broadcast({ type: "job_started", jobId: job.id, position: 0 }, job.id);
897
- this.processor(job).catch((err) => {
898
- job.status = "error";
899
- job.error = err instanceof Error ? err.message : String(err);
900
- this.broadcast(
901
- { type: "error", jobId: job.id, message: job.error },
902
- job.id
903
- );
904
- }).finally(() => {
905
- this.activeJobs.delete(job.id);
906
- this.activeProcesses.delete(job.id);
907
- this.processNext();
908
- if (this.activeJobs.size === 0 && this.queue.length === 0) {
909
- this.broadcast({ type: "queue_drained" }, job.id);
910
- }
911
- });
912
- }
913
- }
914
- };
32
+ ## Output Format
33
+ Output the full model inside <model> tags. The model is a JSON object with these sections:
34
+ - \`tokens\`: Design tokens (colors, spacing, typography, etc.)
35
+ - \`components\`: Component-level patterns (e.g., button styles, card layouts)
36
+ - \`rules\`: Array of plain-language rules extracted from decisions
915
37
 
916
- // src/server/thread-store.ts
917
- import { mkdir, readFile, writeFile } from "fs/promises";
918
- import { dirname, join } from "path";
919
- var EMPTY_STORE = { version: 1, threads: {} };
920
- var ThreadFileStore = class {
921
- constructor(projectRoot) {
922
- this.cache = null;
923
- this.writeChain = Promise.resolve();
924
- this.filePath = join(projectRoot, ".popmelt", "threads.json");
925
- }
926
- async load() {
927
- if (this.cache) return this.cache;
928
- try {
929
- const raw = await readFile(this.filePath, "utf-8");
930
- const parsed = JSON.parse(raw);
931
- if (parsed && parsed.version === 1 && parsed.threads) {
932
- this.cache = parsed;
933
- return this.cache;
934
- }
935
- } catch (e) {
936
- }
937
- this.cache = __spreadProps(__spreadValues({}, EMPTY_STORE), { threads: {} });
938
- return this.cache;
939
- }
940
- async getThread(id) {
941
- var _a;
942
- const store = await this.load();
943
- return (_a = store.threads[id]) != null ? _a : null;
944
- }
945
- async findContinuationThread(linkedSelectors) {
946
- if (linkedSelectors.length === 0) return null;
947
- const store = await this.load();
948
- const selectorSet = new Set(linkedSelectors);
949
- for (const thread of Object.values(store.threads)) {
950
- const hasOverlap = thread.elementIdentifiers.some((id) => selectorSet.has(id));
951
- if (hasOverlap) return thread;
952
- }
953
- return null;
954
- }
955
- async createThread(id, linkedSelectors) {
956
- const store = await this.load();
957
- const thread = {
958
- id,
959
- createdAt: Date.now(),
960
- updatedAt: Date.now(),
961
- elementIdentifiers: linkedSelectors,
962
- messages: []
963
- };
964
- store.threads[id] = thread;
965
- await this.persist();
966
- return thread;
967
- }
968
- async appendMessage(threadId, message) {
969
- const store = await this.load();
970
- const thread = store.threads[threadId];
971
- if (!thread) return;
972
- thread.messages.push(message);
973
- thread.updatedAt = Date.now();
974
- await this.persist();
975
- }
976
- async addElementIdentifiers(threadId, selectors) {
977
- const store = await this.load();
978
- const thread = store.threads[threadId];
979
- if (!thread) return;
980
- const existing = new Set(thread.elementIdentifiers);
981
- for (const sel of selectors) {
982
- if (!existing.has(sel)) {
983
- thread.elementIdentifiers.push(sel);
984
- }
985
- }
986
- thread.updatedAt = Date.now();
987
- await this.persist();
988
- }
989
- async getThreadHistory(threadId, maxMessages = 6) {
990
- const thread = await this.getThread(threadId);
991
- if (!thread || thread.messages.length === 0) return [];
992
- if (thread.messages.length <= maxMessages) {
993
- return thread.messages;
994
- }
995
- return [
996
- thread.messages[0],
997
- ...thread.messages.slice(-(maxMessages - 1))
998
- ];
999
- }
1000
- async persist() {
1001
- this.writeChain = this.writeChain.then(async () => {
1002
- if (!this.cache) return;
1003
- try {
1004
- await mkdir(dirname(this.filePath), { recursive: true });
1005
- await writeFile(this.filePath, JSON.stringify(this.cache, null, 2));
1006
- } catch (err) {
1007
- console.error("[ThreadStore] Failed to persist:", err);
1008
- }
1009
- });
1010
- await this.writeChain;
1011
- }
1012
- };
38
+ Example:
39
+ <model>
40
+ {
41
+ "tokens": {
42
+ "colors": { "primary": "#3b82f6" },
43
+ "spacing": {
44
+ "sm": "4px",
45
+ "md": "8px",
46
+ "section-gap": { "value": "32px", "property": "gap", "bindings": ["gap-8"] }
47
+ }
48
+ },
49
+ "components": {
50
+ "button": { "padding": "12px 24px", "borderRadius": "8px" }
51
+ },
52
+ "rules": [
53
+ "Buttons use 12px vertical padding and 24px horizontal padding",
54
+ "Primary actions use the primary color"
55
+ ]
56
+ }
57
+ </model>
1013
58
 
1014
- // src/server/bridge-server.ts
1015
- var DEFAULT_PORT = 1111;
1016
- var DEFAULT_ALLOWED_TOOLS = ["Read", "Edit", "Write", "Glob", "Grep", "Bash", "WebFetch", "WebSearch", "Bash(curl:*)"];
1017
- var CLEANUP_INTERVAL_MS = 30 * 60 * 1e3;
1018
- var MAX_FILE_AGE_MS = 60 * 60 * 1e3;
1019
- function isLocalhostOrigin(origin) {
1020
- if (!origin) return false;
1021
- try {
1022
- const url = new URL(origin);
1023
- return url.hostname === "localhost" || url.hostname === "127.0.0.1";
1024
- } catch (e) {
1025
- return false;
1026
- }
1027
- }
1028
- function setCors(req, res) {
1029
- const origin = req.headers.origin;
1030
- if (isLocalhostOrigin(origin)) {
1031
- res.setHeader("Access-Control-Allow-Origin", origin);
1032
- res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
1033
- res.setHeader("Access-Control-Allow-Headers", "Content-Type");
1034
- }
1035
- }
1036
- function sendJson(res, status, data) {
1037
- res.writeHead(status, { "Content-Type": "application/json" });
1038
- res.end(JSON.stringify(data));
1039
- }
1040
- function ansiColor(hex, text) {
1041
- if (!hex) return text;
1042
- const m = hex.match(/^#?([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i);
1043
- if (!m) return text;
1044
- const [, r, g, b] = m;
1045
- return `\x1B[38;2;${parseInt(r, 16)};${parseInt(g, 16)};${parseInt(b, 16)}m${text}\x1B[0m`;
1046
- }
1047
- function sendSSE(client, event) {
1048
- try {
1049
- client.res.write(`event: ${event.type}
1050
- data: ${JSON.stringify(event)}
59
+ ## Guidelines
60
+ - Map token-scoped decisions to \`tokens\`, component-scoped to \`components\`.
61
+ - Extract clear, enforceable rules into the \`rules\` array.
62
+ - When merging, new decisions override conflicting older values.
63
+ - Keep the model concise \u2014 only include patterns with clear evidence from decisions.
64
+ - Do NOT output resolution or question blocks. Just output the <model> block.
65
+ - Spacing tokens can be plain strings ("8px") or objects with code bindings:
66
+ { "value": "8px", "property": "gap", "bindings": ["gap-2"] }
67
+ - property: "gap", "padding", or "margin" \u2014 which CSS property this token controls
68
+ - bindings: Tailwind class names (without responsive prefixes) that use this token
69
+ Include property and bindings when the decision context has class-level evidence.`}import{readFile as at}from"fs/promises";import{homedir as ct}from"os";import{join as fe}from"path";var _e=/popmelt/i;function Be(){return{found:!1,name:null,scope:null,disabled:!1}}function ve(r,t,n=!1){return{found:!0,name:r,scope:t,disabled:n}}function Oe(r){for(let t of Object.keys(r))if(_e.test(t))return t;return null}async function Ne(r){try{let t=await at(r,"utf-8");return JSON.parse(t)}catch(t){return null}}async function Ue(r){let t=ct(),n=await Ne(fe(t,".claude.json"));if(n&&typeof n=="object"){let s=n;if(s.mcpServers&&typeof s.mcpServers=="object"){let a=Oe(s.mcpServers);if(a)return ve(a,"user")}if(s.projects&&typeof s.projects=="object"){let i=s.projects[r];if(i&&typeof i=="object"){let c=i;if(c.mcpServers&&typeof c.mcpServers=="object"){let p=Oe(c.mcpServers);if(p){let u=Array.isArray(c.disabledMcpjsonServers)&&c.disabledMcpjsonServers.some(v=>_e.test(v));return ve(p,"project",u)}}}}}let e=await Ne(fe(r,".mcp.json"));if(e&&typeof e=="object"){let s=e;if(s.mcpServers&&typeof s.mcpServers=="object"){let i=Oe(s.mcpServers);if(i){let c=await rt(r,i);return ve(i,"mcp.json",c)}}let a=Oe(s);if(a&&a!=="mcpServers"){let i=await rt(r,a);return ve(a,"mcp.json",i)}}return Be()}async function rt(r,t){let n=fe(r,".claude","settings.local.json"),e=await Ne(n);if(e&&typeof e=="object"){let s=e;if(Array.isArray(s.disabledMcpjsonServers))return s.disabledMcpjsonServers.some(a=>a===t)}return!1}var Gt=/^\[mcp_servers\.([^\]]+)\]/;function qt(r){let t=r.split(`
70
+ `),n=[],e=null;for(let s of t){let a=s.match(Gt);a?(e&&n.push({name:e.name,body:e.bodyLines.join(`
71
+ `)}),e={name:a[1],bodyLines:[]}):s.startsWith("[")?e&&(n.push({name:e.name,body:e.bodyLines.join(`
72
+ `)}),e=null):e&&e.bodyLines.push(s)}return e&&n.push({name:e.name,body:e.bodyLines.join(`
73
+ `)}),n}function Yt(r){return/enabled\s*=\s*false/i.test(r)}async function Fe(r){let t=process.env.CODEX_HOME||fe(ct(),".codex"),n=await it(fe(t,"config.toml"),"user");if(n.found)return n;let e=await it(fe(r,".codex","config.toml"),"project");return e.found?e:Be()}async function it(r,t){try{let n=await at(r,"utf-8"),e=qt(n);for(let s of e)if(_e.test(s.name)){let a=Yt(s.body);return ve(s.name,t,a)}}catch(n){}return Be()}import{mkdir as Qt,readFile as lt,writeFile as dt}from"fs/promises";import{homedir as ut}from"os";import{dirname as Vt,join as ze}from"path";var pt="https://mcp.popmelt.com/mcp";async function ht(r=pt){let t=ze(ut(),".claude.json"),n;try{let s=await lt(t,"utf-8");n=JSON.parse(s)}catch(s){n={}}(!n.mcpServers||typeof n.mcpServers!="object")&&(n.mcpServers={});let e=n.mcpServers;for(let s of Object.keys(e))if(/popmelt/i.test(s))return{installed:!1,provider:"claude",scope:null,reason:"already_configured"};return e.popmelt={type:"http",url:r},await dt(t,JSON.stringify(n,null,2)+`
74
+ `,"utf-8"),{installed:!0,provider:"claude",scope:"user"}}async function ft(r=pt){let t=process.env.CODEX_HOME||ze(ut(),".codex"),n=ze(t,"config.toml"),e;try{e=await lt(n,"utf-8")}catch(a){e=""}if(/\[mcp_servers\.[^\]]*popmelt[^\]]*\]/i.test(e))return{installed:!1,provider:"codex",scope:null,reason:"already_configured"};await Qt(Vt(n),{recursive:!0});let s=`
75
+ [mcp_servers.popmelt]
76
+ url = "${r}"
77
+ `;return await dt(n,e+s,"utf-8"),{installed:!0,provider:"codex",scope:"user"}}async function me(r){let n=(r.headers["content-type"]||"").match(/boundary=(?:"([^"]+)"|([^\s;]+))/);if(!n)throw new Error("Missing multipart boundary");let e=n[1]||n[2],s=await Kt(r),a=Buffer.from(`--${e}`),i=Buffer.from(`--${e}--`),c,p,u,v,G,y,T,S,F,b,z,V=[],ee=0,q=[];for(;ee<s.length;){let B=s.indexOf(a,ee);if(B===-1)break;let te=B+a.length;if(s.slice(B,B+i.length).equals(i))break;let A=te;s[A]===13&&s[A+1]===10&&(A+=2);let j=s.indexOf(`\r
78
+ \r
79
+ `,A);if(j===-1)break;let P=s.slice(A,j).toString("utf-8"),Y=j+4,H=s.indexOf(a,Y),X=H!==-1?H-2:s.length;q.push({headers:P,body:s.slice(Y,X)}),ee=H!==-1?H:s.length}for(let B of q){let te=B.headers.match(/name="([^"]+)"/);if(!te)continue;let A=te[1];if(A==="screenshot")c=B.body;else if(A==="feedback")p=B.body.toString("utf-8");else if(A==="color")u=B.body.toString("utf-8");else if(A==="provider")v=B.body.toString("utf-8");else if(A==="model")G=B.body.toString("utf-8");else if(A==="goal")y=B.body.toString("utf-8");else if(A==="pageUrl")T=B.body.toString("utf-8");else if(A==="viewport")S=B.body.toString("utf-8");else if(A==="planId")F=B.body.toString("utf-8");else if(A==="manifest")b=B.body.toString("utf-8");else if(A==="tasks")z=B.body.toString("utf-8");else if(A.startsWith("image-")){let j=A.split("-"),P=parseInt(j[j.length-1],10),Y=j.slice(1,-1).join("-");Y&&!isNaN(P)&&V.push({annotationId:Y,index:P,data:B.body})}}if(!c)throw new Error("Missing screenshot field");return p||(p=""),{screenshot:c,feedback:p,color:u,provider:v,model:G,goal:y,pageUrl:T,viewport:S,planId:F,manifest:b,tasks:z,pastedImages:V}}function Kt(r){return new Promise((t,n)=>{let e=[];r.on("data",s=>e.push(s)),r.on("end",()=>t(Buffer.concat(e))),r.on("error",n)})}function Se(r,t){var e,s;let n=[];if(r.annotations.length>0){n.push("## Annotations");for(let a of r.annotations){let i=a.elements.map(u=>{let v=[u.selector];return u.reactComponent&&v.push(`(${u.reactComponent})`),v.join(" ")}).join(", "),c=a.instruction||"No text";n.push(`- id=${a.id} [${a.type}] ${c} \u2192 Elements: ${i||"none"}`);let p=t==null?void 0:t[a.id];if(p&&p.length>0)for(let u of p)n.push(` Attached image: use the Read tool to view ${u}`)}}if(r.styleModifications.length>0){n.push(""),n.push("## Style Changes (make permanent in source)"),n.push("The developer previewed these CSS changes via inline style overrides. Find the corresponding styles in the source files and update them so the changes persist:");for(let a of r.styleModifications){let i=(e=a.element)!=null&&e.reactComponent?`(${a.element.reactComponent})`:"";for(let c of a.changes)n.push(`- ${a.selector} ${i}: ${c.property} ${c.original} \u2192 ${c.modified}`)}}if((s=r.spacingTokenChanges)!=null&&s.length){n.push(""),n.push("## Spacing Token Changes"),n.push("The developer adjusted these spacing tokens. Apply each change to the source code:");for(let a of r.spacingTokenChanges){n.push(`
80
+ ### ${a.tokenName}: ${a.originalPx}px \u2192 ${a.newPx}px`);for(let i of a.affectedElements){let c=i.reactComponent?` (${i.reactComponent})`:"";i.matchedClass&&i.suggestedClass?n.push(`- ${i.selector}${c}: \`${i.matchedClass}\` \u2192 \`${i.suggestedClass}\``):n.push(`- ${i.selector}${c}: ${i.property} ${a.originalPx}px \u2192 ${a.newPx}px`),n.push(` class="${i.className}"`)}}}if(r.inspectedElement){let a=r.inspectedElement;n.push(""),n.push("## Inspected Element"),n.push("The developer has this element selected in the inspector:");let i=[a.selector];a.reactComponent&&i.push(`(${a.reactComponent})`),a.context&&i.push(`in ${a.context}`),a.textContent&&i.push(`"${a.textContent.slice(0,80)}"`),n.push(`- ${i.join(" ")}`)}return n.join(`
81
+ `)}function mt(r,t,n){var a;let e=[];if(e.push("You are reviewing a UI screenshot with developer annotations."),e.push(""),(n==null?void 0:n.provider)!=="codex"&&(e.push(`IMPORTANT: First, use the Read tool to view the screenshot at: ${r}`),e.push("")),e.push(`The developer annotated their running app at ${t.url} (${t.viewport.width}x${t.viewport.height}).`),n!=null&&n.threadHistory&&n.threadHistory.length>0){e.push(""),e.push("## Previous Conversation");let i=0;for(let c of n.threadHistory)if(c.role==="human")i++,c.replyToQuestion?(e.push(`### Round ${i} (human) \u2014 reply`),e.push(`"${c.replyToQuestion}"`)):(e.push(`### Round ${i} (human)`),c.feedbackSummary&&e.push(`Annotations: ${c.feedbackSummary}`),c.annotationIds&&c.annotationIds.length>0&&e.push(`Annotation IDs: ${c.annotationIds.join(", ")}`));else if(c.question)e.push(`### Round ${i} (assistant) \u2014 question`),e.push(`"${c.question}"`);else{if(e.push(`### Round ${i} (assistant)`),c.responseText&&e.push(`Response: ${c.responseText}`),c.resolutions&&c.resolutions.length>0)for(let p of c.resolutions){let u=(a=p.finalScope)!=null?a:p.inferredScope,v=u?` [${u.breadth} ${u.target}]`:"";e.push(`- ${p.annotationId}: ${p.status}${v} \u2014 ${p.summary}`),p.filesModified&&p.filesModified.length>0&&e.push(` Files: ${p.filesModified.join(", ")}`)}c.toolsUsed&&c.toolsUsed.length>0&&e.push(`Tools used: ${c.toolsUsed.join(", ")}`)}e.push(""),e.push("The current round is shown in full below.")}if(n!=null&&n.designModel){e.push(""),e.push("## Established Design Policies"),e.push("This project has an established design model (stored in .popmelt/model.json), extracted from the developer's previous design decisions. When making changes, follow these patterns unless the developer explicitly overrides them. When asked about design tokens, component patterns, or design decisions, reference this model as the authoritative source.");let i=n.designModel.rules;if(Array.isArray(i)&&i.length>0){e.push(""),e.push("Rules:");for(let u of i)typeof u=="string"&&e.push(`- ${u}`)}let c=n.designModel.tokens;c&&typeof c=="object"&&(e.push(""),e.push("Design tokens:"),e.push("```json"),e.push(JSON.stringify(c,null,2)),e.push("```"));let p=n.designModel.components;p&&typeof p=="object"&&(e.push(""),e.push("Component patterns:"),e.push("```json"),e.push(JSON.stringify(p,null,2)),e.push("```")),e.push(""),e.push("### Novel Pattern Detection"),e.push("When you make a design decision that has no matching policy in the model above (e.g., styling a component type not yet in the model, choosing a color with no token, picking spacing with no rule), flag it:"),e.push("<novel>"),e.push('[{"category":"component","element":"button","decision":"Used 8px border-radius, 12px 24px padding","reason":"No button pattern in design model"}]'),e.push("</novel>"),e.push('- `category`: "token" (color, spacing, typography), "component" (UI component pattern), or "element" (specific element style)'),e.push("- `element`: What you are styling or creating"),e.push("- `decision`: What you decided to do (specific values)"),e.push("- `reason`: Why this is novel (what is missing from the model)"),e.push("Still do the work \u2014 just flag it so the developer can review and set policy.")}n!=null&&n.designModel||(e.push(""),e.push("## Design Context"),e.push("This project uses Popmelt for design governance. Design decisions are stored in .popmelt/decisions/ (JSON files). A materialized design model may exist at .popmelt/model.json. When the developer asks about design tokens, patterns, or past decisions, check these files first before searching source code."));let s=Se(t,n==null?void 0:n.imagePaths);return s&&(e.push(""),e.push(s)),e.push(""),e.push("Follow the developer's instructions. If they ask for changes, apply them to the source files \u2014 the dev server has HMR so changes appear immediately. If they ask a question or request analysis, respond in text without modifying code."),e.push(""),e.push("IMPORTANT: If any elements you modify have a `data-pm` attribute, preserve it in the source. This attribute tracks annotation positions."),e.push(""),e.push("## Resolution"),e.push("After completing all work, output a resolution block listing what you did for each annotation:"),e.push("<resolution>"),e.push('[{"annotationId":"<id>","status":"resolved","summary":"<what you did>","filesModified":["<file>"],"declaredScope":{"breadth":"...","target":"..."},"inferredScope":{"breadth":"...","target":"..."}}]'),e.push("</resolution>"),e.push(`Use status "resolved" when the change is complete, or "needs_review" if you're unsure about the result.`),e.push(""),e.push("### Scope classification"),e.push("Each resolution MUST include scope fields:"),e.push("- `declaredScope`: What scope the user's instruction text implies. null if no signal."),e.push("- `inferredScope`: What scope the change actually has, based on what you modified."),e.push("Scope has two dimensions:"),e.push('- `breadth`: "instance" (just this occurrence) or "pattern" (all similar occurrences)'),e.push('- `target`: "element" (a specific DOM element), "component" (a React/UI component), or "token" (a design token \u2014 color, spacing, typography)'),e.push('Note: "instance" + "token" is invalid \u2014 tokens are inherently patterns.'),e.push("If you cannot confidently determine scope, set it to null."),e.push(""),e.push("## Questions"),e.push("If the annotation text is unclear, ambiguous, gibberish, or you are unsure what the developer wants, output a question:"),e.push('<question>What do you mean by "..."?</question>'),e.push("Do NOT guess what unclear instructions mean \u2014 ask instead."),e.push("You may output BOTH a <resolution> for clear annotations AND a <question> for unclear ones in the same response."),e.join(`
82
+ `)}function gt(r){var n;let t=r.match(/<question>\s*([\s\S]*?)\s*<\/question>/);return(n=t==null?void 0:t[1])!=null?n:null}function yt(r,t,n,e){let s=[];s.push("You are continuing work on a UI based on the developer's reply to your question."),s.push(""),n!=="codex"&&s.push(`IMPORTANT: First, use the Read tool to view the screenshot at: ${r}`);let a=t.find(i=>i.role==="human"&&i.feedbackContext);if(a!=null&&a.feedbackContext&&(s.push(""),s.push(a.feedbackContext)),t.length>0){s.push(""),s.push("## Conversation History");let i=0;for(let c of t)c.role==="human"?(i++,c.replyToQuestion?(s.push(`### Round ${i} (human) \u2014 reply`),s.push(`"${c.replyToQuestion}"`)):(s.push(`### Round ${i} (human)`),c.feedbackSummary&&s.push(`Annotations: ${c.feedbackSummary}`))):c.question?(s.push(`### Round ${i} (assistant) \u2014 question`),s.push(`"${c.question}"`)):(s.push(`### Round ${i} (assistant)`),c.responseText&&s.push(`Response: ${c.responseText}`))}if(s.push(""),s.push("The developer answered your question. Continue working based on their reply."),s.push("Follow their instructions \u2014 apply code changes only if requested. The dev server has HMR so changes appear immediately."),s.push(""),s.push("IMPORTANT: If any elements you modify have a `data-pm` attribute, preserve it in the source. This attribute tracks annotation positions."),e&&e.length>0){s.push(""),s.push("## Attached Images"),s.push("The developer attached reference images with their reply:");for(let i of e)s.push(`Attached image: use the Read tool to view the image at: ${i}`)}return s.push(""),s.push("## Resolution"),s.push("After completing all work, output a resolution block listing what you did for each annotation:"),s.push("<resolution>"),s.push('[{"annotationId":"<id>","status":"resolved","summary":"<what you did>","filesModified":["<file>"],"declaredScope":{"breadth":"...","target":"..."},"inferredScope":{"breadth":"...","target":"..."}}]'),s.push("</resolution>"),s.push(`Use status "resolved" when the change is complete, or "needs_review" if you're unsure about the result.`),s.push(""),s.push("### Scope classification"),s.push("Each resolution MUST include scope fields:"),s.push("- `declaredScope`: What scope the user's instruction text implies. null if no signal."),s.push("- `inferredScope`: What scope the change actually has, based on what you modified."),s.push("Scope has two dimensions:"),s.push('- `breadth`: "instance" (just this occurrence) or "pattern" (all similar occurrences)'),s.push('- `target`: "element" (a specific DOM element), "component" (a React/UI component), or "token" (a design token \u2014 color, spacing, typography)'),s.push('Note: "instance" + "token" is invalid \u2014 tokens are inherently patterns.'),s.push("If you cannot confidently determine scope, set it to null."),s.push('If the developer\'s reply corrects a prior scope classification (e.g., "this should apply everywhere" or "only fix this one"), set `finalScope` on your resolution to reflect their correction and apply the change at the corrected scope.'),s.push(""),s.push("## Questions"),s.push("If you still need clarification, output:"),s.push("<question>Your question here</question>"),s.push("You may output BOTH a <resolution> and a <question> in the same response."),s.join(`
83
+ `)}function Xt(r){if(typeof r!="object"||r===null)return!1;let t=r;return(t.breadth==="instance"||t.breadth==="pattern")&&(t.target==="element"||t.target==="component"||t.target==="token")}function wt(r){if(typeof r!="object"||r===null||typeof r.annotationId!="string"||r.status!=="resolved"&&r.status!=="needs_review"||typeof r.summary!="string")return!1;let t=r;for(let n of["declaredScope","inferredScope","finalScope"])if(t[n]!==void 0&&t[n]!==null&&!Xt(t[n]))return!1;return!0}function vt(r){let t=r.match(/<resolution>\s*([\s\S]*?)\s*<\/resolution>/);if(!t||!t[1])return[];try{let n=JSON.parse(t[1]);return Array.isArray(n)?n.filter(wt):[]}catch(n){return[]}}function St(r){let t=[],n=/<resolution>\s*([\s\S]*?)\s*<\/resolution>/g,e;for(;(e=n.exec(r))!==null;)if(e[1])try{let s=JSON.parse(e[1]);Array.isArray(s)&&t.push(...s.filter(wt))}catch(s){}return t}function xt(r){let t=r.match(/<novel>\s*([\s\S]*?)\s*<\/novel>/);if(!(t!=null&&t[1]))return[];try{let n=JSON.parse(t[1]);return Array.isArray(n)?n.filter(e=>{if(typeof e!="object"||e===null)return!1;let s=e;return(s.category==="token"||s.category==="component"||s.category==="element")&&typeof s.element=="string"&&typeof s.decision=="string"&&typeof s.reason=="string"}):[]}catch(n){return[]}}function kt(r,t,n,e,s,a){let i=[];return i.push("You are a UI design planner. You are looking at a full-page screenshot of a web application."),i.push(""),i.push(`IMPORTANT: First, use the Read tool to view the screenshot at: ${r}`),i.push(""),i.push(`Page: ${n}`),i.push(`Viewport: ${e.width}x${e.height}`),s&&(i.push(""),i.push("## Page Elements (ground truth)"),i.push("Below is a structured inventory of actual DOM elements on this page. Cross-reference"),i.push("against this list \u2014 do NOT reference elements that aren't listed here."),i.push(""),i.push("<manifest>"),i.push(s),i.push("</manifest>")),a&&(i.push(""),i.push("## Developer Context"),i.push("The developer has the following annotations and style changes on their canvas. Factor these into your plan:"),i.push(a)),i.push(""),i.push("## Goal"),i.push(t),i.push(""),i.push("## Your Task"),i.push("Analyze the screenshot and decompose the goal into specific, element-level tasks."),i.push("Each task targets a specific region of the page and gives a clear instruction for a worker agent."),i.push(""),i.push("Output your plan as a JSON array inside a <plan> tag. Each task has:"),i.push('- `id`: A short unique identifier (e.g., "t1", "t2")'),i.push("- `instruction`: Clear, specific instruction for a worker agent (what to change and how)"),i.push("- `region`: Bounding box in page coordinates `{x, y, width, height}` \u2014 where (x,y) is top-left corner"),i.push("- `priority`: Optional 1-5 (1=highest). Tasks with no dependency can share a priority level."),i.push(""),i.push("Example:"),i.push("<plan>"),i.push("["),i.push(' {"id":"t1","instruction":"Increase heading font-size to 48px and change font-weight to 700","region":{"x":100,"y":50,"width":600,"height":80},"priority":1},'),i.push(' {"id":"t2","instruction":"Add a subtle box-shadow to the card container","region":{"x":80,"y":200,"width":640,"height":300},"priority":2}'),i.push("]"),i.push("</plan>"),i.push(""),i.push("Guidelines:"),i.push("- CRITICAL: Cross-check all element references against the <manifest>. Only reference elements that actually exist. Use the manifest's text content, component names, and bounding rects for precise instructions."),i.push('- Be specific about values (colors, sizes, spacing) rather than vague ("make it look better")'),i.push("- Each task should be independently actionable by a worker that can only see its region"),i.push("- Regions should tightly bound the relevant UI element(s)"),i.push("- Keep tasks atomic \u2014 one change per task, not multiple unrelated changes"),i.push("- Order by priority: structural changes first, then visual polish"),i.push("- If the goal can be accomplished as a single change, return a plan with just one task. Only decompose when the goal genuinely requires multiple independent changes."),i.push("- If the goal is unclear or you need more context, output a question instead:"),i.push("<question>Your question here</question>"),i.push(""),i.push("Do NOT modify any files. You are a planner only \u2014 output a <plan> or <question>, nothing else."),i.join(`
84
+ `)}function It(r){let t=r.match(/<plan>\s*([\s\S]*?)\s*<\/plan>/);if(!(t!=null&&t[1]))return null;try{let n=JSON.parse(t[1]);return Array.isArray(n)?n.filter(e=>{if(typeof e!="object"||e===null)return!1;let s=e;if(typeof s.id!="string"||typeof s.instruction!="string"||typeof s.region!="object"||s.region===null)return!1;let a=s.region;return typeof a.x=="number"&&typeof a.y=="number"&&typeof a.width=="number"&&typeof a.height=="number"}):null}catch(n){return null}}function bt(r,t,n){let e=[];e.push("You are reviewing whether a series of UI changes achieved the original design goal."),e.push(""),e.push(`IMPORTANT: First, use the Read tool to view the screenshot at: ${r}`),e.push(""),e.push("## Original Goal"),e.push(t),e.push(""),e.push("## Completed Tasks");for(let s of n)e.push(`- [${s.id}] ${s.instruction} \u2192 ${s.summary}`);return e.push(""),e.push("## Your Task"),e.push("Look at the current screenshot and determine if the goal has been achieved."),e.push("Output your verdict inside a <review> tag:"),e.push("<review>"),e.push('{"verdict":"pass","summary":"The changes look good..."}'),e.push("</review>"),e.push(""),e.push("Or if issues remain:"),e.push("<review>"),e.push('{"verdict":"fail","summary":"Some issues remain...","issues":["Issue 1","Issue 2"]}'),e.push("</review>"),e.push(""),e.push("Do NOT modify any files. Output only a <review> block."),e.join(`
85
+ `)}function Pt(r,t,n,e,s){let a=[];a.push("You are implementing a series of UI changes on a web application."),a.push(""),s!=="codex"&&(a.push(`IMPORTANT: First, use the Read tool to view the screenshot at: ${r}`),a.push("")),a.push(`Page: ${n} (${e.width}x${e.height})`),a.push(""),a.push("## Tasks"),a.push("Each task targets a specific region of the page. Complete them in order."),a.push("");for(let i of t){if(a.push(`### Task ${i.planTaskId} (annotationId: ${i.annotationId})`),a.push(`Instruction: ${i.instruction}`),a.push(`Region: (${i.region.x}, ${i.region.y}) ${i.region.width}x${i.region.height}`),i.linkedSelector&&a.push(`Target element: ${i.linkedSelector}`),i.elements&&i.elements.length>0){let c=i.elements.map(p=>{let u=[p.selector];return p.reactComponent&&u.push(`(${p.reactComponent})`),u.join(" ")}).join(", ");a.push(`Elements: ${c}`)}a.push("")}return a.push("## Instructions"),a.push("- Apply each change to the source files \u2014 the dev server has HMR so changes appear immediately."),a.push("- IMPORTANT: If any elements you modify have a `data-pm` attribute, preserve it in the source."),a.push("- You may use parallel subagents (Task tool) for independent changes, or work serially \u2014 use your judgment."),a.push(""),a.push("## Resolution"),a.push("CRITICAL: After completing EACH task, immediately output a <resolution> block for that task."),a.push("Do NOT wait until all tasks are done \u2014 output each resolution as soon as that task is finished."),a.push("<resolution>"),a.push('[{"annotationId":"<annotationId>","status":"resolved","summary":"<what you did>","filesModified":["<file>"],"declaredScope":{"breadth":"...","target":"..."},"inferredScope":{"breadth":"...","target":"..."}}]'),a.push("</resolution>"),a.push(`Use status "resolved" when the change is complete, or "needs_review" if you're unsure about the result.`),a.push(""),a.push("### Scope classification"),a.push("Each resolution MUST include scope fields:"),a.push("- `declaredScope`: What scope the task instruction implies. null if no signal."),a.push("- `inferredScope`: What scope the change actually has, based on what you modified."),a.push("Scope has two dimensions:"),a.push('- `breadth`: "instance" (just this occurrence) or "pattern" (all similar occurrences)'),a.push('- `target`: "element" (a specific DOM element), "component" (a React/UI component), or "token" (a design token \u2014 color, spacing, typography)'),a.push('Note: "instance" + "token" is invalid \u2014 tokens are inherently patterns.'),a.push("If you cannot confidently determine scope, set it to null."),a.join(`
86
+ `)}function Tt(r){let t=r.match(/<review>\s*([\s\S]*?)\s*<\/review>/);if(!(t!=null&&t[1]))return null;try{let n=JSON.parse(t[1]);return typeof n!="object"||n===null||n.verdict!=="pass"&&n.verdict!=="fail"||typeof n.summary!="string"?null:{verdict:n.verdict,summary:n.summary,issues:Array.isArray(n.issues)?n.issues.filter(e=>typeof e=="string"):void 0}}catch(n){return null}}var Ce=class{constructor(t=5){this.queue=[];this.activeJobs=new Map;this.activeProcesses=new Map;this.listeners=new Set;this.processor=null;this.maxConcurrency=t}setProcessor(t){this.processor=t}get active(){let t=this.activeJobs.values().next();return t.done?null:t.value}get allActive(){return Array.from(this.activeJobs.values())}get activeCount(){return this.activeJobs.size}get depth(){return this.queue.length}get isRunning(){return this.activeJobs.size>0}setActiveProcess(t,n){n?this.activeProcesses.set(t,n):this.activeProcesses.delete(t)}enqueue(t){return this.queue.push(t),this.processNext(),this.queue.length+this.activeJobs.size}addListener(t){return this.listeners.add(t),()=>this.listeners.delete(t)}broadcast(t,n){for(let e of this.listeners)e(t,n)}cancelJob(t){let n=this.activeProcesses.get(t),e=this.activeJobs.get(t);return!n||!e?!1:(n.kill("SIGTERM"),this.activeProcesses.delete(t),this.activeJobs.delete(t),e.status="error",e.error="Cancelled by user",this.broadcast({type:"error",jobId:e.id,message:"Cancelled by user"},e.id),this.processNext(),!0)}cancelActive(){if(this.activeJobs.size===0)return!1;let t=Array.from(this.activeJobs.keys());for(let n of t)this.cancelJob(n);return!0}destroy(){for(let t of this.activeProcesses.values())t.kill("SIGTERM");this.activeProcesses.clear(),this.activeJobs.clear(),this.queue=[],this.listeners.clear()}processNext(){for(;this.activeJobs.size<this.maxConcurrency&&this.queue.length>0&&this.processor;){let t=this.queue.shift();this.activeJobs.set(t.id,t),t.status="running",this.broadcast({type:"job_started",jobId:t.id,position:0},t.id),this.processor(t).catch(n=>{t.status="error",t.error=n instanceof Error?n.message:String(n),this.broadcast({type:"error",jobId:t.id,message:t.error},t.id)}).finally(()=>{this.activeJobs.delete(t.id),this.activeProcesses.delete(t.id),this.processNext(),this.activeJobs.size===0&&this.queue.length===0&&this.broadcast({type:"queue_drained"},t.id)})}}};import{mkdir as Zt,readFile as en,writeFile as tn}from"fs/promises";import{dirname as nn,join as sn}from"path";var on={version:1,threads:{}},Ee=class{constructor(t){this.cache=null;this.writeChain=Promise.resolve();this.filePath=sn(t,".popmelt","threads.json")}async load(){if(this.cache)return this.cache;try{let t=await en(this.filePath,"utf-8"),n=JSON.parse(t);if(n&&n.version===1&&n.threads)return this.cache=n,this.cache}catch(t){}return this.cache=ae(Z({},on),{threads:{}}),this.cache}async getThread(t){var e;return(e=(await this.load()).threads[t])!=null?e:null}async findContinuationThread(t){if(t.length===0)return null;let n=await this.load(),e=new Set(t);for(let s of Object.values(n.threads))if(s.elementIdentifiers.some(i=>e.has(i)))return s;return null}async createThread(t,n){let e=await this.load(),s={id:t,createdAt:Date.now(),updatedAt:Date.now(),elementIdentifiers:n,messages:[]};return e.threads[t]=s,await this.persist(),s}async appendMessage(t,n){let s=(await this.load()).threads[t];s&&(s.messages.push(n),s.updatedAt=Date.now(),await this.persist())}async addElementIdentifiers(t,n){let s=(await this.load()).threads[t];if(!s)return;let a=new Set(s.elementIdentifiers);for(let i of n)a.has(i)||s.elementIdentifiers.push(i);s.updatedAt=Date.now(),await this.persist()}async getThreadHistory(t,n=6){let e=await this.getThread(t);return!e||e.messages.length===0?[]:e.messages.length<=n?e.messages:[e.messages[0],...e.messages.slice(-(n-1))]}async persist(){this.writeChain=this.writeChain.then(async()=>{if(this.cache)try{await Zt(nn(this.filePath),{recursive:!0}),await tn(this.filePath,JSON.stringify(this.cache,null,2))}catch(t){console.error("[ThreadStore] Failed to persist:",t)}}),await this.writeChain}};var hn=1111,fn=["Read","Edit","Write","Glob","Grep","Bash","WebFetch","WebSearch","Bash(curl:*)"],mn=1800*1e3,gn=3600*1e3;function yn(r){if(!r)return!1;try{let t=new URL(r);return t.hostname==="localhost"||t.hostname==="127.0.0.1"}catch(t){return!1}}function wn(r,t){let n=r.headers.origin;yn(n)&&(t.setHeader("Access-Control-Allow-Origin",n),t.setHeader("Access-Control-Allow-Methods","GET, POST, PATCH, DELETE, OPTIONS"),t.setHeader("Access-Control-Allow-Headers","Content-Type"))}function g(r,t,n){r.writeHead(t,{"Content-Type":"application/json"}),r.end(JSON.stringify(n))}function vn(r,t){if(!r)return t;let n=r.match(/^#?([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i);if(!n)return t;let[,e,s,a]=n;return`\x1B[38;2;${parseInt(e,16)};${parseInt(s,16)};${parseInt(a,16)}m${t}\x1B[0m`}function Rt(r,t){try{r.res.write(`event: ${t.type}
87
+ data: ${JSON.stringify(t)}
1051
88
 
1052
- `);
1053
- } catch (e) {
1054
- }
1055
- }
1056
- async function createPopmelt(options = {}) {
1057
- var _a, _b, _c, _d, _e, _f, _g, _h;
1058
- const port = (_a = options.port) != null ? _a : DEFAULT_PORT;
1059
- const projectRoot = (_b = options.projectRoot) != null ? _b : process.cwd();
1060
- const tempDir = (_c = options.tempDir) != null ? _c : join2(tmpdir(), "popmelt-bridge");
1061
- const maxTurns = (_d = options.maxTurns) != null ? _d : 10;
1062
- const maxBudgetUsd = (_e = options.maxBudgetUsd) != null ? _e : 1;
1063
- const allowedTools = (_f = options.allowedTools) != null ? _f : DEFAULT_ALLOWED_TOOLS;
1064
- const claudePath = (_g = options.claudePath) != null ? _g : "claude";
1065
- const defaultProvider = (_h = options.provider) != null ? _h : "claude";
1066
- const capabilities = {};
1067
- for (const cli of ["claude", "codex"]) {
1068
- try {
1069
- const cliPath = execFileSync("which", [cli], { encoding: "utf-8" }).trim();
1070
- capabilities[cli] = { available: true, path: cliPath };
1071
- } catch (e) {
1072
- capabilities[cli] = { available: false, path: null };
1073
- }
1074
- }
1075
- await mkdir2(tempDir, { recursive: true });
1076
- cleanupTempDir(tempDir).catch(() => {
1077
- });
1078
- const queue = new JobQueue();
1079
- const sseClients = /* @__PURE__ */ new Set();
1080
- const threadStore = new ThreadFileStore(projectRoot);
1081
- const jobGroups = /* @__PURE__ */ new Map();
1082
- queue.addListener((event, _jobId) => {
1083
- for (const client of sseClients) {
1084
- sendSSE(client, event);
1085
- }
1086
- });
1087
- queue.setProcessor(async (job) => {
1088
- var _a2, _b2, _c2, _d2, _e2, _f2, _g2;
1089
- const replyPrompt = job._replyPrompt;
1090
- const provider = (_a2 = job.provider) != null ? _a2 : defaultProvider;
1091
- let resumeSessionId;
1092
- if (job.threadId) {
1093
- const thread = await threadStore.getThread(job.threadId);
1094
- if (thread) {
1095
- for (let i = thread.messages.length - 1; i >= 0; i--) {
1096
- if (thread.messages[i].sessionId) {
1097
- resumeSessionId = thread.messages[i].sessionId;
1098
- break;
1099
- }
1100
- }
1101
- }
1102
- }
1103
- let prompt;
1104
- if (resumeSessionId && replyPrompt) {
1105
- const lastReply = (_b2 = await threadStore.getThread(job.threadId)) == null ? void 0 : _b2.messages.filter((m) => m.role === "human").pop();
1106
- const replyText = (lastReply == null ? void 0 : lastReply.replyToQuestion) || (lastReply == null ? void 0 : lastReply.feedbackSummary) || "";
1107
- prompt = replyText + "\n\nAfter completing work, output a <resolution> block. If unclear, output a <question> block.";
1108
- } else if (resumeSessionId) {
1109
- prompt = formatFeedbackContext(job.feedback, job.imagePaths) + "\n\nFollow the developer's instructions. If they ask for changes, apply them to the source files.\n\nAfter completing work, output a <resolution> block. If unclear, output a <question> block." + (provider !== "codex" ? `
89
+ `)}catch(n){}}async function Mt(r={}){var ye,je,He,Le,We,Ge,qe,Ye;let t=(ye=r.port)!=null?ye:hn,n=(je=r.projectRoot)!=null?je:process.cwd(),e=(He=r.tempDir)!=null?He:le(pn(),"popmelt-bridge"),s=(Le=r.maxTurns)!=null?Le:10,a=(We=r.maxBudgetUsd)!=null?We:1,i=(Ge=r.allowedTools)!=null?Ge:fn,c=(qe=r.claudePath)!=null?qe:"claude",p=(Ye=r.provider)!=null?Ye:"claude",u={};for(let o of["claude","codex"])try{let l=rn("which",[o],{encoding:"utf-8"}).trim();u[o]={available:!0,path:l}}catch(l){u[o]={available:!1,path:null}}let[v,G]=await Promise.all([Ue(n),Fe(n)]);u.claude&&(u.claude.mcp=v),u.codex&&(u.codex.mcp=G),await an(e,{recursive:!0}),$t(e).catch(()=>{});let y=new Ce(1),T=new Set,S=new Ee(n),F=new $e(n),b=new Me(n,F,{claudePath:c,onEvent:o=>{for(let l of T)Rt(l,o)}}),z=new Map;y.addListener((o,l)=>{for(let h of T)Rt(h,o)}),y.setProcessor(async o=>{var W,K,se,C,ne,ie,we;let l=o._replyPrompt,h=(W=o.provider)!=null?W:p,d;if(o.threadId){let I=await S.getThread(o.threadId);if(I){for(let E=I.messages.length-1;E>=0;E--)if(I.messages[E].sessionId){d=I.messages[E].sessionId;break}}}let m;if(d&&l){let I=(K=await S.getThread(o.threadId))==null?void 0:K.messages.filter(M=>M.role==="human").pop();m=((I==null?void 0:I.replyToQuestion)||(I==null?void 0:I.feedbackSummary)||"")+`
1110
90
 
1111
- IMPORTANT: First, use the Read tool to view the updated screenshot at: ${job.screenshotPath}` : "");
1112
- } else {
1113
- const threadHistory = !replyPrompt && job.threadId ? await threadStore.getThreadHistory(job.threadId) : void 0;
1114
- prompt = replyPrompt != null ? replyPrompt : buildPrompt(job.screenshotPath, job.feedback, {
1115
- threadHistory: threadHistory && threadHistory.length > 0 ? threadHistory : void 0,
1116
- provider,
1117
- imagePaths: job.imagePaths
1118
- });
1119
- }
1120
- const tag = ansiColor(job.color, `[\u22B9 ${port}:${job.id}]`);
1121
- console.log(`${tag} Reviewing feedback ${job.screenshotPath} (provider: ${provider})${job.threadId ? ` (thread: ${job.threadId})` : ""}${resumeSessionId ? ` (resuming: ${resumeSessionId.slice(0, 8)})` : ""}`);
1122
- const isPlanExecutor = !!job._isPlanExecutor;
1123
- let deltaBuffer = "";
1124
- let lastResolutionCount = 0;
1125
- const onEvent = (event, jobId) => {
1126
- queue.broadcast(event, jobId);
1127
- if (isPlanExecutor && event.type === "delta" && "text" in event) {
1128
- deltaBuffer += event.text;
1129
- const resolutions = parseAllResolutions(deltaBuffer);
1130
- if (resolutions.length > lastResolutionCount) {
1131
- const newResolutions = resolutions.slice(lastResolutionCount);
1132
- lastResolutionCount = resolutions.length;
1133
- queue.broadcast(
1134
- { type: "task_resolved", jobId, planId: job.planId, resolutions: newResolutions, threadId: job.threadId },
1135
- jobId
1136
- );
1137
- }
1138
- }
1139
- };
1140
- const jobAllowedTools = (_c2 = job._allowedTools) != null ? _c2 : allowedTools;
1141
- const { process: proc, result } = provider === "codex" ? spawnCodex(job.id, {
1142
- prompt,
1143
- projectRoot,
1144
- screenshotPath: job.screenshotPath,
1145
- resumeSessionId,
1146
- model: job.model,
1147
- onEvent
1148
- }) : spawnClaude(job.id, {
1149
- prompt,
1150
- projectRoot,
1151
- maxTurns,
1152
- maxBudgetUsd,
1153
- allowedTools: jobAllowedTools,
1154
- claudePath,
1155
- resumeSessionId,
1156
- model: job.model,
1157
- onEvent
1158
- });
1159
- queue.setActiveProcess(job.id, proc);
1160
- const spawnResult = await result;
1161
- job.result = spawnResult.text;
1162
- if (spawnResult.success) {
1163
- console.log(`${tag} Iteration complete`);
1164
- if (spawnResult.fileEdits && spawnResult.fileEdits.length > 0) {
1165
- console.log(`${tag} Captured ${spawnResult.fileEdits.length} file edit(s): ${spawnResult.fileEdits.map((e) => `${e.tool} ${e.file_path}`).join(", ")}`);
1166
- }
1167
- job.status = "done";
1168
- const question = parseQuestion(spawnResult.text);
1169
- let resolutions = parseResolutions(spawnResult.text);
1170
- if (resolutions.length > 0 && job.annotationIds && job.annotationIds.length > 0) {
1171
- const realIdSet = new Set(job.annotationIds);
1172
- const allMatch = resolutions.every((r) => realIdSet.has(r.annotationId));
1173
- if (!allMatch) {
1174
- resolutions = resolutions.map((r, i) => __spreadProps(__spreadValues({}, r), {
1175
- annotationId: job.annotationIds[i % job.annotationIds.length]
1176
- }));
1177
- }
1178
- }
1179
- const toolsUsed = spawnResult.fileEdits && spawnResult.fileEdits.length > 0 ? spawnResult.fileEdits.map((e) => `${e.tool} ${e.file_path.split("/").pop()}`) : void 0;
1180
- if (job.threadId) {
1181
- await threadStore.appendMessage(job.threadId, {
1182
- role: "assistant",
1183
- timestamp: Date.now(),
1184
- jobId: job.id,
1185
- responseText: spawnResult.text,
1186
- resolutions: resolutions.length > 0 ? resolutions : void 0,
1187
- question: question != null ? question : void 0,
1188
- sessionId: spawnResult.sessionId,
1189
- toolsUsed
1190
- });
1191
- }
1192
- if (job.planId && !job.planTaskId) {
1193
- const group = jobGroups.get(job.planId);
1194
- if (group) {
1195
- const plan = parsePlan(spawnResult.text);
1196
- if (plan && plan.length > 0) {
1197
- group.plan = plan;
1198
- group.status = "awaiting_approval";
1199
- group.plannerThreadId = job.threadId;
1200
- console.log(`${tag} Plan ready: ${plan.length} tasks for group ${job.planId}`);
1201
- queue.broadcast(
1202
- { type: "plan_ready", jobId: job.id, planId: job.planId, tasks: plan, threadId: job.threadId },
1203
- job.id
1204
- );
1205
- } else if (!question) {
1206
- group.status = "error";
1207
- console.error(`${tag} Failed to parse plan from planner response`);
1208
- }
1209
- }
1210
- }
1211
- if (job.planId && job._isReview) {
1212
- const group = jobGroups.get(job.planId);
1213
- if (group) {
1214
- const review = parseReview(spawnResult.text);
1215
- if (review) {
1216
- group.status = review.verdict === "pass" ? "done" : "executing";
1217
- console.log(`${tag} Review verdict: ${review.verdict} \u2014 ${review.summary}`);
1218
- queue.broadcast(
1219
- { type: "plan_review", planId: job.planId, verdict: review.verdict, summary: review.summary, issues: review.issues },
1220
- job.id
1221
- );
1222
- }
1223
- }
1224
- }
1225
- if (question) {
1226
- console.log(`${tag} \u{1F4AC} Question detected: "${question.slice(0, 120)}" \u2192 broadcasting to ${sseClients.size} SSE clients (threadId=${(_d2 = job.threadId) != null ? _d2 : job.id}, annotationIds=${(_f2 = (_e2 = job.annotationIds) == null ? void 0 : _e2.join(",")) != null ? _f2 : "none"})`);
1227
- queue.broadcast(
1228
- { type: "question", jobId: job.id, threadId: (_g2 = job.threadId) != null ? _g2 : job.id, question, annotationIds: job.annotationIds },
1229
- job.id
1230
- );
1231
- }
1232
- queue.broadcast(
1233
- { type: "done", jobId: job.id, success: true, resolutions: resolutions.length > 0 ? resolutions : void 0, responseText: spawnResult.text, threadId: job.threadId },
1234
- job.id
1235
- );
1236
- } else {
1237
- console.error(`${tag} Error: ${spawnResult.error}`);
1238
- job.status = "error";
1239
- job.error = spawnResult.error;
1240
- queue.broadcast(
1241
- {
1242
- type: "error",
1243
- jobId: job.id,
1244
- message: spawnResult.error || "Unknown error"
1245
- },
1246
- job.id
1247
- );
1248
- }
1249
- });
1250
- const server = createServer(async (req, res) => {
1251
- setCors(req, res);
1252
- if (req.method === "OPTIONS") {
1253
- res.writeHead(204);
1254
- res.end();
1255
- return;
1256
- }
1257
- const url = new URL(req.url || "/", `http://127.0.0.1:${port}`);
1258
- const path = url.pathname;
1259
- try {
1260
- if (req.method === "POST" && path === "/send") {
1261
- await handleSend(req, res);
1262
- } else if (req.method === "GET" && path === "/events") {
1263
- handleEvents(req, res);
1264
- } else if (req.method === "GET" && path === "/status") {
1265
- handleStatus(res);
1266
- } else if (req.method === "GET" && path === "/capabilities") {
1267
- sendJson(res, 200, { providers: capabilities });
1268
- } else if (req.method === "POST" && path === "/reply") {
1269
- await handleReply(req, res);
1270
- } else if (req.method === "POST" && path === "/cancel") {
1271
- handleCancel(req, res);
1272
- } else if (req.method === "POST" && path === "/plan") {
1273
- await handlePlan(req, res);
1274
- } else if (req.method === "POST" && path === "/plan/approve") {
1275
- await handlePlanApprove(req, res);
1276
- } else if (req.method === "POST" && path === "/plan/execute") {
1277
- await handlePlanExecute(req, res);
1278
- } else if (req.method === "POST" && path === "/plan/review") {
1279
- await handlePlanReview(req, res);
1280
- } else if (req.method === "GET" && path.startsWith("/plan/")) {
1281
- handleGetPlan(path.slice("/plan/".length), res);
1282
- } else if (req.method === "GET" && path.startsWith("/thread/")) {
1283
- const threadId = path.slice("/thread/".length);
1284
- await handleGetThread(threadId, res);
1285
- } else {
1286
- sendJson(res, 404, { error: "Not found" });
1287
- }
1288
- } catch (err) {
1289
- console.error("[Bridge] Request error:", err);
1290
- sendJson(res, 500, {
1291
- error: err instanceof Error ? err.message : "Internal error"
1292
- });
1293
- }
1294
- });
1295
- async function handleSend(req, res) {
1296
- const { screenshot, feedback: feedbackStr, color, provider: providerStr, model: modelStr, pastedImages } = await parseMultipart(req);
1297
- let feedback;
1298
- try {
1299
- feedback = JSON.parse(feedbackStr);
1300
- } catch (e) {
1301
- sendJson(res, 400, { error: "Invalid feedback JSON" });
1302
- return;
1303
- }
1304
- const jobId = randomUUID().slice(0, 8);
1305
- const screenshotPath = join2(tempDir, `screenshot-${jobId}.png`);
1306
- await writeFile2(screenshotPath, screenshot);
1307
- const imagePaths = {};
1308
- if (pastedImages.length > 0) {
1309
- for (const img of pastedImages) {
1310
- const imgPath = join2(tempDir, `pasted-${jobId}-${img.annotationId}-${img.index}.png`);
1311
- await writeFile2(imgPath, img.data);
1312
- if (!imagePaths[img.annotationId]) imagePaths[img.annotationId] = [];
1313
- imagePaths[img.annotationId].push(imgPath);
1314
- }
1315
- }
1316
- const linkedSelectors = feedback.annotations.map((a) => a.linkedSelector).filter((s) => !!s);
1317
- let threadId;
1318
- if (linkedSelectors.length > 0) {
1319
- const existingThread = await threadStore.findContinuationThread(linkedSelectors);
1320
- if (existingThread) {
1321
- threadId = existingThread.id;
1322
- await threadStore.addElementIdentifiers(threadId, linkedSelectors);
1323
- } else {
1324
- const newThread = await threadStore.createThread(jobId, linkedSelectors);
1325
- threadId = newThread.id;
1326
- }
1327
- }
1328
- const annotationIds = feedback.annotations.map((a) => a.id);
1329
- const job = __spreadValues({
1330
- id: jobId,
1331
- status: "queued",
1332
- screenshotPath,
1333
- feedback,
1334
- createdAt: Date.now(),
1335
- color,
1336
- threadId,
1337
- annotationIds,
1338
- provider: providerStr === "claude" || providerStr === "codex" ? providerStr : void 0,
1339
- model: modelStr || void 0
1340
- }, Object.keys(imagePaths).length > 0 ? { imagePaths } : {});
1341
- if (threadId) {
1342
- const feedbackSummary = feedback.annotations.map((a) => a.instruction || `[${a.type}]`).join("; ");
1343
- const feedbackContext = formatFeedbackContext(feedback, Object.keys(imagePaths).length > 0 ? imagePaths : void 0);
1344
- await threadStore.appendMessage(threadId, {
1345
- role: "human",
1346
- timestamp: Date.now(),
1347
- jobId,
1348
- screenshotPath,
1349
- annotationIds,
1350
- feedbackSummary,
1351
- feedbackContext: feedbackContext || void 0
1352
- });
1353
- }
1354
- const position = queue.enqueue(job);
1355
- sendJson(res, 200, { jobId, position, threadId });
1356
- }
1357
- async function handleReply(req, res) {
1358
- const contentType = req.headers["content-type"] || "";
1359
- let threadId;
1360
- let reply;
1361
- let color;
1362
- let providerStr;
1363
- let modelStr;
1364
- let replyImageBuffers = [];
1365
- if (contentType.includes("multipart/form-data")) {
1366
- const parsed = await parseMultipart(req);
1367
- const meta = parsed.feedback ? JSON.parse(parsed.feedback) : {};
1368
- threadId = meta.threadId;
1369
- reply = meta.reply;
1370
- color = meta.color;
1371
- providerStr = meta.provider;
1372
- modelStr = meta.model;
1373
- for (const img of parsed.pastedImages) {
1374
- replyImageBuffers.push(img.data);
1375
- }
1376
- } else {
1377
- const chunks = [];
1378
- try {
1379
- for (var iter = __forAwait(req), more, temp, error; more = !(temp = await iter.next()).done; more = false) {
1380
- const chunk = temp.value;
1381
- chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
1382
- }
1383
- } catch (temp) {
1384
- error = [temp];
1385
- } finally {
1386
- try {
1387
- more && (temp = iter.return) && await temp.call(iter);
1388
- } finally {
1389
- if (error)
1390
- throw error[0];
1391
- }
1392
- }
1393
- const body = Buffer.concat(chunks).toString("utf-8");
1394
- let parsed;
1395
- try {
1396
- parsed = JSON.parse(body);
1397
- } catch (e) {
1398
- sendJson(res, 400, { error: "Invalid JSON" });
1399
- return;
1400
- }
1401
- threadId = parsed.threadId;
1402
- reply = parsed.reply;
1403
- color = parsed.color;
1404
- providerStr = parsed.provider;
1405
- modelStr = parsed.model;
1406
- }
1407
- if (!threadId || !reply) {
1408
- sendJson(res, 400, { error: "Missing threadId or reply" });
1409
- return;
1410
- }
1411
- const thread = await threadStore.getThread(threadId);
1412
- if (!thread) {
1413
- sendJson(res, 404, { error: "Thread not found" });
1414
- return;
1415
- }
1416
- const jobId = randomUUID().slice(0, 8);
1417
- const replyImagePaths = [];
1418
- for (let i = 0; i < replyImageBuffers.length; i++) {
1419
- const imgPath = join2(tempDir, `reply-${jobId}-${i}.png`);
1420
- await writeFile2(imgPath, replyImageBuffers[i]);
1421
- replyImagePaths.push(imgPath);
1422
- }
1423
- let screenshotPath = "";
1424
- {
1425
- const history2 = await threadStore.getThreadHistory(threadId);
1426
- for (let i = history2.length - 1; i >= 0; i--) {
1427
- if (history2[i].screenshotPath) {
1428
- screenshotPath = history2[i].screenshotPath;
1429
- break;
1430
- }
1431
- }
1432
- }
1433
- if (!screenshotPath) {
1434
- sendJson(res, 400, { error: "No screenshot available" });
1435
- return;
1436
- }
1437
- await threadStore.appendMessage(threadId, {
1438
- role: "human",
1439
- timestamp: Date.now(),
1440
- jobId,
1441
- replyToQuestion: reply,
1442
- screenshotPath
1443
- });
1444
- const history = await threadStore.getThreadHistory(threadId);
1445
- const annotationIds = [];
1446
- for (const msg of history) {
1447
- if (msg.annotationIds) {
1448
- for (const id of msg.annotationIds) {
1449
- if (!annotationIds.includes(id)) annotationIds.push(id);
1450
- }
1451
- }
1452
- }
1453
- const replyProvider = providerStr === "claude" || providerStr === "codex" ? providerStr : void 0;
1454
- const prompt = buildReplyPrompt(
1455
- screenshotPath,
1456
- history,
1457
- replyProvider,
1458
- replyImagePaths.length > 0 ? replyImagePaths : void 0
1459
- );
1460
- const job = {
1461
- id: jobId,
1462
- status: "queued",
1463
- screenshotPath,
1464
- feedback: { timestamp: (/* @__PURE__ */ new Date()).toISOString(), url: "", viewport: { width: 0, height: 0 }, scrollPosition: { x: 0, y: 0 }, annotations: [], styleModifications: [] },
1465
- createdAt: Date.now(),
1466
- color,
1467
- threadId,
1468
- annotationIds: annotationIds.length > 0 ? annotationIds : void 0,
1469
- provider: replyProvider,
1470
- model: modelStr || void 0
1471
- };
1472
- job._replyPrompt = prompt;
1473
- const position = queue.enqueue(job);
1474
- sendJson(res, 200, { jobId, position, threadId });
1475
- }
1476
- function handleEvents(req, res) {
1477
- res.writeHead(200, {
1478
- "Content-Type": "text/event-stream",
1479
- "Cache-Control": "no-cache",
1480
- Connection: "keep-alive"
1481
- });
1482
- res.write(`event: connected
1483
- data: {"status":"connected"}
91
+ After completing work, output a <resolution> block with declaredScope and inferredScope. If the developer corrected scope, set finalScope. If unclear, output a <question> block.`}else if(d)m=Se(o.feedback,o.imagePaths)+`
1484
92
 
1485
- `);
1486
- const client = { id: randomUUID().slice(0, 8), res };
1487
- sseClients.add(client);
1488
- req.on("close", () => {
1489
- sseClients.delete(client);
1490
- });
1491
- }
1492
- function handleStatus(res) {
1493
- const allActive = queue.allActive;
1494
- sendJson(res, 200, {
1495
- ok: true,
1496
- activeJob: allActive[0] ? { id: allActive[0].id, status: allActive[0].status } : null,
1497
- activeJobs: allActive.map((j) => ({ id: j.id, status: j.status })),
1498
- queueDepth: queue.depth
1499
- });
1500
- }
1501
- function handleCancel(req, res) {
1502
- const reqUrl = new URL(req.url || "/", `http://127.0.0.1:${port}`);
1503
- const jobId = reqUrl.searchParams.get("jobId");
1504
- const cancelled = jobId ? queue.cancelJob(jobId) : queue.cancelActive();
1505
- sendJson(res, 200, { cancelled });
1506
- }
1507
- async function handlePlan(req, res) {
1508
- const { screenshot, feedback: feedbackStr, goal: goalStr, pageUrl: pageUrlStr, viewport: viewportStr, provider: providerStr, model: modelStr, manifest: manifestStr } = await parseMultipart(req);
1509
- if (!screenshot || !goalStr) {
1510
- sendJson(res, 400, { error: "Missing screenshot or goal" });
1511
- return;
1512
- }
1513
- const pageUrl = pageUrlStr || "";
1514
- let viewport = { width: 1440, height: 900 };
1515
- try {
1516
- if (viewportStr) viewport = JSON.parse(viewportStr);
1517
- } catch (e) {
1518
- }
1519
- let feedbackContext;
1520
- if (feedbackStr) {
1521
- try {
1522
- const feedback = JSON.parse(feedbackStr);
1523
- const ctx = formatFeedbackContext(feedback);
1524
- if (ctx) feedbackContext = ctx;
1525
- } catch (e) {
1526
- }
1527
- }
1528
- const planId = randomUUID().slice(0, 8);
1529
- const jobId = randomUUID().slice(0, 8);
1530
- const screenshotPath = join2(tempDir, `screenshot-plan-${planId}.png`);
1531
- await writeFile2(screenshotPath, screenshot);
1532
- const thread = await threadStore.createThread(jobId, []);
1533
- const threadId = thread.id;
1534
- const group = {
1535
- id: planId,
1536
- goal: goalStr,
1537
- status: "planning",
1538
- plannerJobId: jobId,
1539
- plannerThreadId: threadId,
1540
- workerJobIds: [],
1541
- screenshotPath,
1542
- pageUrl,
1543
- viewport,
1544
- createdAt: Date.now()
1545
- };
1546
- jobGroups.set(planId, group);
1547
- const prompt = buildPlannerPrompt(screenshotPath, goalStr, pageUrl, viewport, manifestStr, feedbackContext);
1548
- await threadStore.appendMessage(threadId, {
1549
- role: "human",
1550
- timestamp: Date.now(),
1551
- jobId,
1552
- screenshotPath,
1553
- feedbackSummary: `Plan: ${goalStr}`,
1554
- feedbackContext: `Goal: ${goalStr}
1555
- Page: ${pageUrl}`
1556
- });
1557
- const provider = providerStr === "claude" || providerStr === "codex" ? providerStr : defaultProvider;
1558
- const job = {
1559
- id: jobId,
1560
- status: "queued",
1561
- screenshotPath,
1562
- feedback: {
1563
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1564
- url: pageUrl,
1565
- viewport,
1566
- scrollPosition: { x: 0, y: 0 },
1567
- annotations: [],
1568
- styleModifications: []
1569
- },
1570
- createdAt: Date.now(),
1571
- threadId,
1572
- provider,
1573
- model: modelStr || void 0,
1574
- planId
1575
- };
1576
- job._replyPrompt = prompt;
1577
- job._allowedTools = ["Read"];
1578
- const position = queue.enqueue(job);
1579
- sendJson(res, 200, { planId, jobId, position, threadId });
1580
- }
1581
- async function handlePlanApprove(req, res) {
1582
- const chunks = [];
1583
- try {
1584
- for (var iter = __forAwait(req), more, temp, error; more = !(temp = await iter.next()).done; more = false) {
1585
- const chunk = temp.value;
1586
- chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
1587
- }
1588
- } catch (temp) {
1589
- error = [temp];
1590
- } finally {
1591
- try {
1592
- more && (temp = iter.return) && await temp.call(iter);
1593
- } finally {
1594
- if (error)
1595
- throw error[0];
1596
- }
1597
- }
1598
- const body = Buffer.concat(chunks).toString("utf-8");
1599
- let parsed;
1600
- try {
1601
- parsed = JSON.parse(body);
1602
- } catch (e) {
1603
- sendJson(res, 400, { error: "Invalid JSON" });
1604
- return;
1605
- }
1606
- const { planId, approvedTaskIds } = parsed;
1607
- if (!planId) {
1608
- sendJson(res, 400, { error: "Missing planId" });
1609
- return;
1610
- }
1611
- const group = jobGroups.get(planId);
1612
- if (!group) {
1613
- sendJson(res, 404, { error: "Plan not found" });
1614
- return;
1615
- }
1616
- if (!group.plan) {
1617
- sendJson(res, 400, { error: "Plan has no tasks" });
1618
- return;
1619
- }
1620
- const tasks = approvedTaskIds ? group.plan.filter((t) => approvedTaskIds.includes(t.id)) : group.plan;
1621
- group.status = "executing";
1622
- sendJson(res, 200, { planId, tasks, status: "executing" });
1623
- }
1624
- async function handlePlanExecute(req, res) {
1625
- const { screenshot, planId: planIdStr, tasks: tasksStr, provider: providerStr, model: modelStr } = await parseMultipart(req);
1626
- if (!planIdStr || !tasksStr || !screenshot) {
1627
- sendJson(res, 400, { error: "Missing planId, tasks, or screenshot" });
1628
- return;
1629
- }
1630
- const group = jobGroups.get(planIdStr);
1631
- if (!group) {
1632
- sendJson(res, 404, { error: "Plan not found" });
1633
- return;
1634
- }
1635
- if (group.status !== "executing") {
1636
- sendJson(res, 400, { error: `Plan status is ${group.status}, expected executing` });
1637
- return;
1638
- }
1639
- let tasks;
1640
- try {
1641
- tasks = JSON.parse(tasksStr);
1642
- } catch (e) {
1643
- sendJson(res, 400, { error: "Invalid tasks JSON" });
1644
- return;
1645
- }
1646
- const jobId = randomUUID().slice(0, 8);
1647
- const screenshotPath = join2(tempDir, `screenshot-exec-${planIdStr}.png`);
1648
- await writeFile2(screenshotPath, screenshot);
1649
- const provider = providerStr === "claude" || providerStr === "codex" ? providerStr : defaultProvider;
1650
- const prompt = buildPlanExecutorPrompt(screenshotPath, tasks, group.pageUrl, group.viewport, provider);
1651
- const annotationIds = tasks.map((t) => t.annotationId);
1652
- const job = {
1653
- id: jobId,
1654
- status: "queued",
1655
- screenshotPath,
1656
- feedback: {
1657
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1658
- url: group.pageUrl,
1659
- viewport: group.viewport,
1660
- scrollPosition: { x: 0, y: 0 },
1661
- annotations: [],
1662
- styleModifications: []
1663
- },
1664
- createdAt: Date.now(),
1665
- provider,
1666
- model: modelStr || void 0,
1667
- planId: planIdStr,
1668
- annotationIds
1669
- };
1670
- job._replyPrompt = prompt;
1671
- job._isPlanExecutor = true;
1672
- group.executorJobId = jobId;
1673
- const position = queue.enqueue(job);
1674
- sendJson(res, 200, { jobId, planId: planIdStr, position });
1675
- }
1676
- async function handlePlanReview(req, res) {
1677
- const { screenshot, planId: planIdStr, provider: providerStr, model: modelStr } = await parseMultipart(req);
1678
- if (!planIdStr) {
1679
- sendJson(res, 400, { error: "Missing planId" });
1680
- return;
1681
- }
1682
- const group = jobGroups.get(planIdStr);
1683
- if (!group) {
1684
- sendJson(res, 404, { error: "Plan not found" });
1685
- return;
1686
- }
1687
- group.status = "reviewing";
1688
- const jobId = randomUUID().slice(0, 8);
1689
- let screenshotPath = group.screenshotPath;
1690
- if (screenshot) {
1691
- screenshotPath = join2(tempDir, `screenshot-review-${planIdStr}.png`);
1692
- await writeFile2(screenshotPath, screenshot);
1693
- }
1694
- const completedTasks = (group.plan || []).map((t) => ({
1695
- id: t.id,
1696
- instruction: t.instruction,
1697
- summary: "completed"
1698
- // Workers will have set resolution summaries
1699
- }));
1700
- const prompt = buildReviewerPrompt(screenshotPath, group.goal, completedTasks);
1701
- const provider = providerStr === "claude" || providerStr === "codex" ? providerStr : defaultProvider;
1702
- const job = {
1703
- id: jobId,
1704
- status: "queued",
1705
- screenshotPath,
1706
- feedback: {
1707
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1708
- url: group.pageUrl,
1709
- viewport: group.viewport,
1710
- scrollPosition: { x: 0, y: 0 },
1711
- annotations: [],
1712
- styleModifications: []
1713
- },
1714
- createdAt: Date.now(),
1715
- // Don't set threadId — avoids resuming planner session, which would discard the review prompt
1716
- provider,
1717
- model: modelStr || void 0,
1718
- planId: planIdStr
1719
- };
1720
- job._replyPrompt = prompt;
1721
- job._isReview = true;
1722
- job._allowedTools = ["Read"];
1723
- const position = queue.enqueue(job);
1724
- sendJson(res, 200, { jobId, planId: planIdStr, position });
1725
- }
1726
- function handleGetPlan(planId, res) {
1727
- const group = jobGroups.get(planId);
1728
- if (!group) {
1729
- sendJson(res, 404, { error: "Plan not found" });
1730
- return;
1731
- }
1732
- sendJson(res, 200, group);
1733
- }
1734
- async function handleGetThread(threadId, res) {
1735
- const thread = await threadStore.getThread(threadId);
1736
- if (!thread) {
1737
- sendJson(res, 404, { error: "Thread not found" });
1738
- return;
1739
- }
1740
- const messages = thread.messages.map((_a2) => {
1741
- var _b2 = _a2, { screenshotPath } = _b2, rest = __objRest(_b2, ["screenshotPath"]);
1742
- return rest;
1743
- });
1744
- sendJson(res, 200, { id: thread.id, createdAt: thread.createdAt, messages });
1745
- }
1746
- const cleanupTimer = setInterval(() => {
1747
- cleanupTempDir(tempDir).catch(() => {
1748
- });
1749
- }, CLEANUP_INTERVAL_MS);
1750
- return new Promise((resolve, reject) => {
1751
- server.on("error", (err) => {
1752
- if (err.code === "EADDRINUSE") {
1753
- console.log(`[\u22B9 already watching :${port}]`);
1754
- resolve({
1755
- port,
1756
- close: async () => {
1757
- }
1758
- });
1759
- return;
1760
- }
1761
- reject(err);
1762
- });
1763
- server.listen(port, "127.0.0.1", () => {
1764
- console.log(`[\u22B9 is watching :${port}]`);
1765
- resolve({
1766
- port,
1767
- close: async () => {
1768
- clearInterval(cleanupTimer);
1769
- queue.destroy();
1770
- for (const client of sseClients) {
1771
- try {
1772
- client.res.end();
1773
- } catch (e) {
1774
- }
1775
- }
1776
- sseClients.clear();
1777
- return new Promise((res) => {
1778
- server.close(() => res());
1779
- });
1780
- }
1781
- });
1782
- });
1783
- });
1784
- }
1785
- async function cleanupTempDir(tempDir) {
1786
- try {
1787
- const files = await readdir(tempDir);
1788
- const now = Date.now();
1789
- for (const file of files) {
1790
- const filePath = join2(tempDir, file);
1791
- try {
1792
- const stats = await stat(filePath);
1793
- if (now - stats.mtimeMs > MAX_FILE_AGE_MS) {
1794
- await unlink(filePath);
1795
- }
1796
- } catch (e) {
1797
- }
1798
- }
1799
- } catch (e) {
1800
- }
1801
- }
93
+ Follow the developer's instructions. If they ask for changes, apply them to the source files.
1802
94
 
1803
- // src/server/index.ts
1804
- async function startPopmelt(options) {
1805
- if (process.env.NODE_ENV === "production" && !(options == null ? void 0 : options.force)) {
1806
- throw new Error(
1807
- "[Bridge] Refusing to start in production. Pass { force: true } to override."
1808
- );
1809
- }
1810
- return createPopmelt(options);
1811
- }
1812
- export {
1813
- startPopmelt
1814
- };
95
+ After completing work, output a <resolution> block with declaredScope and inferredScope. If unclear, output a <question> block.`+(h!=="codex"?`
96
+
97
+ IMPORTANT: First, use the Read tool to view the updated screenshot at: ${o.screenshotPath}`:"");else{let I=!l&&o.threadId?await S.getThreadHistory(o.threadId):void 0,E=l?null:await b.loadModel();m=l!=null?l:mt(o.screenshotPath,o.feedback,{threadHistory:I&&I.length>0?I:void 0,provider:h,imagePaths:o.imagePaths,designModel:E!=null?E:void 0})}let k=vn(o.color,`[\u22B9 ${t}:${o.id}]`);console.log(`${k} Reviewing feedback ${o.screenshotPath} (provider: ${h})${o.threadId?` (thread: ${o.threadId})`:""}${d?` (resuming: ${d.slice(0,8)})`:""}`);let R=!!o._isPlanExecutor,x="",$=0,w=(I,E)=>{if(y.broadcast(I,E),R&&I.type==="delta"&&"text"in I){x+=I.text;let M=St(x);if(M.length>$){let N=M.slice($);$=M.length,y.broadcast({type:"task_resolved",jobId:E,planId:o.planId,resolutions:N,threadId:o.threadId},E)}}},L=(se=o._allowedTools)!=null?se:i,{process:J,result:_}=h==="codex"?tt(o.id,{prompt:m,projectRoot:n,screenshotPath:o.screenshotPath,resumeSessionId:d,model:o.model,onEvent:w}):Re(o.id,{prompt:m,projectRoot:n,maxTurns:s,maxBudgetUsd:a,allowedTools:L,claudePath:c,resumeSessionId:d,model:o.model,onEvent:w});y.setActiveProcess(o.id,J);let f=await _;if(o.result=f.text,f.success){console.log(`${k} Iteration complete`),f.fileEdits&&f.fileEdits.length>0&&console.log(`${k} Captured ${f.fileEdits.length} file edit(s): ${f.fileEdits.map(O=>`${O.tool} ${O.file_path}`).join(", ")}`),o.status="done";let I=gt(f.text),E=vt(f.text);if(E.length>0&&o.annotationIds&&o.annotationIds.length>0){let O=new Set(o.annotationIds);E.every(re=>O.has(re.annotationId))||(E=E.map((re,de)=>ae(Z({},re),{annotationId:o.annotationIds[de%o.annotationIds.length]})))}let M=f.fileEdits&&f.fileEdits.length>0?f.fileEdits.map(O=>`${O.tool} ${O.file_path.split("/").pop()}`):void 0;if(o.threadId&&await S.appendMessage(o.threadId,{role:"assistant",timestamp:Date.now(),jobId:o.id,responseText:f.text,resolutions:E.length>0?E:void 0,question:I!=null?I:void 0,sessionId:f.sessionId,toolsUsed:M}),F.captureGitDiff(n).then(async O=>{var Qe;let U=Date.now(),re=o.imagePaths?Object.values(o.imagePaths).flat():[],de=[];if(o.imagePaths)for(let[Ot,Ct]of Object.entries(o.imagePaths))for(let Ae=0;Ae<Ct.length;Ae++)de.push(`screenshots/p-${o.id}-${Ot}-${Ae}.png`);await F.persist({version:1,id:o.id,createdAt:o.createdAt,completedAt:U,durationMs:U-o.createdAt,url:o.feedback.url,viewport:o.feedback.viewport,screenshotPath:`screenshots/s-${o.id}.png`,pastedImagePaths:de,annotations:o.feedback.annotations,styleModifications:o.feedback.styleModifications,inspectedElement:o.feedback.inspectedElement,provider:o.provider,model:o.model,sessionId:f.sessionId,threadId:o.threadId,planId:o.planId,planTaskId:o.planTaskId,responseText:f.text,resolutions:E.length>0?E:[],question:I!=null?I:void 0,fileEdits:(Qe=f.fileEdits)!=null?Qe:[],toolsUsed:M,gitDiff:O},o.screenshotPath,re)}).catch(()=>{}),E.length>0&&E.some(U=>{var de;let re=(de=U.finalScope)!=null?de:U.inferredScope;return(re==null?void 0:re.breadth)==="pattern"})&&b.run().catch(()=>{}),o.planId&&!o.planTaskId){let O=z.get(o.planId);if(O){let U=It(f.text);U&&U.length>0?(O.plan=U,O.status="awaiting_approval",O.plannerThreadId=o.threadId,console.log(`${k} Plan ready: ${U.length} tasks for group ${o.planId}`),y.broadcast({type:"plan_ready",jobId:o.id,planId:o.planId,tasks:U,threadId:o.threadId},o.id)):I||(O.status="error",console.error(`${k} Failed to parse plan from planner response`))}}if(o.planId&&o._isReview){let O=z.get(o.planId);if(O){let U=Tt(f.text);U&&(O.status=U.verdict==="pass"?"done":"executing",console.log(`${k} Review verdict: ${U.verdict} \u2014 ${U.summary}`),y.broadcast({type:"plan_review",planId:o.planId,verdict:U.verdict,summary:U.summary,issues:U.issues},o.id))}}I&&(console.log(`${k} \u{1F4AC} Question detected: "${I.slice(0,120)}" \u2192 broadcasting to ${T.size} SSE clients (threadId=${(C=o.threadId)!=null?C:o.id}, annotationIds=${(ie=(ne=o.annotationIds)==null?void 0:ne.join(","))!=null?ie:"none"})`),y.broadcast({type:"question",jobId:o.id,threadId:(we=o.threadId)!=null?we:o.id,question:I,annotationIds:o.annotationIds},o.id));let N=xt(f.text);N.length>0&&(console.log(`${k} Novel pattern(s): ${N.map(O=>`${O.category}/${O.element}`).join(", ")}`),y.broadcast({type:"novel_patterns",jobId:o.id,patterns:N,threadId:o.threadId},o.id)),y.broadcast({type:"done",jobId:o.id,success:!0,resolutions:E.length>0?E:void 0,responseText:f.text,threadId:o.threadId},o.id)}else console.error(`${k} Error: ${f.error}`),o.status="error",o.error=f.error,y.broadcast({type:"error",jobId:o.id,message:f.error||"Unknown error"},o.id)});let V=un(async(o,l)=>{if(wn(o,l),o.method==="OPTIONS"){l.writeHead(204),l.end();return}let d=new URL(o.url||"/",`http://127.0.0.1:${t}`).pathname;try{if(o.method==="POST"&&d==="/send")await ee(o,l);else if(o.method==="GET"&&d==="/events")B(o,l);else if(o.method==="GET"&&d==="/status")te(l);else if(o.method==="GET"&&d==="/capabilities")g(l,200,{providers:u});else if(o.method==="POST"&&d==="/mcp/install")await ke(o,l);else if(o.method==="POST"&&d==="/reply")await q(o,l);else if(o.method==="POST"&&d==="/cancel")A(o,l);else if(o.method==="POST"&&d==="/materialize")await j(l);else if(o.method==="POST"&&d==="/plan")await P(o,l);else if(o.method==="POST"&&d==="/plan/approve")await Y(o,l);else if(o.method==="POST"&&d==="/plan/execute")await H(o,l);else if(o.method==="POST"&&d==="/plan/review")await X(o,l);else if(o.method==="GET"&&d.startsWith("/plan/"))xe(d.slice(6),l);else if(o.method==="POST"&&d==="/model/component")await Ie(o,l);else if(o.method==="DELETE"&&d==="/model/component")await be(o,l);else if(o.method==="PATCH"&&d==="/model/token")await Pe(o,l);else if(o.method==="DELETE"&&d==="/model/token")await Q(o,l);else if(o.method==="GET"&&d==="/model"){let m=await b.loadModel();g(l,200,{model:m})}else if(o.method==="GET"&&d.startsWith("/thread/")){let m=d.slice(8);await De(m,l)}else g(l,404,{error:"Not found"})}catch(m){console.error("[Bridge] Request error:",m),g(l,500,{error:m instanceof Error?m.message:"Internal error"})}});async function ee(o,l){let{screenshot:h,feedback:d,color:m,provider:k,model:R,pastedImages:x}=await me(o),$;try{$=JSON.parse(d)}catch(C){g(l,400,{error:"Invalid feedback JSON"});return}let w=pe().slice(0,8),L=le(e,`screenshot-${w}.png`);await ge(L,h);let J={};if(x.length>0)for(let C of x){let ne=le(e,`pasted-${w}-${C.annotationId}-${C.index}.png`);await ge(ne,C.data),J[C.annotationId]||(J[C.annotationId]=[]),J[C.annotationId].push(ne)}let _=$.annotations.map(C=>C.linkedSelector).filter(C=>!!C),f;if(_.length>0){let C=await S.findContinuationThread(_);C?(f=C.id,await S.addElementIdentifiers(f,_)):f=(await S.createThread(w,_)).id}let W=$.annotations.map(C=>C.id),K=Z({id:w,status:"queued",screenshotPath:L,feedback:$,createdAt:Date.now(),color:m,threadId:f,annotationIds:W,provider:k==="claude"||k==="codex"?k:void 0,model:R||void 0},Object.keys(J).length>0?{imagePaths:J}:{});if(f){let C=$.annotations.map(ie=>ie.instruction||`[${ie.type}]`).join("; "),ne=Se($,Object.keys(J).length>0?J:void 0);await S.appendMessage(f,{role:"human",timestamp:Date.now(),jobId:w,screenshotPath:L,annotationIds:W,feedbackSummary:C,feedbackContext:ne||void 0})}let se=y.enqueue(K);g(l,200,{jobId:w,position:se,threadId:f})}async function q(o,l){let h=o.headers["content-type"]||"",d,m,k,R,x,$=[];if(h.includes("multipart/form-data")){let M=await me(o),N=M.feedback?JSON.parse(M.feedback):{};d=N.threadId,m=N.reply,k=N.color,R=N.provider,x=N.model;for(let O of M.pastedImages)$.push(O.data)}else{let M=[];try{for(var ie=ce(o),we,I,E;we=!(I=await ie.next()).done;we=!1){let U=I.value;M.push(typeof U=="string"?Buffer.from(U):U)}}catch(I){E=[I]}finally{try{we&&(I=ie.return)&&await I.call(ie)}finally{if(E)throw E[0]}}let N=Buffer.concat(M).toString("utf-8"),O;try{O=JSON.parse(N)}catch(U){g(l,400,{error:"Invalid JSON"});return}d=O.threadId,m=O.reply,k=O.color,R=O.provider,x=O.model}if(!d||!m){g(l,400,{error:"Missing threadId or reply"});return}if(!await S.getThread(d)){g(l,404,{error:"Thread not found"});return}let L=pe().slice(0,8),J=[];for(let M=0;M<$.length;M++){let N=le(e,`reply-${L}-${M}.png`);await ge(N,$[M]),J.push(N)}let _="";{let M=await S.getThreadHistory(d);for(let N=M.length-1;N>=0;N--)if(M[N].screenshotPath){_=M[N].screenshotPath;break}}if(!_){g(l,400,{error:"No screenshot available"});return}await S.appendMessage(d,{role:"human",timestamp:Date.now(),jobId:L,replyToQuestion:m,screenshotPath:_});let f=await S.getThreadHistory(d),W=[];for(let M of f)if(M.annotationIds)for(let N of M.annotationIds)W.includes(N)||W.push(N);let K=R==="claude"||R==="codex"?R:void 0,se=yt(_,f,K,J.length>0?J:void 0),C={id:L,status:"queued",screenshotPath:_,feedback:{timestamp:new Date().toISOString(),url:"",viewport:{width:0,height:0},scrollPosition:{x:0,y:0},annotations:[],styleModifications:[]},createdAt:Date.now(),color:k,threadId:d,annotationIds:W.length>0?W:void 0,provider:K,model:x||void 0};C._replyPrompt=se;let ne=y.enqueue(C);g(l,200,{jobId:L,position:ne,threadId:d})}function B(o,l){l.writeHead(200,{"Content-Type":"text/event-stream","Cache-Control":"no-cache",Connection:"keep-alive"}),l.write(`event: connected
98
+ data: {"status":"connected"}
99
+
100
+ `);let h={id:pe().slice(0,8),res:l};T.add(h),o.on("close",()=>{T.delete(h)})}function te(o){let l=y.allActive;g(o,200,{ok:!0,activeJob:l[0]?{id:l[0].id,status:l[0].status}:null,activeJobs:l.map(h=>({id:h.id,status:h.status})),queueDepth:y.depth})}function A(o,l){let d=new URL(o.url||"/",`http://127.0.0.1:${t}`).searchParams.get("jobId"),m=d?y.cancelJob(d):y.cancelActive();g(l,200,{cancelled:m})}async function j(o){if(b.isRunning){g(o,200,{skipped:!0,reason:"Already running"});return}let l=await b.getUnmaterializedPatternDecisions();if(l.length===0){g(o,200,{skipped:!0,reason:"No unmaterialized pattern decisions"});return}b.run().catch(()=>{}),g(o,200,{started:!0,decisionCount:l.length,decisionIds:l.map(h=>h.id)})}async function P(o,l){let{screenshot:h,feedback:d,goal:m,pageUrl:k,viewport:R,provider:x,model:$,manifest:w}=await me(o);if(!h||!m){g(l,400,{error:"Missing screenshot or goal"});return}let L=k||"",J={width:1440,height:900};try{R&&(J=JSON.parse(R))}catch(M){}let _;if(d)try{let M=JSON.parse(d),N=Se(M);N&&(_=N)}catch(M){}let f=pe().slice(0,8),W=pe().slice(0,8),K=le(e,`screenshot-plan-${f}.png`);await ge(K,h);let C=(await S.createThread(W,[])).id,ne={id:f,goal:m,status:"planning",plannerJobId:W,plannerThreadId:C,workerJobIds:[],screenshotPath:K,pageUrl:L,viewport:J,createdAt:Date.now()};z.set(f,ne);let ie=kt(K,m,L,J,w,_);await S.appendMessage(C,{role:"human",timestamp:Date.now(),jobId:W,screenshotPath:K,feedbackSummary:`Plan: ${m}`,feedbackContext:`Goal: ${m}
101
+ Page: ${L}`});let we=x==="claude"||x==="codex"?x:p,I={id:W,status:"queued",screenshotPath:K,feedback:{timestamp:new Date().toISOString(),url:L,viewport:J,scrollPosition:{x:0,y:0},annotations:[],styleModifications:[]},createdAt:Date.now(),threadId:C,provider:we,model:$||void 0,planId:f};I._replyPrompt=ie,I._allowedTools=["Read"];let E=y.enqueue(I);g(l,200,{planId:f,jobId:W,position:E,threadId:C})}async function Y(o,l){let h=[];try{for(var w=ce(o),L,J,_;L=!(J=await w.next()).done;L=!1){let f=J.value;h.push(typeof f=="string"?Buffer.from(f):f)}}catch(J){_=[J]}finally{try{L&&(J=w.return)&&await J.call(w)}finally{if(_)throw _[0]}}let d=Buffer.concat(h).toString("utf-8"),m;try{m=JSON.parse(d)}catch(f){g(l,400,{error:"Invalid JSON"});return}let{planId:k,approvedTaskIds:R}=m;if(!k){g(l,400,{error:"Missing planId"});return}let x=z.get(k);if(!x){g(l,404,{error:"Plan not found"});return}if(!x.plan){g(l,400,{error:"Plan has no tasks"});return}let $=R?x.plan.filter(f=>R.includes(f.id)):x.plan;x.status="executing",g(l,200,{planId:k,tasks:$,status:"executing"})}async function H(o,l){let{screenshot:h,planId:d,tasks:m,provider:k,model:R}=await me(o);if(!d||!m||!h){g(l,400,{error:"Missing planId, tasks, or screenshot"});return}let x=z.get(d);if(!x){g(l,404,{error:"Plan not found"});return}if(x.status!=="executing"){g(l,400,{error:`Plan status is ${x.status}, expected executing`});return}let $;try{$=JSON.parse(m)}catch(se){g(l,400,{error:"Invalid tasks JSON"});return}let w=pe().slice(0,8),L=le(e,`screenshot-exec-${d}.png`);await ge(L,h);let J=k==="claude"||k==="codex"?k:p,_=Pt(L,$,x.pageUrl,x.viewport,J),f=$.map(se=>se.annotationId),W={id:w,status:"queued",screenshotPath:L,feedback:{timestamp:new Date().toISOString(),url:x.pageUrl,viewport:x.viewport,scrollPosition:{x:0,y:0},annotations:[],styleModifications:[]},createdAt:Date.now(),provider:J,model:R||void 0,planId:d,annotationIds:f};W._replyPrompt=_,W._isPlanExecutor=!0,x.executorJobId=w;let K=y.enqueue(W);g(l,200,{jobId:w,planId:d,position:K})}async function X(o,l){let{screenshot:h,planId:d,provider:m,model:k}=await me(o);if(!d){g(l,400,{error:"Missing planId"});return}let R=z.get(d);if(!R){g(l,404,{error:"Plan not found"});return}R.status="reviewing";let x=pe().slice(0,8),$=R.screenshotPath;h&&($=le(e,`screenshot-review-${d}.png`),await ge($,h));let w=(R.plan||[]).map(W=>({id:W.id,instruction:W.instruction,summary:"completed"})),L=bt($,R.goal,w),J=m==="claude"||m==="codex"?m:p,_={id:x,status:"queued",screenshotPath:$,feedback:{timestamp:new Date().toISOString(),url:R.pageUrl,viewport:R.viewport,scrollPosition:{x:0,y:0},annotations:[],styleModifications:[]},createdAt:Date.now(),provider:J,model:k||void 0,planId:d};_._replyPrompt=L,_._isReview=!0,_._allowedTools=["Read"];let f=y.enqueue(_);g(l,200,{jobId:x,planId:d,position:f})}function xe(o,l){let h=z.get(o);if(!h){g(l,404,{error:"Plan not found"});return}g(l,200,h)}async function ke(o,l){var J,_;let h=[];try{for(var x=ce(o),$,w,L;$=!(w=await x.next()).done;$=!1){let f=w.value;h.push(typeof f=="string"?Buffer.from(f):f)}}catch(w){L=[w]}finally{try{$&&(w=x.return)&&await w.call(x)}finally{if(L)throw L[0]}}let d;if(h.length>0)try{d=JSON.parse(Buffer.concat(h).toString("utf-8")).serverUrl}catch(f){}let m=[];(J=u.claude)!=null&&J.available&&u.claude.mcp&&!u.claude.mcp.found&&m.push(await ht(d)),(_=u.codex)!=null&&_.available&&u.codex.mcp&&!u.codex.mcp.found&&m.push(await ft(d));let[k,R]=await Promise.all([Ue(n),Fe(n)]);u.claude&&(u.claude.mcp=k),u.codex&&(u.codex.mcp=R),g(l,200,{results:m,capabilities:{providers:u}})}async function Ie(o,l){let h=[];try{for(var k=ce(o),R,x,$;R=!(x=await k.next()).done;R=!1){let w=x.value;h.push(typeof w=="string"?Buffer.from(w):w)}}catch(x){$=[x]}finally{try{R&&(x=k.return)&&await x.call(k)}finally{if($)throw $[0]}}let d;try{d=JSON.parse(Buffer.concat(h).toString("utf-8"))}catch(w){g(l,400,{error:"Invalid JSON"});return}if(!d.name||typeof d.name!="string"){g(l,400,{error:"Missing or invalid name"});return}let m=await b.addComponent(d.name);g(l,200,m)}async function be(o,l){let h=[];try{for(var k=ce(o),R,x,$;R=!(x=await k.next()).done;R=!1){let w=x.value;h.push(typeof w=="string"?Buffer.from(w):w)}}catch(x){$=[x]}finally{try{R&&(x=k.return)&&await x.call(k)}finally{if($)throw $[0]}}let d;try{d=JSON.parse(Buffer.concat(h).toString("utf-8"))}catch(w){g(l,400,{error:"Invalid JSON"});return}if(!d.name||typeof d.name!="string"){g(l,400,{error:"Missing or invalid name"});return}let m=await b.removeComponent(d.name);g(l,200,m)}async function Pe(o,l){let h=[];try{for(var k=ce(o),R,x,$;R=!(x=await k.next()).done;R=!1){let w=x.value;h.push(typeof w=="string"?Buffer.from(w):w)}}catch(x){$=[x]}finally{try{R&&(x=k.return)&&await x.call(k)}finally{if($)throw $[0]}}let d;try{d=JSON.parse(Buffer.concat(h).toString("utf-8"))}catch(w){g(l,400,{error:"Invalid JSON"});return}if(!d.path||typeof d.path!="string"||typeof d.value!="string"){g(l,400,{error:"Missing or invalid path/value"});return}let m=await b.updateToken(d.path,d.value);g(l,200,m)}async function Q(o,l){let h=[];try{for(var k=ce(o),R,x,$;R=!(x=await k.next()).done;R=!1){let w=x.value;h.push(typeof w=="string"?Buffer.from(w):w)}}catch(x){$=[x]}finally{try{R&&(x=k.return)&&await x.call(k)}finally{if($)throw $[0]}}let d;try{d=JSON.parse(Buffer.concat(h).toString("utf-8"))}catch(w){g(l,400,{error:"Invalid JSON"});return}if(!d.path||typeof d.path!="string"){g(l,400,{error:"Missing or invalid path"});return}let m=await b.removeToken(d.path);g(l,200,m)}async function De(o,l){let h=await S.getThread(o);if(!h){g(l,404,{error:"Thread not found"});return}let d=h.messages.map(R=>{var x=R,{screenshotPath:m}=x,k=et(x,["screenshotPath"]);return k});g(l,200,{id:h.id,createdAt:h.createdAt,messages:d})}let D=setInterval(()=>{$t(e).catch(()=>{})},mn);return new Promise((o,l)=>{V.on("error",h=>{if(h.code==="EADDRINUSE"){console.log(`[\u22B9 already watching :${t}]`),o({port:t,close:async()=>{}});return}l(h)}),V.listen(t,"127.0.0.1",()=>{console.log(`[\u22B9 is watching :${t}]`),o({port:t,close:async()=>{clearInterval(D),y.destroy();for(let h of T)try{h.res.end()}catch(d){}return T.clear(),new Promise(h=>{V.close(()=>h())})}})})})}async function $t(r){try{let t=await cn(r),n=Date.now();for(let e of t){let s=le(r,e);try{let a=await ln(s);n-a.mtimeMs>gn&&await dn(s)}catch(a){}}}catch(t){}}async function ws(r){if(process.env.NODE_ENV==="production"&&!(r!=null&&r.force))throw new Error("[Bridge] Refusing to start in production. Pass { force: true } to override.");return Mt(r)}export{ws as startPopmelt};