loop-canvas 0.1.2 → 0.1.3

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/cli.mjs CHANGED
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
- import u from"node:fs";import i from"node:path";import A from"node:os";import{spawn as te,execFileSync as B}from"node:child_process";import{fileURLToPath as oe}from"node:url";import g from"node:fs";import m from"node:path";import{spawn as q,execFileSync as M}from"node:child_process";var V=new Set(["node_modules",".git",".loop",".next",".nuxt",".svelte-kit",".output",".vercel","dist","build","out","coverage",".turbo",".cache"]);function j(e,t=6,o=[]){if(t<0||!g.existsSync(e))return o;for(let r of g.readdirSync(e,{withFileTypes:!0})){if(V.has(r.name)||r.name.startsWith("."))continue;let n=m.join(e,r.name);r.isDirectory()?j(n,t-1,o):o.push(n)}return o}var b=(e,...t)=>t.map(o=>m.join(e,o)).find(o=>g.existsSync(o)&&g.statSync(o).isDirectory()),Y=e=>/^\[|^\$|^:|\*/.test(e);function O(e){let t=[];for(let o of e)if(!(!o||o==="index"||o==="_index")&&!(/^\(.*\)$/.test(o)||o.startsWith("@")||o.startsWith("_"))&&!/^\[\[\.\.\..+\]\]$/.test(o)){if(Y(o))return null;t.push(o)}return"/"+t.join("/")}var W={"next-app":e=>{let t=b(e,"app","src/app");return t?j(t).filter(o=>/[\\/]page\.(t|j)sx?$|[\\/]page\.mdx$/.test(o)).map(o=>O(m.relative(t,m.dirname(o)).split(m.sep))):null},"next-pages":e=>{let t=b(e,"pages","src/pages");return t?j(t).filter(o=>/\.(t|j)sx?$|\.mdx$/.test(o)).map(o=>m.relative(t,o).replace(/\.(t|j)sx?$|\.mdx$/,"")).filter(o=>!o.startsWith("api")&&!/^_(app|document|error)$/.test(o)&&!/^(404|500)$/.test(o)).map(o=>O(o.split(m.sep))):null},sveltekit:e=>{let t=b(e,"src/routes");return t?j(t).filter(o=>/\+page\.svelte$/.test(o)).map(o=>O(m.relative(t,m.dirname(o)).split(m.sep))):null},nuxt:e=>{let t=b(e,"pages","app/pages");return t?j(t).filter(o=>o.endsWith(".vue")).map(o=>O(m.relative(t,o).replace(/\.vue$/,"").split(m.sep))):null},astro:e=>{let t=b(e,"src/pages");return t?j(t).filter(o=>/\.(astro|md|mdx|html)$/.test(o)).map(o=>O(m.relative(t,o).replace(/\.(astro|md|mdx|html)$/,"").split(m.sep))):null},remix:e=>{let t=b(e,"app/routes");return t?j(t,1).filter(o=>/\.(t|j)sx?$/.test(o)).map(o=>m.relative(t,o).replace(/\/route\.(t|j)sx?$|\.(t|j)sx?$/,"")).filter(o=>!/^(api|resources)\b/.test(o)&&!o.includes("[.")).map(o=>O(o.split("."))):null},spa:e=>{let t=b(e,"src","app")||e,o=new Set(["/"]),r=[/\bpath\s*:\s*["'`]([^"'`]*)["'`]/g,/<Route\b[^>]*\bpath=["']([^"']*)["']/g,/createRoute\([^)]*path\s*:\s*["']([^"']*)["']/g];for(let n of j(t).filter(a=>/\.(t|j)sx?$|\.vue$/.test(a)&&!/\.(test|spec|stories)\./.test(a)).slice(0,2e3)){let a;try{a=g.readFileSync(n,"utf8")}catch{continue}if(!(a.length>4e5))for(let c of r)for(let h of a.matchAll(c)){let f=h[1].trim();f===""||f.includes(":")||f.includes("*")||f.includes("$")||/\s/.test(f)||f.length>80||!f.startsWith("/")&&/\.\w{2,4}$/.test(f)||o.add("/"+f.replace(/^\/+/,""))}}return[...o]}};function U(e){let t=null;try{t=JSON.parse(g.readFileSync(m.join(e,"package.json"),"utf8"))}catch{}let o={...t?.dependencies||{},...t?.devDependencies||{}},r=l=>l in o,n=null,a=null,c=null;r("next")?(n="Next.js",a=b(e,"app","src/app")?"next-app":"next-pages",c=3e3):r("@sveltejs/kit")?(n="SvelteKit",a="sveltekit",c=5173):r("nuxt")?(n="Nuxt",a="nuxt",c=3e3):r("astro")?(n="Astro",a="astro",c=4321):r("@remix-run/react")||r("@remix-run/dev")?(n="Remix",a="remix",c=5173):r("@angular/core")?(n="Angular",a="spa",c=4200):r("react-router")||r("react-router-dom")||r("@tanstack/react-router")||r("vue-router")?(n="SPA",a="spa",c=r("vite")?5173:3e3):r("vite")?(n="Vite",a="spa",c=5173):r("react-scripts")?(n="Create React App",a="spa",c=3e3):(r("expo")||r("react-native"))&&(n="React Native");let h=[],f=0;if(a){let l=[...W[a](e)||[]];a==="next-app"&&b(e,"pages","src/pages")&&l.push(...W["next-pages"](e)||[]),f=l.filter($=>$==null).length,h=l.filter($=>$!=null)}h=[...new Set(h)].sort((l,$)=>l==="/"?-1:$==="/"?1:l.split("/").length-$.split("/").length||l.localeCompare($));let S=!a&&n!=="React Native"?j(e,4).filter(l=>l.endsWith(".html")).slice(0,60):[],w=t?.scripts||{},_=["dev","start","serve"].find(l=>w[l]),T=g.existsSync(m.join(e,"pnpm-lock.yaml"))?"pnpm":g.existsSync(m.join(e,"yarn.lock"))?"yarn":g.existsSync(m.join(e,"bun.lockb"))||g.existsSync(m.join(e,"bun.lock"))?"bun":"npm";return{framework:n,router:a,routes:h,dynamic:f,html:S,port:c,script:_,pm:T,hasPackage:!!t}}function R(e){return e==="/"?"Home":e.split("/").filter(Boolean).map(t=>t.replace(/[-_]+/g," ").replace(/^\w/,o=>o.toUpperCase())).join(" / ")}async function Q(e){try{return(await fetch(`http://localhost:${e}/__loop/api/ping`,{signal:AbortSignal.timeout(800)})).ok}catch{return!1}}async function G(e){try{return(await fetch(e,{signal:AbortSignal.timeout(1200),redirect:"manual"})).status<500}catch{return!1}}async function H(e,t,{url:o,log:r=console.error}={}){try{e=g.realpathSync(e)}catch{}if(o)return{url:o.replace(/\/$/,""),started:!1};for(let w of Z())if(w.cwd&&(w.cwd===e||w.cwd.startsWith(e+m.sep))&&!await Q(w.port)&&await G(`http://localhost:${w.port}`))return{url:`http://localhost:${w.port}`,started:!1,reused:!0};if(!t.script)return null;let n=m.join(e,".loop");g.mkdirSync(n,{recursive:!0});let a=m.join(n,"dev.log");g.writeFileSync(a,"");let c=g.openSync(a,"a"),h=t.pm==="pnpm"&&!ee("pnpm")?["corepack",["pnpm","run",t.script]]:[t.pm,["run",t.script]];r(`starting the dev server: ${h[0]} ${h[1].join(" ")} (log: .loop/dev.log)`);let f=q(h[0],h[1],{cwd:e,detached:!0,stdio:["ignore",c,c],env:{...process.env,BROWSER:"none",FORCE_COLOR:"0"}});f.unref(),g.writeFileSync(m.join(n,"dev.json"),JSON.stringify({pid:f.pid,cmd:h.flat().join(" ")}));let S=Date.now()+12e4;for(;Date.now()<S;){await new Promise(T=>setTimeout(T,700));let w=g.readFileSync(a,"utf8").replace(/\x1b\[[0-9;]*m/g,""),_=w.match(/https?:\/\/(?:localhost|127\.0\.0\.1|\[::1\]|0\.0\.0\.0):(\d+)/);if(_&&await G(`http://localhost:${_[1]}`))return{url:`http://localhost:${_[1]}`,started:!0,pid:f.pid};try{process.kill(f.pid,0)}catch{throw new Error(`the dev server exited \u2014 see .loop/dev.log:
2
+ import u from"node:fs";import i from"node:path";import A from"node:os";import{spawn as oe,execFileSync as z}from"node:child_process";import{fileURLToPath as ne}from"node:url";import g from"node:fs";import m from"node:path";import{spawn as V,execFileSync as W}from"node:child_process";var Y=new Set(["node_modules",".git",".loop",".next",".nuxt",".svelte-kit",".output",".vercel","dist","build","out","coverage",".turbo",".cache"]);function b(e,t=6,o=[]){if(t<0||!g.existsSync(e))return o;for(let s of g.readdirSync(e,{withFileTypes:!0})){if(Y.has(s.name)||s.name.startsWith("."))continue;let n=m.join(e,s.name);s.isDirectory()?b(n,t-1,o):o.push(n)}return o}var _=(e,...t)=>t.map(o=>m.join(e,o)).find(o=>g.existsSync(o)&&g.statSync(o).isDirectory()),Q=e=>/^\[|^\$|^:|\*/.test(e);function O(e){let t=[];for(let o of e)if(!(!o||o==="index"||o==="_index")&&!(/^\(.*\)$/.test(o)||o.startsWith("@")||o.startsWith("_"))&&!/^\[\[\.\.\..+\]\]$/.test(o)){if(Q(o))return null;t.push(o)}return"/"+t.join("/")}var G={"next-app":e=>{let t=_(e,"app","src/app");return t?b(t).filter(o=>/[\\/]page\.(t|j)sx?$|[\\/]page\.mdx$/.test(o)).map(o=>O(m.relative(t,m.dirname(o)).split(m.sep))):null},"next-pages":e=>{let t=_(e,"pages","src/pages");return t?b(t).filter(o=>/\.(t|j)sx?$|\.mdx$/.test(o)).map(o=>m.relative(t,o).replace(/\.(t|j)sx?$|\.mdx$/,"")).filter(o=>!o.startsWith("api")&&!/^_(app|document|error)$/.test(o)&&!/^(404|500)$/.test(o)).map(o=>O(o.split(m.sep))):null},sveltekit:e=>{let t=_(e,"src/routes");return t?b(t).filter(o=>/\+page\.svelte$/.test(o)).map(o=>O(m.relative(t,m.dirname(o)).split(m.sep))):null},nuxt:e=>{let t=_(e,"pages","app/pages");return t?b(t).filter(o=>o.endsWith(".vue")).map(o=>O(m.relative(t,o).replace(/\.vue$/,"").split(m.sep))):null},astro:e=>{let t=_(e,"src/pages");return t?b(t).filter(o=>/\.(astro|md|mdx|html)$/.test(o)).map(o=>O(m.relative(t,o).replace(/\.(astro|md|mdx|html)$/,"").split(m.sep))):null},remix:e=>{let t=_(e,"app/routes");return t?b(t,1).filter(o=>/\.(t|j)sx?$/.test(o)).map(o=>m.relative(t,o).replace(/\/route\.(t|j)sx?$|\.(t|j)sx?$/,"")).filter(o=>!/^(api|resources)\b/.test(o)&&!o.includes("[.")).map(o=>O(o.split("."))):null},spa:e=>{let t=_(e,"src","app")||e,o=new Set(["/"]),s=[/\bpath\s*:\s*["'`]([^"'`]*)["'`]/g,/<Route\b[^>]*\bpath=["']([^"']*)["']/g,/createRoute\([^)]*path\s*:\s*["']([^"']*)["']/g];for(let n of b(t).filter(a=>/\.(t|j)sx?$|\.vue$/.test(a)&&!/\.(test|spec|stories)\./.test(a)).slice(0,2e3)){let a;try{a=g.readFileSync(n,"utf8")}catch{continue}if(!(a.length>4e5))for(let c of s)for(let h of a.matchAll(c)){let f=h[1].trim();f===""||f.includes(":")||f.includes("*")||f.includes("$")||/\s/.test(f)||f.length>80||!f.startsWith("/")&&/\.\w{2,4}$/.test(f)||o.add("/"+f.replace(/^\/+/,""))}}return[...o]}};function H(e){let t=null;try{t=JSON.parse(g.readFileSync(m.join(e,"package.json"),"utf8"))}catch{}let o={...t?.dependencies||{},...t?.devDependencies||{}},s=l=>l in o,n=null,a=null,c=null;s("next")?(n="Next.js",a=_(e,"app","src/app")?"next-app":"next-pages",c=3e3):s("@sveltejs/kit")?(n="SvelteKit",a="sveltekit",c=5173):s("nuxt")?(n="Nuxt",a="nuxt",c=3e3):s("astro")?(n="Astro",a="astro",c=4321):s("@remix-run/react")||s("@remix-run/dev")?(n="Remix",a="remix",c=5173):s("@angular/core")?(n="Angular",a="spa",c=4200):s("react-router")||s("react-router-dom")||s("@tanstack/react-router")||s("vue-router")?(n="SPA",a="spa",c=s("vite")?5173:3e3):s("vite")?(n="Vite",a="spa",c=5173):s("react-scripts")?(n="Create React App",a="spa",c=3e3):(s("expo")||s("react-native"))&&(n="React Native");let h=[],f=0;if(a){let l=[...G[a](e)||[]];a==="next-app"&&_(e,"pages","src/pages")&&l.push(...G["next-pages"](e)||[]),f=l.filter($=>$==null).length,h=l.filter($=>$!=null)}h=[...new Set(h)].sort((l,$)=>l==="/"?-1:$==="/"?1:l.split("/").length-$.split("/").length||l.localeCompare($));let S=!a&&n!=="React Native"?b(e,4).filter(l=>l.endsWith(".html")).slice(0,60):[],w=t?.scripts||{},j=["dev","start","serve"].find(l=>w[l]),T=g.existsSync(m.join(e,"pnpm-lock.yaml"))?"pnpm":g.existsSync(m.join(e,"yarn.lock"))?"yarn":g.existsSync(m.join(e,"bun.lockb"))||g.existsSync(m.join(e,"bun.lock"))?"bun":"npm";return{framework:n,router:a,routes:h,dynamic:f,html:S,port:c,script:j,pm:T,hasPackage:!!t}}function R(e){return e==="/"?"Home":e.split("/").filter(Boolean).map(t=>t.replace(/[-_]+/g," ").replace(/^\w/,o=>o.toUpperCase())).join(" / ")}async function Z(e){try{return(await fetch(`http://localhost:${e}/__loop/api/ping`,{signal:AbortSignal.timeout(800)})).ok}catch{return!1}}async function U(e){try{return(await fetch(e,{signal:AbortSignal.timeout(1200),redirect:"manual"})).status<500}catch{return!1}}async function J(e,t,{url:o,log:s=console.error}={}){try{e=g.realpathSync(e)}catch{}if(o)return{url:o.replace(/\/$/,""),started:!1};for(let w of ee())if(w.cwd&&(w.cwd===e||w.cwd.startsWith(e+m.sep))&&!await Z(w.port)&&await U(`http://localhost:${w.port}`))return{url:`http://localhost:${w.port}`,started:!1,reused:!0};if(!t.script)return null;let n=m.join(e,".loop");g.mkdirSync(n,{recursive:!0});let a=m.join(n,"dev.log");g.writeFileSync(a,"");let c=g.openSync(a,"a"),h=t.pm==="pnpm"&&!te("pnpm")?["corepack",["pnpm","run",t.script]]:[t.pm,["run",t.script]];s(`starting the dev server: ${h[0]} ${h[1].join(" ")} (log: .loop/dev.log)`);let f=V(h[0],h[1],{cwd:e,detached:!0,stdio:["ignore",c,c],env:{...process.env,BROWSER:"none",FORCE_COLOR:"0"}});f.unref(),g.writeFileSync(m.join(n,"dev.json"),JSON.stringify({pid:f.pid,cmd:h.flat().join(" ")}));let S=Date.now()+12e4;for(;Date.now()<S;){await new Promise(T=>setTimeout(T,700));let w=g.readFileSync(a,"utf8").replace(/\x1b\[[0-9;]*m/g,""),j=w.match(/https?:\/\/(?:localhost|127\.0\.0\.1|\[::1\]|0\.0\.0\.0):(\d+)/);if(j&&await U(`http://localhost:${j[1]}`))return{url:`http://localhost:${j[1]}`,started:!0,pid:f.pid};try{process.kill(f.pid,0)}catch{throw new Error(`the dev server exited \u2014 see .loop/dev.log:
3
3
  ${w.split(`
4
4
  `).slice(-12).join(`
5
- `)}`)}}throw new Error("the dev server took more than 2 minutes to start \u2014 see .loop/dev.log")}function Z(){try{let e=M("lsof",["-nP","-iTCP","-sTCP:LISTEN","-Fpn"],{encoding:"utf8",stdio:["ignore","pipe","ignore"]}),t=new Map,o=null;for(let n of e.split(`
6
- `))if(n[0]==="p")o=n.slice(1);else if(n[0]==="n"&&o){let a=n.match(/:(\d+)$/);a&&(t.get(o)||t.set(o,new Set).get(o)).add(+a[1])}let r=[];for(let[n,a]of t){let c=null;try{c=M("lsof",["-a","-p",n,"-d","cwd","-Fn"],{encoding:"utf8",stdio:["ignore","pipe","ignore"]}).split(`
7
- `).find(h=>h[0]==="n")?.slice(1)||null}catch{}if(c)try{c=g.realpathSync(c)}catch{}for(let h of a)h>=1024&&h!==4848&&r.push({port:h,pid:+n,cwd:c})}return r}catch{return[]}}function ee(e){return(process.env.PATH||"").split(":").some(t=>{try{return g.accessSync(m.join(t,e),g.constants.X_OK),!0}catch{return!1}})}var P=i.dirname(u.realpathSync(oe(import.meta.url))),[ne="help",...re]=process.argv.slice(2),{flags:s,pos:p}=ae(re),se=`loop \u2014 a canvas of live frames you can draw on, point at, and comment on.
5
+ `)}`)}}throw new Error("the dev server took more than 2 minutes to start \u2014 see .loop/dev.log")}function ee(){try{let e=W("lsof",["-nP","-iTCP","-sTCP:LISTEN","-Fpn"],{encoding:"utf8",stdio:["ignore","pipe","ignore"]}),t=new Map,o=null;for(let n of e.split(`
6
+ `))if(n[0]==="p")o=n.slice(1);else if(n[0]==="n"&&o){let a=n.match(/:(\d+)$/);a&&(t.get(o)||t.set(o,new Set).get(o)).add(+a[1])}let s=[];for(let[n,a]of t){let c=null;try{c=W("lsof",["-a","-p",n,"-d","cwd","-Fn"],{encoding:"utf8",stdio:["ignore","pipe","ignore"]}).split(`
7
+ `).find(h=>h[0]==="n")?.slice(1)||null}catch{}if(c)try{c=g.realpathSync(c)}catch{}for(let h of a)h>=1024&&h!==4848&&s.push({port:h,pid:+n,cwd:c})}return s}catch{return[]}}function te(e){return(process.env.PATH||"").split(":").some(t=>{try{return g.accessSync(m.join(t,e),g.constants.X_OK),!0}catch{return!1}})}var P=i.dirname(u.realpathSync(ne(import.meta.url))),[se="help",...re]=process.argv.slice(2),{flags:r,pos:p}=ie(re),ae=`loop \u2014 a canvas of live frames you can draw on, point at, and comment on.
8
8
  Comments go to your agent; the frame shows it working and reloads when it's done.
9
9
 
10
10
  loop open start the canvas for this project and open it
@@ -31,11 +31,11 @@ Comments go to your agent; the frame shows it working and reloads when it's done
31
31
  loop stop stop the canvas server
32
32
  loop install-skill install the Claude Code skill (~/.claude/skills/loop-canvas)
33
33
 
34
- Options: --dir <path> uses that .loop folder instead of ./.loop (searched upward).`;function ae(e){let t={},o=[];for(let r=0;r<e.length;r++)if(e[r].startsWith("--")){let n=e[r].slice(2);t[n]=e[r+1]&&!e[r+1].startsWith("--")?e[++r]:!0}else o.push(e[r]);return{flags:t,pos:o}}function ie(){if(s.dir)return i.resolve(s.dir);let e=process.cwd();for(;;){if(u.existsSync(i.join(e,".loop","canvas.json"))||u.existsSync(i.join(e,".loop","server.json")))return i.join(e,".loop");let t=i.dirname(e);if(t===e)return i.join(process.cwd(),".loop");e=t}}var k=ie();async function D(e){try{return(await fetch(`http://127.0.0.1:${e}/__loop/api/ping`,{signal:AbortSignal.timeout(800)})).ok}catch{return!1}}function N(){try{return JSON.parse(u.readFileSync(i.join(k,"server.json"),"utf8"))}catch{return null}}function le(){let e=String(s.agent||process.env.LOOP_AGENT||"").toLowerCase();if(e.includes("codex"))return"codex";if(e.includes("claude"))return"claude";let t=Object.keys(process.env);if(process.env.CLAUDECODE||process.env.CLAUDE_CODE_ENTRYPOINT)return"claude";if(t.some(o=>o.startsWith("CODEX_")))return"codex";try{let o=process.ppid;for(let r=0;r<14&&o>1;r++){let n=B("ps",["-o","ppid=,command=","-p",String(o)],{encoding:"utf8",stdio:["ignore","pipe","ignore"]}).trim(),a=/^(\d+)\s+(.*)$/.exec(n);if(!a)break;let c=a[2].toLowerCase();if(/(^|\/)codex(\s|$)|codex-cli|codex\.app/.test(c))return"codex";if(/(^|\/)claude(\s|$)|claude-code|claude\.app/.test(c))return"claude";o=+a[1]}}catch{}return null}function ce(){let e={runner:"claude",effort:process.env.CLAUDE_EFFORT||void 0};try{let t=process.env.CLAUDE_CODE_SESSION_ID,o=i.join(A.homedir(),".claude","projects");if(!t||!/^[\w-]+$/.test(t))return e;for(let r of u.readdirSync(o)){let n=i.join(o,r,`${t}.jsonl`);if(!u.existsSync(n))continue;let a=u.statSync(n).size,c=Math.min(a,4e5),h=Buffer.alloc(c),f=u.openSync(n,"r");u.readSync(f,h,0,c,a-c),u.closeSync(f);let S=[...h.toString("utf8").matchAll(/"model":"(claude-[\w.-]+)"/g)];S.length&&(e.model=S[S.length-1][1]);break}}catch{}return e}var J=!1;async function L(){if(J)return;J=!0;let e=le();if(e)try{await d("POST","/__loop/api/settings",{runner:e,...e==="claude"&&process.env.CLAUDECODE?{mode:"inbox",chat:ce()}:{}})}catch{}}function pe(){return(process.env.PATH||"").split(":").some(e=>{try{return u.realpathSync(i.join(e,"loop"))===u.realpathSync(i.join(P,"cli.mjs"))}catch{return!1}})}async function E(){let e=N();if(e&&await D(e.port))return e.port;u.mkdirSync(k,{recursive:!0}),de();let t=u.openSync(i.join(k,"server.log"),"a"),o=te(process.execPath,[i.join(P,"server.mjs"),"--dir",k,...s.port?["--port",String(s.port)]:[]],{detached:!0,stdio:["ignore",t,t]});o.unref();for(let r=0;r<50;r++){await new Promise(a=>setTimeout(a,100));let n=N();if(n&&n.pid===o.pid&&await D(n.port))return n.port}throw new Error(`Loop server didn't start \u2014 see ${i.join(k,"server.log")}`)}function de(){let e=i.join(k,".gitignore");u.existsSync(e)||u.writeFileSync(e,`server.json
34
+ Options: --dir <path> uses that .loop folder instead of ./.loop (searched upward).`;function ie(e){let t={},o=[];for(let s=0;s<e.length;s++)if(e[s].startsWith("--")){let n=e[s].slice(2);t[n]=e[s+1]&&!e[s+1].startsWith("--")?e[++s]:!0}else o.push(e[s]);return{flags:t,pos:o}}function le(){if(r.dir)return i.resolve(r.dir);let e=process.cwd();for(;;){if(u.existsSync(i.join(e,".loop","canvas.json"))||u.existsSync(i.join(e,".loop","server.json")))return i.join(e,".loop");let t=i.dirname(e);if(t===e)return i.join(process.cwd(),".loop");e=t}}var k=le();async function D(e){try{return(await fetch(`http://127.0.0.1:${e}/__loop/api/ping`,{signal:AbortSignal.timeout(800)})).ok}catch{return!1}}function C(){try{return JSON.parse(u.readFileSync(i.join(k,"server.json"),"utf8"))}catch{return null}}function I(){let e=String(r.agent||process.env.LOOP_AGENT||"").toLowerCase();if(e.includes("codex"))return"codex";if(e.includes("claude"))return"claude";let t=Object.keys(process.env);if(process.env.CLAUDECODE||process.env.CLAUDE_CODE_ENTRYPOINT)return"claude";if(t.some(o=>o.startsWith("CODEX_")))return"codex";try{let o=process.ppid;for(let s=0;s<14&&o>1;s++){let n=z("ps",["-o","ppid=,command=","-p",String(o)],{encoding:"utf8",stdio:["ignore","pipe","ignore"]}).trim(),a=/^(\d+)\s+(.*)$/.exec(n);if(!a)break;let c=a[2].toLowerCase();if(/(^|\/)codex(\s|$)|codex-cli|codex\.app/.test(c))return"codex";if(/(^|\/)claude(\s|$)|claude-code|claude\.app/.test(c))return"claude";o=+a[1]}}catch{}return null}function ce(){let e={runner:"claude",effort:process.env.CLAUDE_EFFORT||void 0};try{let t=process.env.CLAUDE_CODE_SESSION_ID,o=i.join(A.homedir(),".claude","projects");if(!t||!/^[\w-]+$/.test(t))return e;for(let s of u.readdirSync(o)){let n=i.join(o,s,`${t}.jsonl`);if(!u.existsSync(n))continue;let a=u.statSync(n).size,c=Math.min(a,4e5),h=Buffer.alloc(c),f=u.openSync(n,"r");u.readSync(f,h,0,c,a-c),u.closeSync(f);let S=[...h.toString("utf8").matchAll(/"model":"(claude-[\w.-]+)"/g)];S.length&&(e.model=S[S.length-1][1]);break}}catch{}return e}var K=!1;async function N(){if(K)return;K=!0;let e=I();if(e)try{await d("POST","/__loop/api/settings",{runner:e,...e==="claude"&&process.env.CLAUDECODE?{mode:"inbox",chat:ce()}:{}})}catch{}}function pe(){return(process.env.PATH||"").split(":").some(e=>{try{return u.realpathSync(i.join(e,"loop"))===u.realpathSync(i.join(P,"cli.mjs"))}catch{return!1}})}async function E(){let e=C();if(e&&await D(e.port))return e.port;u.mkdirSync(k,{recursive:!0}),de();let t=u.openSync(i.join(k,"server.log"),"a"),o=oe(process.execPath,[i.join(P,"server.mjs"),"--dir",k,...r.port?["--port",String(r.port)]:[]],{detached:!0,stdio:["ignore",t,t]});o.unref();for(let s=0;s<50;s++){await new Promise(a=>setTimeout(a,100));let n=C();if(n&&n.pid===o.pid&&await D(n.port))return n.port}throw new Error(`Loop server didn't start \u2014 see ${i.join(k,"server.log")}`)}function de(){let e=i.join(k,".gitignore");u.existsSync(e)||u.writeFileSync(e,`server.json
35
35
  server.log
36
36
  history/
37
37
  annotations/
38
38
  *.tmp
39
- `)}async function d(e,t,o){let r=await E(),n=await fetch(`http://127.0.0.1:${r}${t}`,{method:e,headers:{"content-type":"application/json"},body:o?JSON.stringify(o):void 0}),a=await n.json().catch(()=>({}));if(!n.ok)throw new Error(a.error||`HTTP ${n.status}`);return a}var y=e=>e==null||e===!0?void 0:+e;function K(e){try{B(process.platform==="darwin"?"open":"xdg-open",[e],{stdio:"ignore"})}catch{}}async function ue(){switch(ne){case"open":case"start":{let e=await E();await L();let t=`http://localhost:${e}/__loop/`;console.log(`Loop canvas \u25B8 ${t}`),s["no-open"]||K(t);break}case"new":{await E(),await L();let e=p.join(" ")||"Untitled",t=await d("POST","/__loop/api/frames",{new:!0,name:e,w:y(s.w),h:y(s.h),x:y(s.x),y:y(s.y),near:s.near});console.log(t.dir),console.error(`frame ${t.id} "${t.name}" \u2014 write ${i.join(t.dir,"index.html")}; the canvas reloads it live`);break}case"add":{if(!p[0])throw new Error("usage: loop add <path|url> [--name ...]");let e=/^https?:\/\//.test(p[0])?p[0]:i.resolve(p[0]),t=await d("POST","/__loop/api/frames",{src:e,name:s.name,w:y(s.w),h:y(s.h),x:y(s.x),y:y(s.y)});console.log(`added ${t.id} "${t.name}" (${t.kind}: ${t.src})`);break}case"create":{let e=p.join(" ");if(!e)throw new Error('usage: loop create "<brief>"');let t=await d("POST","/__loop/api/frames",{prompt:e,name:s.name,w:y(s.w),h:y(s.h)});console.log(`frame ${t.id} "${t.name}" \u2014 the agent is building it in ${t.dir}`);break}case"list":case"ls":{let e=await d("GET","/__loop/api/state");if(!e.frames.length)return console.log("No frames yet.");for(let t of e.frames){let o=e.live[t.id],r=e.comments.filter(n=>n.frameId===t.id&&["queued","waiting","working"].includes(n.status)).length;console.log(`${t.id} ${t.name.padEnd(28)} ${o?`\u27F3 ${o.step}`:r?`${r} open`:"\xB7"} ${t.src}`)}break}case"rm":await d("DELETE",`/__loop/api/frames/${p[0]}`),console.log("removed");break;case"share":{let e=(await d("GET","/__loop/api/state")).share;s.public&&(e=await d("POST","/__loop/api/share/public")),e.lan&&console.log(`Same network: ${e.lan}`),e.public?console.log(`Public: ${e.public}`):s.public||console.log(e.tunnelAvailable?"Public link: loop share --public":"Public link: brew install cloudflared, then loop share --public");break}case"agent":{if(!["headless","inbox"].includes(p[0]))throw new Error("usage: loop agent headless|inbox");await d("POST","/__loop/api/settings",{mode:p[0]}),console.log(`agent mode: ${p[0]}`);break}case"watch":{await E(),await L(),await d("POST","/__loop/api/settings",{mode:"inbox"});let e=i.join(k,"inbox");u.mkdirSync(e,{recursive:!0});let t=pe()?"loop":`node ${i.join(P,"cli.mjs")}`;for(console.log("Loop is sending this canvas's comments to this chat. Each comment arrives as a line starting with COMMENT.");;){let o;try{o=await d("GET","/__loop/api/inbox?wait=1")}catch{await new Promise(n=>setTimeout(n,2e3));continue}if(!o.comment)continue;let r=i.join(e,`${o.comment.id}.md`);u.writeFileSync(r,o.prompt),console.log(`COMMENT ${o.comment.id} on "${o.frame.name}": ${JSON.stringify(o.comment.text)} | details (where they pointed, marks, attachments, a screenshot of the frame as they saw it): ${r} | say what you're doing as you go: ${t} step ${o.comment.id} "<a few words>" | check design work: ${t} shot ${o.frame.id} | when done: ${t} reply ${o.comment.id} "<what you changed>" --model <your model id>`)}}case"note":{if(!p[0]||!p[1])throw new Error('usage: loop note <commentId> "<message>" [--model <id>] (adds your message to that comment thread on the canvas)');await d("POST",`/__loop/api/comments/${p[0]}/note`,{text:p.slice(1).join(" "),model:s.model}),console.log("noted on the canvas");break}case"thread":{let e=await d("GET","/__loop/api/state"),t=e.comments.find(r=>r.id===p[0]);if(!t)throw new Error("no such comment");let o=t.parentId||t.id;for(let r of e.comments.filter(n=>n.id===o||n.parentId===o).sort((n,a)=>n.createdAt-a.createdAt))console.log(`[${r.id}] ${r.author?.agent?"Agent":r.author?.name||"User"}: ${r.text}${r.reply?`
40
- \u21B3 ${r.reply}`:""}`);break}case"inbox":{let e=await d("GET",`/__loop/api/inbox${s.wait?"?wait=1":""}`);console.log(JSON.stringify(e,null,2));break}case"step":await d("POST",`/__loop/api/comments/${p[0]}/step`,{text:p.slice(1).join(" ")});break;case"shot":{if(!p[0])throw new Error("usage: loop shot <frameId>");let e=await d("POST",`/__loop/api/frames/${p[0]}/shot`,s.option?{option:s.option}:{});console.log(e.path);break}case"option":{if(!p[0])throw new Error('usage: loop option <commentId> "<short name>"');let e=await d("POST",`/__loop/api/comments/${p[0]}/option`,{name:p.slice(1).join(" ")});console.log(e.frame?`option ${e.id}: ${e.dir} is a full copy of the frame, already on the canvas as its own frame. Make this option's changes there (edit its files in place).`:`option ${e.id}: write the changed files into ${e.dir} (paths relative to ${e.root})`);break}case"reply":{await d("POST",`/__loop/api/comments/${p[0]}/reply`,{text:p.slice(1).join(" ")||"Done.",error:!!s.error,model:s.model}),console.log("replied");break}case"add-route":{if(!p[0])throw new Error("usage: loop add-route </path> [--name ...] (needs a dev server \u2014 run loop scan first)");if(!(await d("GET","/__loop/api/state")).proxy)throw new Error("no dev server is connected yet \u2014 run loop scan (or loop scan --url http://localhost:PORT) first");let t=await d("POST","/__loop/api/frames",{route:p[0],name:s.name||R(p[0]),w:y(s.w),h:y(s.h),near:s.near});console.log(`added ${t.id} "${t.name}" (route ${t.src})`);break}case"scan":{let e=i.dirname(k);await E(),await L();let t=U(e);if(t.framework==="React Native")throw new Error("this looks like a React Native app \u2014 it can't render in a browser canvas yet");console.log(`${t.framework||(t.html.length?"Static HTML":"No framework found")} \xB7 ${t.routes.length||t.html.length} screen${(t.routes.length||t.html.length)===1?"":"s"}${t.dynamic?` (+${t.dynamic} that need a URL parameter, skipped)`:""}`),await E();let o=await d("GET","/__loop/api/state"),n=(y(s.w)&&y(s.h)?[[y(s.w),y(s.h)]]:null)||(s.landscape?[[852,393]]:s.mobile?[[393,852]]:s.both?[[1440,1024],[393,852]]:[[1440,1024]]),a=Math.max(...n.map(l=>l[0]))+(n.length>1?n[1][0]+80:0),c=Math.max(...n.map(l=>l[1])),h=+s.cols||(n[0][0]<600?6:4),f=o.frames.length?Math.max(...o.frames.map(l=>l.y+l.h))+280:0,S=+s.limit||40,w=0,_=(l,$)=>({x:l%h*(a+160)+($?n[0][0]+80:0),y:f+Math.floor(l/h)*(c+220)});if(t.routes.length){let l=await H(e,t,{url:s.url});if(!l)throw new Error("couldn't find or start a dev server. Start it yourself, then run: loop scan --url http://localhost:<port>");l.reused&&console.log(`using this project's dev server at ${l.url}`),await d("POST","/__loop/api/settings",{proxy:l.url});let $=new Set(o.frames.filter(x=>x.kind==="route").map(x=>`${x.src}@${x.w}`)),v=t.routes.includes("/")?"/":t.routes[0],C=s.all?t.routes.slice(0,S):[v];!s.all&&t.routes.length>1&&(console.log(`placed the app as one live frame (${v}). Click around inside it; duplicate it (\u2318D or \u2325-drag the title) to keep a screen side by side.`),console.log(`other screens: ${t.routes.filter(x=>x!==v).slice(0,12).join(" ")}${t.routes.length>13?" \u2026":""}`),console.log("add one directly: loop add-route /path \xB7 every screen at once: loop scan --all")),C.forEach((x,z)=>n.forEach(([I,X],F)=>{$.has(`${x}@${I}`)||(w++,d("POST","/__loop/api/frames",{route:x,name:R(x)+(n.length>1?F?" \xB7 mobile":" \xB7 desktop":""),w:I,h:X,..._(z,F)}))})),s.all&&t.routes.length>S&&console.log(`showing the first ${S} \u2014 pass --limit ${t.routes.length} for all`)}else if(t.html.length){let l=new Set(o.frames.filter(v=>v.kind==="folder").map(v=>i.join(v.src,v.entry))),$=t.html.find(v=>/(^|\/)index\.html?$/i.test(i.relative(e,v))&&!i.relative(e,v).includes("/"))||t.html[0];!s.all&&t.html.length>1&&console.log(`placed ${i.relative(e,$)} as one frame. Click through it, or add another page with: loop add <file> \xB7 every page: loop scan --all`),(s.all?t.html.slice(0,S):[$]).forEach((v,C)=>{l.has(v)||(w++,d("POST","/__loop/api/frames",{src:v,name:i.relative(e,v),w:n[0][0],h:n[0][1],..._(C,0)}))})}else console.log('Nothing to scan here. Ask your agent to create frames (loop new "<name>"), or pass --url for a running app.');await new Promise(l=>setTimeout(l,300));let T=N().port;console.log(`added ${w} frame${w===1?"":"s"} \u25B8 http://localhost:${T}/__loop/`),!s["no-open"]&&w&&K(`http://localhost:${T}/__loop/`);break}case"name":{await E(),p[0]&&await d("POST","/__loop/api/settings",{hostName:p.join(" ")});let e=await d("GET","/__loop/api/state");console.log(`you appear on the canvas as "${e.hostName}"`);break}case"stop":{let e=N();e&&await D(e.port)?(await fetch(`http://127.0.0.1:${e.port}/__loop/api/shutdown`,{method:"POST"}).catch(()=>{}),console.log("stopped")):console.log("not running");try{let t=JSON.parse(u.readFileSync(i.join(k,"dev.json"),"utf8"));process.kill(-t.pid,"SIGTERM"),u.rmSync(i.join(k,"dev.json")),console.log("stopped the dev server Loop started")}catch{}break}case"install-skill":{let e=i.join(A.homedir(),".claude","skills","loop-canvas");u.mkdirSync(e,{recursive:!0}),u.copyFileSync(i.join(P,"skills","loop-canvas","SKILL.md"),i.join(e,"SKILL.md"));let t=i.join(A.homedir(),".local","bin","loop");try{u.mkdirSync(i.dirname(t),{recursive:!0});try{u.unlinkSync(t)}catch{}u.symlinkSync(i.join(P,"cli.mjs"),t),u.chmodSync(i.join(P,"cli.mjs"),493)}catch(o){console.error(`(couldn't link ${t}: ${o.message})`)}console.log(`Skill installed \u2192 ${e}/SKILL.md
41
- CLI linked \u2192 ${t}`);break}default:console.log(se)}}ue().catch(e=>{console.error(`margin: ${e.message}`),process.exit(1)});
39
+ `)}async function d(e,t,o){let s=await E(),n=await fetch(`http://127.0.0.1:${s}${t}`,{method:e,headers:{"content-type":"application/json"},body:o?JSON.stringify(o):void 0}),a=await n.json().catch(()=>({}));if(!n.ok)throw new Error(a.error||`HTTP ${n.status}`);return a}var y=e=>e==null||e===!0?void 0:+e;function B(e){try{z(process.platform==="darwin"?"open":"xdg-open",[e],{stdio:"ignore"})}catch{}}async function ue(){switch(se){case"open":case"start":{let e=await E();await N();let t=`http://localhost:${e}/__loop/`;console.log(`Loop canvas \u25B8 ${t}`);let o=!1;if(I()&&!r.force)try{o=!(await d("GET","/__loop/api/state")).frames.length}catch{}o?console.log("Nothing is on the canvas yet, so the browser was not opened. Add the frames, check them with `loop shot`, then run `loop open` again."):r["no-open"]||B(t);break}case"new":{await E(),await N();let e=p.join(" ")||"Untitled",t=await d("POST","/__loop/api/frames",{new:!0,name:e,w:y(r.w),h:y(r.h),x:y(r.x),y:y(r.y),near:r.near});console.log(t.dir),console.error(`frame ${t.id} "${t.name}" \u2014 write ${i.join(t.dir,"index.html")}; the canvas reloads it live`);break}case"add":{if(!p[0])throw new Error("usage: loop add <path|url> [--name ...]");let e=/^https?:\/\//.test(p[0])?p[0]:i.resolve(p[0]),t=await d("POST","/__loop/api/frames",{src:e,name:r.name,w:y(r.w),h:y(r.h),x:y(r.x),y:y(r.y)});console.log(`added ${t.id} "${t.name}" (${t.kind}: ${t.src})`);break}case"create":{let e=p.join(" ");if(!e)throw new Error('usage: loop create "<brief>"');let t=await d("POST","/__loop/api/frames",{prompt:e,name:r.name,w:y(r.w),h:y(r.h)});console.log(`frame ${t.id} "${t.name}" \u2014 the agent is building it in ${t.dir}`);break}case"list":case"ls":{let e=await d("GET","/__loop/api/state");if(!e.frames.length)return console.log("No frames yet.");for(let t of e.frames){let o=e.live[t.id],s=e.comments.filter(n=>n.frameId===t.id&&["queued","waiting","working"].includes(n.status)).length;console.log(`${t.id} ${t.name.padEnd(28)} ${o?`\u27F3 ${o.step}`:s?`${s} open`:"\xB7"} ${t.src}`)}break}case"rm":await d("DELETE",`/__loop/api/frames/${p[0]}`),console.log("removed");break;case"share":{let e=(await d("GET","/__loop/api/state")).share;r.public&&(e=await d("POST","/__loop/api/share/public")),e.lan&&console.log(`Same network: ${e.lan}`),e.public?console.log(`Public: ${e.public}`):r.public||console.log(e.tunnelAvailable?"Public link: loop share --public":"Public link: brew install cloudflared, then loop share --public");break}case"agent":{if(!["headless","inbox"].includes(p[0]))throw new Error("usage: loop agent headless|inbox");await d("POST","/__loop/api/settings",{mode:p[0]}),console.log(`agent mode: ${p[0]}`);break}case"watch":{await E(),await N(),await d("POST","/__loop/api/settings",{mode:"inbox"});let e=i.join(k,"inbox");u.mkdirSync(e,{recursive:!0});let t=pe()?"loop":`node ${i.join(P,"cli.mjs")}`;for(console.log("Loop is sending this canvas's comments to this chat. Each comment arrives as a line starting with COMMENT.");;){let o;try{o=await d("GET","/__loop/api/inbox?wait=1")}catch{await new Promise(n=>setTimeout(n,2e3));continue}if(!o.comment)continue;let s=i.join(e,`${o.comment.id}.md`);u.writeFileSync(s,o.prompt),console.log(`COMMENT ${o.comment.id} on "${o.frame.name}": ${JSON.stringify(o.comment.text)} | details (where they pointed, marks, attachments, a screenshot of the frame as they saw it): ${s} | say what you're doing as you go: ${t} step ${o.comment.id} "<a few words>" | check design work: ${t} shot ${o.frame.id} | when done: ${t} reply ${o.comment.id} "<what you changed>" --model <your model id>`)}}case"note":{if(!p[0]||!p[1])throw new Error('usage: loop note <commentId> "<message>" [--model <id>] (adds your message to that comment thread on the canvas)');await d("POST",`/__loop/api/comments/${p[0]}/note`,{text:p.slice(1).join(" "),model:r.model}),console.log("noted on the canvas");break}case"thread":{let e=await d("GET","/__loop/api/state"),t=e.comments.find(s=>s.id===p[0]);if(!t)throw new Error("no such comment");let o=t.parentId||t.id;for(let s of e.comments.filter(n=>n.id===o||n.parentId===o).sort((n,a)=>n.createdAt-a.createdAt))console.log(`[${s.id}] ${s.author?.agent?"Agent":s.author?.name||"User"}: ${s.text}${s.reply?`
40
+ \u21B3 ${s.reply}`:""}`);break}case"inbox":{let e=await d("GET",`/__loop/api/inbox${r.wait?"?wait=1":""}`);console.log(JSON.stringify(e,null,2));break}case"step":await d("POST",`/__loop/api/comments/${p[0]}/step`,{text:p.slice(1).join(" ")});break;case"shot":{if(!p[0])throw new Error("usage: loop shot <frameId>");let e=await d("POST",`/__loop/api/frames/${p[0]}/shot`,r.option?{option:r.option}:{});console.log(e.path);break}case"option":{if(!p[0])throw new Error('usage: loop option <commentId> "<short name>"');let e=await d("POST",`/__loop/api/comments/${p[0]}/option`,{name:p.slice(1).join(" ")});console.log(e.frame?`option ${e.id}: ${e.dir} is a full copy of the frame, already on the canvas as its own frame. Make this option's changes there (edit its files in place).`:`option ${e.id}: write the changed files into ${e.dir} (paths relative to ${e.root})`);break}case"reply":{await d("POST",`/__loop/api/comments/${p[0]}/reply`,{text:p.slice(1).join(" ")||"Done.",error:!!r.error,model:r.model}),console.log("replied");break}case"add-route":{if(!p[0])throw new Error("usage: loop add-route </path> [--name ...] (needs a dev server \u2014 run loop scan first)");if(!(await d("GET","/__loop/api/state")).proxy)throw new Error("no dev server is connected yet \u2014 run loop scan (or loop scan --url http://localhost:PORT) first");let t=await d("POST","/__loop/api/frames",{route:p[0],name:r.name||R(p[0]),w:y(r.w),h:y(r.h),near:r.near});console.log(`added ${t.id} "${t.name}" (route ${t.src})`);break}case"scan":{let e=i.dirname(k);await E(),await N();let t=H(e);if(t.framework==="React Native")throw new Error("this looks like a React Native app \u2014 it can't render in a browser canvas yet");console.log(`${t.framework||(t.html.length?"Static HTML":"No framework found")} \xB7 ${t.routes.length||t.html.length} screen${(t.routes.length||t.html.length)===1?"":"s"}${t.dynamic?` (+${t.dynamic} that need a URL parameter, skipped)`:""}`),await E();let o=await d("GET","/__loop/api/state"),n=(y(r.w)&&y(r.h)?[[y(r.w),y(r.h)]]:null)||(r.landscape?[[852,393]]:r.mobile?[[393,852]]:r.both?[[1440,1024],[393,852]]:[[1440,1024]]),a=Math.max(...n.map(l=>l[0]))+(n.length>1?n[1][0]+80:0),c=Math.max(...n.map(l=>l[1])),h=+r.cols||(n[0][0]<600?6:4),f=o.frames.length?Math.max(...o.frames.map(l=>l.y+l.h))+280:0,S=+r.limit||40,w=0,j=(l,$)=>({x:l%h*(a+160)+($?n[0][0]+80:0),y:f+Math.floor(l/h)*(c+220)});if(t.routes.length){let l=await J(e,t,{url:r.url});if(!l)throw new Error("couldn't find or start a dev server. Start it yourself, then run: loop scan --url http://localhost:<port>");l.reused&&console.log(`using this project's dev server at ${l.url}`),await d("POST","/__loop/api/settings",{proxy:l.url});let $=new Set(o.frames.filter(x=>x.kind==="route").map(x=>`${x.src}@${x.w}`)),v=t.routes.includes("/")?"/":t.routes[0],L=r.all?t.routes.slice(0,S):[v];!r.all&&t.routes.length>1&&(console.log(`placed the app as one live frame (${v}). Click around inside it; duplicate it (\u2318D or \u2325-drag the title) to keep a screen side by side.`),console.log(`other screens: ${t.routes.filter(x=>x!==v).slice(0,12).join(" ")}${t.routes.length>13?" \u2026":""}`),console.log("add one directly: loop add-route /path \xB7 every screen at once: loop scan --all")),L.forEach((x,X)=>n.forEach(([F,q],M)=>{$.has(`${x}@${F}`)||(w++,d("POST","/__loop/api/frames",{route:x,name:R(x)+(n.length>1?M?" \xB7 mobile":" \xB7 desktop":""),w:F,h:q,...j(X,M)}))})),r.all&&t.routes.length>S&&console.log(`showing the first ${S} \u2014 pass --limit ${t.routes.length} for all`)}else if(t.html.length){let l=new Set(o.frames.filter(v=>v.kind==="folder").map(v=>i.join(v.src,v.entry))),$=t.html.find(v=>/(^|\/)index\.html?$/i.test(i.relative(e,v))&&!i.relative(e,v).includes("/"))||t.html[0];!r.all&&t.html.length>1&&console.log(`placed ${i.relative(e,$)} as one frame. Click through it, or add another page with: loop add <file> \xB7 every page: loop scan --all`),(r.all?t.html.slice(0,S):[$]).forEach((v,L)=>{l.has(v)||(w++,d("POST","/__loop/api/frames",{src:v,name:i.relative(e,v),w:n[0][0],h:n[0][1],...j(L,0)}))})}else console.log('Nothing to scan here. Ask your agent to create frames (loop new "<name>"), or pass --url for a running app.');await new Promise(l=>setTimeout(l,300));let T=C().port;console.log(`added ${w} frame${w===1?"":"s"} \u25B8 http://localhost:${T}/__loop/`),I()&&!r.open?w&&console.log("The browser was not opened. Check the frames with `loop shot <frame>`, finish the canvas, then run `loop open`."):!r["no-open"]&&w&&B(`http://localhost:${T}/__loop/`);break}case"name":{await E(),p[0]&&await d("POST","/__loop/api/settings",{hostName:p.join(" ")});let e=await d("GET","/__loop/api/state");console.log(`you appear on the canvas as "${e.hostName}"`);break}case"stop":{let e=C();e&&await D(e.port)?(await fetch(`http://127.0.0.1:${e.port}/__loop/api/shutdown`,{method:"POST"}).catch(()=>{}),console.log("stopped")):console.log("not running");try{let t=JSON.parse(u.readFileSync(i.join(k,"dev.json"),"utf8"));process.kill(-t.pid,"SIGTERM"),u.rmSync(i.join(k,"dev.json")),console.log("stopped the dev server Loop started")}catch{}break}case"install-skill":{let e=i.join(A.homedir(),".claude","skills","loop-canvas");u.mkdirSync(e,{recursive:!0}),u.copyFileSync(i.join(P,"skills","loop-canvas","SKILL.md"),i.join(e,"SKILL.md"));let t=i.join(A.homedir(),".local","bin","loop");try{u.mkdirSync(i.dirname(t),{recursive:!0});try{u.unlinkSync(t)}catch{}u.symlinkSync(i.join(P,"cli.mjs"),t),u.chmodSync(i.join(P,"cli.mjs"),493)}catch(o){console.error(`(couldn't link ${t}: ${o.message})`)}console.log(`Skill installed \u2192 ${e}/SKILL.md
41
+ CLI linked \u2192 ${t}`);break}default:console.log(ae)}}ue().catch(e=>{console.error(`margin: ${e.message}`),process.exit(1)});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loop-canvas",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "A canvas of live screens you can point at, draw on and comment on. Comments go straight to your coding agent (Claude Code or Codex), which updates the screen.",
5
5
  "type": "module",
6
6
  "bin": {