notemend 0.1.9 → 0.1.11

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.
@@ -1 +1 @@
1
- import{readFileSync as R,existsSync as D,statSync as z,readdirSync as b}from"node:fs";import{mask as K}from"../store/mask.mjs";import{join as I,isAbsolute as V}from"node:path";import{isIP as H}from"node:net";import{pathToFileURL as Z}from"node:url";import{openStore as Q,retry as X}from"../store/cloud.mjs";import{mentionsIn as Y}from"../store/thread.mjs";import{CATEGORIES as q}from"../store/categories.mjs";import{BREAKPOINTS as tt,DEVICES as et,ICONS as M,SHELLS as G,BROWSERS as J}from"../presets.mjs";const U={png:"image/png",jpg:"image/jpeg",jpeg:"image/jpeg",webp:"image/webp",gif:"image/gif",svg:"image/svg+xml",webm:"video/webm"},W=t=>t.split(".").slice(0,2).map(Number).reduce((n,l)=>n*100+(Number.isNaN(l)?9999:l),0),nt=(t,n)=>{try{const m=R(V(n)?n:I(t,n),"utf8").match(/title="([^"]+)"|\btitle:\s*["'`]([^"'`]+)["'`]/);return(m?.[1]??m?.[2])?.split("\u2014").pop()?.trim()||null}catch{return null}};function vt(t){const n=["app","src/app"].map(u=>I(t,u)).find(D),l=[],m=(u,f)=>{for(const a of b(u,{withFileTypes:!0})){const c=a.name;if(a.isDirectory()){if(/^[_@]|^\(\./.test(c))continue;m(I(u,c),/^\(.+\)$/.test(c)?f:[...f,c])}else/^page\.(jsx?|tsx?|mdx?)$/.test(c)&&l.push({origin:"project",type:"page",pattern:"/"+f.join("/"),entrypoint:I(u,c)})}};return n&&m(n,[]),l}const rt=t=>t[0].toUpperCase()+t.slice(1).replace(/[-_]+/g," ");function ot({root:t,options:n,routes:l}){const m=n.pages??{},u=(e,d,$)=>({route:e,name:d,label:d.replace(/^[\d.]+[A-Z]?\.\s*/,""),group:$}),f=l.filter(e=>e.origin==="project"&&e.type==="page"&&!e.pattern.includes("[")).map(e=>({route:e.pattern.replace(/\/+$/,"")||"/",entrypoint:e.entrypoint})),a=new Set(f.map(e=>e.route)),c=[...f,...Object.keys(m).filter(e=>!a.has(e)).map(e=>({route:e}))],N={};for(const{route:e}of c){const d=e.split("/")[1];d&&(N[d]=(N[d]||0)+1)}const E=e=>{const d=e.split("/")[1];return d&&N[d]>1?rt(d):"Pages"},P=n.groups?()=>n.groups[n.groups.length-1]:E,k=c.map(({route:e,entrypoint:d})=>{const[$,B]=m[e]??[];return u(e,$??(d&&nt(t,d))??e,B??P(e))}),r=n.groups??[...new Set([...Object.values(m).map(([,e])=>e).filter(Boolean),"Pages",...k.map(e=>e.group).sort()])],g=k.sort((e,d)=>r.indexOf(e.group)-r.indexOf(d.group)||W(e.name)-W(d.name)||e.route.localeCompare(d.route)),o=(n.breakpoints??tt).map(e=>({id:e.id,label:e.label,min:e.min,ideal:e.ideal,max:Number.isFinite(e.max)?e.max:null,icon:e.icon??M[e.id]??M.desktop})),i=(n.devices??et).filter(e=>e.w>0&&e.h>0&&G.includes(e.shell)&&J.includes(e.browser)?!0:(console.warn(`[notemend] device "${e.id}" skipped: it needs w and h, shell (${G.join(", ")}) and browser (${J.join(", ")})${e.frame?"; `frame` pictures were replaced by shell + browser in 0.3":""}`),!1));return{pages:g,groups:r.filter(e=>g.some(d=>d.group===e)),breakpoints:o,devices:i,categories:q,sanity:!!n.sanity,shortcuts:n.shortcuts!==!1,ignore:n.ignore??[],grid:{container:"container",grid:"grid",columns:12,...n.grid},zIndex:n.zIndex??2000000020}}const st=t=>{const n=I(t,"notemend.config.mjs");let l=0,m={};return async()=>{if(!D(n))return{};const u=z(n).mtimeMs;if(u===l)return m;l=u;try{const{default:f,...a}=await import(/* webpackIgnore: true */ /* turbopackIgnore: true */ `${Z(n).href}?t=${u}`);m={...a,...f}}catch(f){console.warn(`[notemend] notemend.config.mjs: ${f.message}`)}return m}},it=t=>{const n=String(t.headers.host||"").toLowerCase(),l=n.startsWith("[")?n.slice(1,n.indexOf("]")):n.replace(/:\d+$/,"");return l.includes(":")?H(l)===6:!l||!l.includes(".")||H(l)===4||/\.(localhost|local|test|internal|lan|home\.arpa)$/.test(l)},p=(t,n)=>String(t??"").replace(/[\u0000-\u001f\u007f]+/g," ").trim().slice(0,n),at=t=>({route:p(t.route,500),text:String(t.text??"").slice(0,1e4),selector:p(t.selector,2e3),rx:Number.isFinite(t.rx)?t.rx:0,ry:Number.isFinite(t.ry)?t.ry:0,label:p(t.label,200),ctx:p(t.ctx,120),breakpoint:p(t.breakpoint,40),browser:p(t.browser,80),device:p(t.device,80),tag:p(t.tag,40),classes:Array.isArray(t.classes)?t.classes.slice(0,20).map(n=>p(n,80)):[],nth:Number.isInteger(t.nth)?t.nth:null,category:p(t.category,40),...["urgent","high","low"].includes(t.priority)?{priority:t.priority}:{},viewport:t.viewport&&Number.isFinite(t.viewport.w)&&Number.isFinite(t.viewport.h)?{w:t.viewport.w,h:t.viewport.h}:null,...t.assignee==="claude"||typeof t.assignee=="string"&&/^[^\s@]+@[^\s@]+$/.test(t.assignee)&&t.assignee.length<=200?{assignee:t.assignee.toLowerCase()}:{},...typeof t.host=="string"&&/^[a-z0-9.-]{1,120}(:\d{1,5})?$/i.test(t.host)?{host:t.host.toLowerCase()}:{},...Array.isArray(t.logs)&&t.logs.length?{logs:t.logs.slice(-20).map(n=>({at:p(n?.at,30),kind:["console","error","rejection","network"].includes(n?.kind)?n.kind:"console",text:K(p(n?.text,500))})).filter(n=>n.text)}:{},...t.suggest&&typeof t.suggest.from=="string"&&typeof t.suggest.to=="string"&&t.suggest.to.trim()?{suggest:{from:t.suggest.from.slice(0,2e3),to:t.suggest.to.slice(0,2e3)}}:{}}),ct=t=>{const n={};return(t.status==="open"||t.status==="done")&&(n.status=t.status),"doneBy"in t&&(n.doneBy=t.doneBy==="you"?"you":null),typeof t.category=="string"&&(n.category=p(t.category,40)),"priority"in t&&(n.priority=["urgent","high","low"].includes(t.priority)?t.priority:null),t.fixNow===!0&&(n.fixNow=new Date().toISOString(),n.assignee="claude"),"assignee"in t&&(n.assignee=t.assignee==="claude"||typeof t.assignee=="string"&&/^[^\s@]+@[^\s@]+$/.test(t.assignee)&&t.assignee.length<=200?t.assignee.toLowerCase():null),n},ut=t=>{const n=t.headers.origin;if(!n)return!1;try{return new URL(n).host!==t.headers.host}catch{return!0}};function lt({root:t,options:n,routes:l,open:m=()=>Q(t),crossOrigin:u=!1}){const f=st(t),a=(r,g=200)=>({status:g,type:"application/json",body:JSON.stringify(r)}),c=(r,g)=>({status:r,type:"text/plain",body:g}),N=async r=>{const g=await r.body();if(g.length>45e5)throw Object.assign(new Error("too large"),{status:413});try{return g.length?JSON.parse(g.toString("utf8")):{}}catch{throw Object.assign(new Error("not JSON"),{status:400})}},E=async(r,g)=>{const o=async i=>{const e=await g(i);if(e.status>=400)throw Object.assign(new Error("not written"),{out:e});return e};try{if(r.cloud)return await X(r,o);const i=r.load(),e=await o(i);return r.save(i),e}catch(i){if(i.out)return i.out;throw i}},P=async(r,g)=>{if(r.method!=="GET"&&!u&&ut(r))return c(403,"Cross-origin write refused");const o=await m();if(r.method==="GET"){const s=g.searchParams.get("route"),h=o.version?String(await o.version()):null;if(h&&g.searchParams.get("since")===h)return{status:204,type:"text/plain",body:""};const y=await o.load();return a({version:h,comments:s?y.comments.filter(j=>j.route===s):y.comments,listening:y.listening??null,turbo:y.turbo??null})}let i=null,e=null;const d=o.cloud&&r.method!=="DELETE"?o.people().then(s=>{i=s,e=s.me},s=>{throw s}).catch(s=>s):Promise.resolve(),$=async()=>{const s=await d;if(s instanceof Error)throw s},B=s=>{if(e&&s){s.by=e.email,s.byName=e.name;const h=Y(s.text,i.team.filter(y=>y.email!==e.email));h.length&&(s.mentions=h)}return s};if(r.method==="POST"){const{image:s,images:h,...y}=await N(r),j=at(y);return j.text.trim()?j.route.startsWith("/")?E(o,async C=>{await $();const A=B(o.add(C,j));for(const O of[].concat(h??s??[]))await o.attach(C,A.id,O);return a(A,201)}):a({error:"route must be a path"},400):a({error:"empty comment"},400)}if(r.method==="PATCH"){const s=await N(r);if(s.turbo==="on"||s.turbo==="off")return E(o,async S=>(S.turbo={paused:s.turbo==="off",at:new Date().toISOString()},a({turbo:S.turbo})));const{id:h,reply:y,image:j,images:C,removeImage:A,edit:O,...F}=s,x=Number(h);if(!Number.isInteger(x))return a({error:"bad id"},400);const v=ct(F);return E(o,async S=>{if(await $(),O){const w=S.comments.find(T=>T.id===Number(x)),L=O.at?w?.replies?.find(T=>T.at===O.at):w;if(e&&L?.by&&L.by!==e.email||!(O.text?.trim?.()&&o.edit(S,x,O.at??null,O.text.trim().slice(0,1e4))))return a({error:`no message of yours to edit in #${x}`},404)}y?.trim?.()&&B(o.reply(S,x,"you",y.trim().slice(0,1e4))?.replies.at(-1));for(const w of[].concat(C??j??[]))await o.attach(S,x,w);for(const w of[].concat(A||[]))await o.detach(S,x,w);if(e&&v.status==="done"&&Object.assign(v,{confirmedBy:e.email,confirmedAt:new Date().toISOString()}),e&&v.status==="open"&&Object.assign(v,{confirmedBy:null,confirmedAt:null}),F.archive===!0){const w=S.comments.find(L=>L.id===x);w&&w.status!=="done"&&Object.assign(v,{status:"done",doneBy:"you",working:null}),w?.doneBy==="claude"&&e&&Object.assign(v,{doneBy:"you",confirmedBy:e.email,confirmedAt:new Date().toISOString()}),Object.assign(v,{archivedAt:new Date().toISOString(),...e?{archivedBy:e.email}:{}})}const _=Object.keys(v).length||!y?o.patch(S,x,v):S.comments.find(w=>w.id===Number(x));return _?a(_):a({error:`no comment #${x}`},404)})}if(r.method==="DELETE"){const s=Number(g.searchParams.get("id"));return E(o,async h=>await o.remove(h,s)?a({ok:!0,id:s}):a({error:`no comment #${s}`},404))}return c(405,"Method not allowed")},k=(r,g)=>{const o=g.replace(/^.*[/\\]/,""),i=o.split(".").pop()?.toLowerCase()??"";if(!/^[\w.-]+$/.test(o)||!U[i])return c(400,"Bad name");const e=I(r,o);return D(e)?{status:200,type:U[i],body:R(e)}:c(404,"Not found")};return async r=>{const g=new URL(r.url,"http://devbar"),o=g.pathname.slice(10);if(!u&&!it(r))return c(403,"Notemend answers local addresses only");try{if(o==="config"){const i=ot({root:t,options:{...await f(),...n},routes:l()}),e=await m();return e.cloud&&Object.assign(i,await e.people().catch(()=>({}))),a(i)}if(o==="comments")return await P(r,g);if(o==="image"){const i=await m(),e=(g.searchParams.get("name")||"").replace(/^.*[/\\]/,"");return i.cloud&&/^[\w.-]+$/.test(e)&&await i.fetchImage(e).catch(()=>null),k(i.imageDir,e)}return c(404,"Not found")}catch(i){return i.status===400||i.status===413?c(i.status,i.message):(console.error("[notemend]",i),c(500,"Something went wrong \u2014 the dev server log says what."))}}}function Ot(t){const n=lt(t),l=m=>new Promise((u,f)=>{const a=[];m.on("data",c=>a.push(c)),m.on("end",()=>u(Buffer.concat(a))),m.on("error",f)});return async function(u,f,a){if(!u.url||!u.url.startsWith("/__devbar/"))return a();const c=await n({method:u.method,url:u.url,headers:u.headers,body:()=>l(u)});f.statusCode=c.status,f.setHeader("content-type",c.type),f.setHeader("cache-control","no-store"),f.end(c.body)}}export{ot as buildConfig,lt as notemendHandler,Ot as notemendMiddleware,vt as scanApp,nt as titleOf,rt as titled};
1
+ import{readFileSync as R,existsSync as D,statSync as K,readdirSync as Z}from"node:fs";import{mask as Q}from"../store/mask.mjs";import{join as B,isAbsolute as X}from"node:path";import{isIP as H}from"node:net";import{pathToFileURL as Y}from"node:url";import{openStore as q,retry as tt,CABINET as et,session as G}from"../store/cloud.mjs";import{mentionsIn as nt,cleanAssignee as _,toAgent as rt,STAGES as M}from"../store/thread.mjs";import{CATEGORIES as ot}from"../store/categories.mjs";import{BREAKPOINTS as st,DEVICES as at,ICONS as z,SHELLS as J,BROWSERS as U}from"../presets.mjs";const W={png:"image/png",jpg:"image/jpeg",jpeg:"image/jpeg",webp:"image/webp",gif:"image/gif",svg:"image/svg+xml",webm:"video/webm",m4a:"audio/mp4"},V=t=>t.split(".").slice(0,2).map(Number).reduce((n,d)=>n*100+(Number.isNaN(d)?9999:d),0),it=(t,n)=>{try{const u=R(X(n)?n:B(t,n),"utf8").match(/title="([^"]+)"|\btitle:\s*["'`]([^"'`]+)["'`]/);return(u?.[1]??u?.[2])?.split("\u2014").pop()?.trim()||null}catch{return null}};function kt(t){const n=["app","src/app"].map(l=>B(t,l)).find(D),d=[],u=(l,f)=>{for(const c of Z(l,{withFileTypes:!0})){const i=c.name;if(c.isDirectory()){if(/^[_@]|^\(\./.test(i))continue;u(B(l,i),/^\(.+\)$/.test(i)?f:[...f,i])}else/^page\.(jsx?|tsx?|mdx?)$/.test(i)&&d.push({origin:"project",type:"page",pattern:"/"+f.join("/"),entrypoint:B(l,i)})}};return n&&u(n,[]),d}const ct=t=>t[0].toUpperCase()+t.slice(1).replace(/[-_]+/g," ");function ut({root:t,options:n,routes:d}){const u=n.pages??{},l=(e,m,v)=>({route:e,name:m,label:m.replace(/^[\d.]+[A-Z]?\.\s*/,""),group:v}),f=d.filter(e=>e.origin==="project"&&e.type==="page"&&!e.pattern.includes("[")).map(e=>({route:e.pattern.replace(/\/+$/,"")||"/",entrypoint:e.entrypoint})),c=new Set(f.map(e=>e.route)),i=[...f,...Object.keys(u).filter(e=>!c.has(e)).map(e=>({route:e}))],j={};for(const{route:e}of i){const m=e.split("/")[1];m&&(j[m]=(j[m]||0)+1)}const $=e=>{const m=e.split("/")[1];return m&&j[m]>1?ct(m):"Pages"},P=n.groups?()=>n.groups[n.groups.length-1]:$,I=i.map(({route:e,entrypoint:m})=>{const[v,E]=u[e]??[];return l(e,v??(m&&it(t,m))??e,E??P(e))}),o=n.groups??[...new Set([...Object.values(u).map(([,e])=>e).filter(Boolean),"Pages",...I.map(e=>e.group).sort()])],g=I.sort((e,m)=>o.indexOf(e.group)-o.indexOf(m.group)||V(e.name)-V(m.name)||e.route.localeCompare(m.route)),s=(n.breakpoints??st).map(e=>({id:e.id,label:e.label,min:e.min,ideal:e.ideal,max:Number.isFinite(e.max)?e.max:null,icon:e.icon??z[e.id]??z.desktop})),r=(n.devices??at).filter(e=>e.w>0&&e.h>0&&J.includes(e.shell)&&U.includes(e.browser)?!0:(console.warn(`[notemend] device "${e.id}" skipped: it needs w and h, shell (${J.join(", ")}) and browser (${U.join(", ")})${e.frame?"; `frame` pictures were replaced by shell + browser in 0.3":""}`),!1));return{pages:g,groups:o.filter(e=>g.some(m=>m.group===e)),breakpoints:s,devices:r,categories:ot,sanity:!!n.sanity,shortcuts:n.shortcuts!==!1,ignore:n.ignore??[],grid:{container:"container",grid:"grid",columns:12,...n.grid},zIndex:n.zIndex??2000000020}}const lt=t=>{const n=B(t,"notemend.config.mjs");let d=0,u={};return async()=>{if(!D(n))return{};const l=K(n).mtimeMs;if(l===d)return u;d=l;try{const{default:f,...c}=await import(/* webpackIgnore: true */ /* turbopackIgnore: true */ `${Y(n).href}?t=${l}`);u={...c,...f}}catch(f){console.warn(`[notemend] notemend.config.mjs: ${f.message}`)}return u}},mt=t=>{const n=String(t.headers.host||"").toLowerCase(),d=n.startsWith("[")?n.slice(1,n.indexOf("]")):n.replace(/:\d+$/,"");return d.includes(":")?H(d)===6:!d||!d.includes(".")||H(d)===4||/\.(localhost|local|test|internal|lan|home\.arpa)$/.test(d)},p=(t,n)=>String(t??"").replace(/[\u0000-\u001f\u007f]+/g," ").trim().slice(0,n),dt=t=>({route:p(t.route,500),text:String(t.text??"").slice(0,1e4),selector:p(t.selector,2e3),rx:Number.isFinite(t.rx)?t.rx:0,ry:Number.isFinite(t.ry)?t.ry:0,label:p(t.label,200),ctx:p(t.ctx,120),breakpoint:p(t.breakpoint,40),browser:p(t.browser,80),device:p(t.device,80),tag:p(t.tag,40),classes:Array.isArray(t.classes)?t.classes.slice(0,20).map(n=>p(n,80)):[],nth:Number.isInteger(t.nth)?t.nth:null,category:p(t.category,40),...["urgent","high","normal","low"].includes(t.priority)?{priority:t.priority}:{},...t.shared===!0?{shared:!0}:{},viewport:t.viewport&&Number.isFinite(t.viewport.w)&&Number.isFinite(t.viewport.h)?{w:t.viewport.w,h:t.viewport.h}:null,..._(t.assignee)?{assignee:_(t.assignee)}:{},...M.includes(t.stage)?{stage:t.stage}:{},...typeof t.host=="string"&&/^[a-z0-9.-]{1,120}(:\d{1,5})?$/i.test(t.host)?{host:t.host.toLowerCase()}:{},...Array.isArray(t.logs)&&t.logs.length?{logs:t.logs.slice(-20).map(n=>({at:p(n?.at,30),kind:["console","error","rejection","network"].includes(n?.kind)?n.kind:"console",text:Q(p(n?.text,500))})).filter(n=>n.text)}:{},...t.suggest&&typeof t.suggest.from=="string"&&typeof t.suggest.to=="string"&&t.suggest.to.trim()?{suggest:{from:t.suggest.from.slice(0,2e3),to:t.suggest.to.slice(0,2e3)}}:{}}),gt=t=>{const n={};return(t.status==="open"||t.status==="done")&&(n.status=t.status),"doneBy"in t&&(n.doneBy=t.doneBy==="you"?"you":null),typeof t.category=="string"&&(n.category=p(t.category,40)),"priority"in t&&(n.priority=["urgent","high","normal","low"].includes(t.priority)?t.priority:null),typeof t.shared=="boolean"&&(n.shared=t.shared||null),"assignee"in t&&(n.assignee=_(t.assignee)),"stage"in t&&(n.stage=M.includes(t.stage)?t.stage:null),t.fixNow===!0&&(n.fixNow=new Date().toISOString(),n.stage="approved",rt(n.assignee)||(n.assignee="claude")),n},ft=t=>{const n=t.headers.origin;if(!n)return!1;try{return new URL(n).host!==t.headers.host}catch{return!0}};function pt({root:t,options:n,routes:d,open:u=()=>q(t),crossOrigin:l=!1}){const f=lt(t),c=(o,g=200)=>({status:g,type:"application/json",body:JSON.stringify(o)}),i=(o,g)=>({status:o,type:"text/plain",body:g}),j=async o=>{const g=await o.body();if(g.length>45e5)throw Object.assign(new Error("too large"),{status:413});try{return g.length?JSON.parse(g.toString("utf8")):{}}catch{throw Object.assign(new Error("not JSON"),{status:400})}},$=async(o,g)=>{const s=async r=>{const e=await g(r);if(e.status>=400)throw Object.assign(new Error("not written"),{out:e});return e};try{if(o.cloud)return await tt(o,s);const r=o.load(),e=await s(r);return o.save(r),e}catch(r){if(r.out)return r.out;throw r}},P=async(o,g)=>{if(o.method!=="GET"&&!l&&ft(o))return i(403,"Cross-origin write refused");const s=await u();if(o.method==="GET"){const a=g.searchParams.get("route"),h=s.version?String(await s.version()):null;if(h&&g.searchParams.get("since")===h)return{status:204,type:"text/plain",body:""};const w=await s.load();return c({version:h,comments:a?w.comments.filter(k=>k.route===a):w.comments,listening:w.listening??null,turbo:w.turbo??null})}let r=null,e=null;const m=s.cloud&&o.method!=="DELETE"?s.people().then(a=>{r=a,e=a.me},a=>{throw a}).catch(a=>a):Promise.resolve(),v=async()=>{const a=await m;if(a instanceof Error)throw a},E=a=>{if(e&&a){a.by=e.email,a.byName=e.name;const h=nt(a.text,r.team.filter(w=>w.email!==e.email));h.length&&(a.mentions=h)}return a};if(o.method==="POST"){const{image:a,images:h,...w}=await j(o),k=dt(w);return k.text.trim()?k.route.startsWith("/")?$(s,async A=>{await v();const T=E(s.add(A,k));for(const N of[].concat(h??a??[]))await s.attach(A,T.id,N);return c(T,201)}):c({error:"route must be a path"},400):c({error:"empty comment"},400)}if(o.method==="PATCH"){const a=await j(o);if(a.turbo==="on"||a.turbo==="off")return $(s,async S=>(S.turbo={paused:a.turbo==="off",at:new Date().toISOString()},c({turbo:S.turbo})));const{id:h,reply:w,image:k,images:A,removeImage:T,edit:N,...b}=a,x=Number(h);if(!Number.isInteger(x))return c({error:"bad id"},400);const O=gt(b);return $(s,async S=>{if(await v(),N){const y=S.comments.find(L=>L.id===Number(x)),C=N.at?y?.replies?.find(L=>L.at===N.at):y;if(e&&C?.by&&C.by!==e.email||!(N.text?.trim?.()&&s.edit(S,x,N.at??null,N.text.trim().slice(0,1e4))))return c({error:`no message of yours to edit in #${x}`},404)}w?.trim?.()&&E(s.reply(S,x,"you",w.trim().slice(0,1e4))?.replies.at(-1));for(const y of[].concat(A??k??[]))await s.attach(S,x,y);for(const y of[].concat(T||[]))await s.detach(S,x,y);if(e&&O.status==="done"&&Object.assign(O,{confirmedBy:e.email,confirmedAt:new Date().toISOString()}),e&&O.status==="open"&&Object.assign(O,{confirmedBy:null,confirmedAt:null}),b.archive===!0){const y=S.comments.find(C=>C.id===x);y&&y.status!=="done"&&Object.assign(O,{status:"done",doneBy:"you",working:null}),y?.doneBy==="claude"&&e&&Object.assign(O,{doneBy:"you",confirmedBy:e.email,confirmedAt:new Date().toISOString()}),Object.assign(O,{archivedAt:new Date().toISOString(),...e?{archivedBy:e.email}:{}})}const F=Object.keys(O).length||!w?s.patch(S,x,O):S.comments.find(y=>y.id===Number(x));return F?c(F):c({error:`no comment #${x}`},404)})}if(o.method==="DELETE"){const a=Number(g.searchParams.get("id"));return $(s,async h=>await s.remove(h,a)?c({ok:!0,id:a}):c({error:`no comment #${a}`},404))}return i(405,"Method not allowed")},I=(o,g)=>{const s=g.replace(/^.*[/\\]/,""),r=s.split(".").pop()?.toLowerCase()??"";if(!/^[\w.-]+$/.test(s)||!W[r])return i(400,"Bad name");const e=B(o,s);return D(e)?{status:200,type:W[r],body:R(e)}:i(404,"Not found")};return async o=>{const g=new URL(o.url,"http://devbar"),s=g.pathname.slice(10);if(!l&&!mt(o))return i(403,"Notemend answers local addresses only");try{if(s==="config"){const r=ut({root:t,options:{...await f(),...n},routes:d()}),e=await u();return e.cloud&&Object.assign(r,await e.people().catch(()=>({}))),(e.cloud||await G().catch(()=>null))&&(r.voice=!0),c(r)}if(s==="transcribe"&&o.method==="POST"){const r=await u(),e=r.cloud?await r.token():(await G().catch(()=>null))?.access_token;if(!e)return i(401,"Voice replies need Notemend \u2014 run `npx notemend login`.");const{audio:m}=await j(o),v=/^data:(audio\/(?:webm|mp4))(?:;[\w=.-]+)*;base64,(.+)$/s.exec(String(m||""));if(!v)return i(400,"Not a recording");const E=await fetch(`${et}/api/transcribe`,{method:"POST",body:Buffer.from(v[2],"base64"),headers:{authorization:`Bearer ${e}`,"content-type":v[1]}});return{status:E.status,type:"application/json",body:await E.text()}}if(s==="comments")return await P(o,g);if(s==="image"){const r=await u(),e=(g.searchParams.get("name")||"").replace(/^.*[/\\]/,"");return r.cloud&&/^[\w.-]+$/.test(e)&&await r.fetchImage(e).catch(()=>null),I(r.imageDir,e)}return i(404,"Not found")}catch(r){return r.status===400||r.status===413?i(r.status,r.message):(console.error("[notemend]",r),i(500,"Something went wrong \u2014 the dev server log says what."))}}}function $t(t){const n=pt(t),d=u=>new Promise((l,f)=>{const c=[];u.on("data",i=>c.push(i)),u.on("end",()=>l(Buffer.concat(c))),u.on("error",f)});return async function(l,f,c){if(!l.url||!l.url.startsWith("/__devbar/"))return c();const i=await n({method:l.method,url:l.url,headers:l.headers,body:()=>d(l)});f.statusCode=i.status,f.setHeader("content-type",i.type),f.setHeader("cache-control","no-store"),f.end(i.body)}}export{ut as buildConfig,pt as notemendHandler,$t as notemendMiddleware,kt as scanApp,it as titleOf,ct as titled};
@@ -1,3 +1,3 @@
1
- import{readFileSync as D,writeFileSync as x,existsSync as A,mkdirSync as j,rmSync as $}from"node:fs";import{homedir as L}from"node:os";import{dirname as F,join as h}from"node:path";import{createStore as H,add as K,reply as z,edit as U,patch as Y,shotName as v}from"./comments.mjs";const B=process.env.NOTEMEND_APP||"https://app.notemend.com",_={url:process.env.NOTEMEND_SUPABASE_URL||"https://xzhnjldvkirrydbyrdnm.supabase.co",key:process.env.NOTEMEND_SUPABASE_KEY||"sb_publishable_b8h8jTHZ9KtbPvMuroLhqA_-yBqZwd6"},b=h(process.env.NOTEMEND_HOME||h(L(),".config","notemend"),"session.json"),E=e=>h(e,".notemend.json"),Z=e=>{try{return JSON.parse(D(E(e),"utf8")).project||null}catch{return null}},se=(e,n)=>x(E(e),JSON.stringify({project:n},null,2)+`
2
- `),ae=e=>$(E(e),{force:!0}),G=()=>{try{const e=JSON.parse(D(b,"utf8"));return e.url===_.url?e:null}catch{return null}},R=e=>{j(F(b),{recursive:!0}),x(b,JSON.stringify(e,null,2)+`
3
- `,{mode:384})},ie=()=>$(b,{force:!0});async function w(e,{method:n="GET",token:i,body:s,headers:f={},raw:c=!1}={}){const d=await fetch(`${_.url}${e}`,{method:n,headers:{apikey:_.key,authorization:`Bearer ${i||_.key}`,...s&&!c?{"content-type":"application/json"}:{},...f},body:c?s:s&&JSON.stringify(s)});if(!d.ok){const m=await d.text();let y=m;try{const k=JSON.parse(m);y=k.msg||k.message||k.error_description||k.error||m}catch{}const g=new Error(`${y} (${d.status})`);throw g.status=d.status,g}return d}const N=e=>{const n={url:_.url,email:e.user.email,user:e.user.id,access_token:e.access_token,refresh_token:e.refresh_token,expires_at:e.expires_at??Math.floor(Date.now()/1e3)+e.expires_in};return R(n),n},ce=e=>w("/auth/v1/otp",{method:"POST",body:{email:e,create_user:!0}}),ue=async(e,n)=>N(await(await w("/auth/v1/verify",{method:"POST",body:{email:e,token:n,type:"email"}})).json());async function le(e){const n=await fetch(`${B}/api/cli/device`,{method:"POST"}),i=await n.json().catch(()=>({}));if(!n.ok)throw new Error(i.error||`The cabinet said ${n.status}`);console.log(`Your code: ${i.code}`),e(i.url);const s=Date.now()+i.expires_in*1e3;for(;Date.now()<s;){await new Promise(m=>setTimeout(m,i.interval*1e3));const f=await fetch(`${B}/api/cli/device?device=${i.device}`).catch(()=>null);if(!f||f.status===202)continue;const c=await f.json().catch(()=>({}));if(!f.ok)throw new Error(c.error||`The cabinet said ${f.status}`);const d=await(await w("/auth/v1/user",{token:c.access_token})).json();return N({user:d,access_token:c.access_token,refresh_token:c.refresh_token,expires_at:Number(c.expires_at)})}throw new Error("No answer from the browser in 10 minutes.")}async function fe(e,n){const i=await O();if(!i)throw new Error("Sign in first: npx notemend login");return(await(await w("/rest/v1/projects",{method:"POST",token:i.access_token,headers:{prefer:"return=representation"},body:{name:e,...n?{site_url:n}:{}}})).json())[0]}async function O(){const e=G();if(!e)return null;if(e.expires_at-60>Date.now()/1e3)return e;try{const n=await w("/auth/v1/token?grant_type=refresh_token",{method:"POST",body:{refresh_token:e.refresh_token}});return N(await n.json())}catch(n){if(n.status===400||n.status===401)return null;throw n}}class M extends Error{}function W(e,n,i=null){const s=h(e,"feedback","images"),f=t=>`feedback/images/${t}`,c=new WeakMap,d=i?async()=>i:async()=>{const t=await O();if(!t)throw new Error("Not signed in to Notemend \u2014 run `npx notemend login`.");return t},m=async(t,r={})=>w(`/rest/v1/${t}`,{...r,token:(await d()).access_token}),y=async(t,r={})=>w(`/storage/v1/object/shots/${n}/${t}`,{...r,token:(await d()).access_token}),g=async()=>{const t=await(await m(`stores?project_id=eq.${n}&select=doc,version`)).json();if(!t.length)throw new Error(`Project ${n} is not there, or you are not in it. Check \`npx notemend link\`.`);const r=t[0].doc;return c.set(r,t[0].version),r},k=async()=>(await(await m(`stores?project_id=eq.${n}&select=version`)).json())[0]?.version??null,q=async t=>{const r=c.get(t),u=await(await m(`stores?project_id=eq.${n}&version=eq.${r}`,{method:"PATCH",body:{doc:t},headers:{prefer:"return=representation"}})).json();if(!u.length)throw new M("Someone else wrote first");c.set(t,u[0].version)},J=async(t,r,u)=>{const a=t.comments.find(I=>I.id===Number(r)),l=/^data:(?:image|video)\/(png|jpeg|webp|gif|webm);base64,(.+)$/s.exec(u||"");if(!a||!l)return null;const p=l[1]==="jpeg"?"jpg":l[1];let o=Date.now();for(;A(h(s,`${a.id}-${o}.${p}`));)o++;const S=`${a.id}-${o}.${p}`,P=Buffer.from(l[2],"base64");return await y(S,{method:"POST",body:P,raw:!0,headers:{"content-type":l[1]==="webm"?"video/webm":`image/${l[1]}`,"x-upsert":"true"}}),j(s,{recursive:!0}),x(h(s,S),P),(a.images||=[]).push(f(S)),a.updatedAt=new Date().toISOString(),f(S)},T=async t=>{v(t)&&($(h(s,t),{force:!0}),await y(t,{method:"DELETE"}).catch(()=>{}))};return{cloud:!0,project:n,root:e,imageDir:s,load:g,version:k,save:q,attach:J,detach:async(t,r,u)=>{const a=t.comments.find(o=>o.id===Number(r)),l=v(u),p=l&&a?.images?.find(o=>v(o)===l);return p?(await T(l),a.images=a.images.filter(o=>o!==p),a.updatedAt=new Date().toISOString(),!0):!1},remove:async(t,r)=>{const u=t.comments.find(a=>a.id===Number(r));if(!u)return!1;for(const a of u.images||[])await T(v(a));return t.comments=t.comments.filter(a=>a.id!==Number(r)),!0},fetchImage:async t=>{if(!v(t)||v(t)!==t)return null;const r=h(s,t);if(A(r))return r;const u=await y(t).catch(()=>null);return u?(j(s,{recursive:!0}),x(r,Buffer.from(await u.arrayBuffer())),r):null},people:async()=>{const t=await d(),[r,u,a]=await Promise.all([m("profiles?select=email,name,nickname,avatar_url").then(o=>o.json()),m("rpc/project_people",{method:"POST",body:{p:n}}).then(o=>o.json()).catch(()=>null),m("rpc/is_staff",{method:"POST",body:{p:n}}).then(o=>o.json()).catch(()=>!0)]),l=Array.isArray(u)?new Set(u):null,p=l?r.filter(o=>l.has(o.email)):r;return{me:r.find(o=>o.email===t.email)??{email:t.email,name:"",avatar_url:null},team:p,client:a===!1}},add:K,reply:z,edit:U,patch:Y}}let C=!1;async function me(e){const n=Z(e);return n&&await O()?W(e,n):(n&&!C&&(C=!0,console.warn("[notemend] this site is linked to a Notemend project, but this computer is not signed in \u2014 comments stay in feedback/ until you run `npx notemend login`.")),H(e))}async function de(e,n){for(let i=0;;i++){const s=await e.load(),f=await n(s);try{return await e.save(s),f}catch(c){if(!(c instanceof M)||i>=4)throw c}}}export{_ as BACKEND,B as CABINET,M as Conflict,W as cloudStore,fe as createProject,le as deviceLogin,se as link,Z as linkedProject,ie as logout,me as openStore,de as retry,ce as sendCode,O as session,ae as unlink,ue as verifyCode};
1
+ import{readFileSync as O,writeFileSync as g,existsSync as B,mkdirSync as $,rmSync as T}from"node:fs";import{homedir as K}from"node:os";import{dirname as J,join as w}from"node:path";import{createStore as z,add as U,reply as Y,edit as Z,patch as G,shotName as v,mediaOf as R,mediaName as M}from"./comments.mjs";const C=process.env.NOTEMEND_APP||"https://app.notemend.com",x={url:process.env.NOTEMEND_SUPABASE_URL||"https://xzhnjldvkirrydbyrdnm.supabase.co",key:process.env.NOTEMEND_SUPABASE_KEY||"sb_publishable_b8h8jTHZ9KtbPvMuroLhqA_-yBqZwd6"},N=w(process.env.NOTEMEND_HOME||w(K(),".config","notemend"),"session.json"),P=e=>w(e,".notemend.json"),W=e=>{try{return JSON.parse(O(P(e),"utf8")).project||null}catch{return null}},ue=(e,t)=>g(P(e),JSON.stringify({project:t},null,2)+`
2
+ `),le=e=>T(P(e),{force:!0}),Q=()=>{try{const e=JSON.parse(O(N,"utf8"));return e.url===x.url?e:null}catch{return null}},V=e=>{$(J(N),{recursive:!0}),g(N,JSON.stringify(e,null,2)+`
3
+ `,{mode:384})},fe=()=>T(N,{force:!0});async function y(e,{method:t="GET",token:o,body:s,headers:c={},raw:i=!1}={}){const l=await fetch(`${x.url}${e}`,{method:t,headers:{apikey:x.key,authorization:`Bearer ${o||x.key}`,...s&&!i?{"content-type":"application/json"}:{},...c},body:i?s:s&&JSON.stringify(s)});if(!l.ok){const p=await l.text();let _=p;try{const k=JSON.parse(p);_=k.msg||k.message||k.error_description||k.error||p}catch{}const b=new Error(`${_} (${l.status})`);throw b.status=l.status,b}return l}const D=e=>{const t={url:x.url,email:e.user.email,user:e.user.id,access_token:e.access_token,refresh_token:e.refresh_token,expires_at:e.expires_at??Math.floor(Date.now()/1e3)+e.expires_in};return V(t),t},pe=e=>y("/auth/v1/otp",{method:"POST",body:{email:e,create_user:!0}}),me=async(e,t)=>D(await(await y("/auth/v1/verify",{method:"POST",body:{email:e,token:t,type:"email"}})).json());async function de(e){const t=await fetch(`${C}/api/cli/device`,{method:"POST"}),o=await t.json().catch(()=>({}));if(!t.ok)throw new Error(o.error||`The cabinet said ${t.status}`);console.log(`Your code: ${o.code}`),e(o.url);const s=Date.now()+o.expires_in*1e3;for(;Date.now()<s;){await new Promise(p=>setTimeout(p,o.interval*1e3));const c=await fetch(`${C}/api/cli/device?device=${o.device}`).catch(()=>null);if(!c||c.status===202)continue;const i=await c.json().catch(()=>({}));if(!c.ok)throw new Error(i.error||`The cabinet said ${c.status}`);const l=await(await y("/auth/v1/user",{token:i.access_token})).json();return D({user:l,access_token:i.access_token,refresh_token:i.refresh_token,expires_at:Number(i.expires_at)})}throw new Error("No answer from the browser in 10 minutes.")}async function he(e,t){const o=await j();if(!o)throw new Error("Sign in first: npx notemend login");return(await(await y("/rest/v1/projects",{method:"POST",token:o.access_token,headers:{prefer:"return=representation"},body:{name:e,...t?{site_url:t}:{}}})).json())[0]}async function j(){const e=Q();if(!e)return null;if(e.expires_at-60>Date.now()/1e3)return e;try{const t=await y("/auth/v1/token?grant_type=refresh_token",{method:"POST",body:{refresh_token:e.refresh_token}});return D(await t.json())}catch(t){if(t.status===400||t.status===401)return null;throw t}}async function we(e,t,o){const s=w(J(N),"seen.json");let c={};try{c=JSON.parse(O(s,"utf8"))}catch{}const i=`${e}|${t}|${o}`;if(Date.now()-(c[i]||0)<6*36e5)return;const l=await j();l&&(await y("/rest/v1/rpc/agent_seen",{method:"POST",token:l.access_token,body:{p_agent:e,p_version:t,p_via:o}}),g(s,JSON.stringify({...c,[i]:Date.now()})))}class q extends Error{}function X(e,t,o=null){const s=w(e,"feedback","images"),c=n=>`feedback/images/${n}`,i=new WeakMap,l=o?async()=>o:async()=>{const n=await j();if(!n)throw new Error("Not signed in to Notemend \u2014 run `npx notemend login`.");return n},p=async(n,r={})=>y(`/rest/v1/${n}`,{...r,token:(await l()).access_token}),_=async(n,r={})=>y(`/storage/v1/object/shots/${t}/${n}`,{...r,token:(await l()).access_token}),b=async()=>{const n=await(await p(`stores?project_id=eq.${t}&select=doc,version`)).json();if(!n.length)throw new Error(`Project ${t} is not there, or you are not in it. Check \`npx notemend link\`.`);const r=n[0].doc;return i.set(r,n[0].version),r},k=async()=>(await(await p(`stores?project_id=eq.${t}&select=version`)).json())[0]?.version??null,L=async n=>{const r=i.get(n),u=await(await p(`stores?project_id=eq.${t}&version=eq.${r}`,{method:"PATCH",body:{doc:n},headers:{prefer:"return=representation"}})).json();if(!u.length)throw new q("Someone else wrote first");i.set(n,u[0].version)},F=async(n,r,u)=>{const a=n.comments.find(E=>E.id===Number(r)),m=R(u);if(!a||!m)return null;let d=Date.now();for(;B(w(s,M(a.id,d,m)));)d++;const f=M(a.id,d,m),S=Buffer.from(m.base64,"base64");return await _(f,{method:"POST",body:S,raw:!0,headers:{"content-type":m.type,"x-upsert":"true"}}),$(s,{recursive:!0}),g(w(s,f),S),(a.images||=[]).push(c(f)),a.updatedAt=new Date().toISOString(),c(f)},A=async n=>{v(n)&&(T(w(s,n),{force:!0}),await _(n,{method:"DELETE"}).catch(()=>{}))};return{cloud:!0,project:t,root:e,imageDir:s,load:b,version:k,save:L,attach:F,detach:async(n,r,u)=>{const a=n.comments.find(f=>f.id===Number(r)),m=v(u),d=m&&a?.images?.find(f=>v(f)===m);return d?(await A(m),a.images=a.images.filter(f=>f!==d),a.updatedAt=new Date().toISOString(),!0):!1},remove:async(n,r)=>{const u=n.comments.find(a=>a.id===Number(r));if(!u)return!1;for(const a of u.images||[])await A(v(a));return n.comments=n.comments.filter(a=>a.id!==Number(r)),!0},fetchImage:async n=>{if(!v(n)||v(n)!==n)return null;const r=w(s,n);if(B(r))return r;const u=await _(n).catch(()=>null);return u?($(s,{recursive:!0}),g(r,Buffer.from(await u.arrayBuffer())),r):null},people:async()=>{const n=await l(),r=h=>p(`rpc/${h}`,{method:"POST",body:{p:t}}).then(H=>H.json()).catch(()=>null),[u,a,m,d,f]=await Promise.all([p("profiles?select=email,name,nickname,avatar_url").then(h=>h.json()),r("project_people"),r("is_staff").then(h=>h??!0),r("project_staff"),r("project_lead")]),S=Array.isArray(a)?new Set(a):null,E=S?u.filter(h=>S.has(h.email)):u;return{me:u.find(h=>h.email===n.email)??{email:n.email,name:"",avatar_url:null},team:E,client:m===!1,...Array.isArray(d)?{studio:d}:{},...typeof f=="string"?{lead:f}:{}}},add:U,reply:Y,edit:Z,patch:G,token:async()=>(await l()).access_token}}let I=!1;async function ye(e){const t=W(e);return t&&await j()?X(e,t):(t&&!I&&(I=!0,console.warn("[notemend] this site is linked to a Notemend project, but this computer is not signed in \u2014 comments stay in feedback/ until you run `npx notemend login`.")),z(e))}async function _e(e,t){for(let o=0;;o++){const s=await e.load(),c=await t(s);try{return await e.save(s),c}catch(i){if(!(i instanceof q)||o>=4)throw i}}}export{x as BACKEND,C as CABINET,q as Conflict,X as cloudStore,he as createProject,de as deviceLogin,ue as link,W as linkedProject,fe as logout,ye as openStore,_e as retry,we as seen,pe as sendCode,j as session,le as unlink,me as verifyCode};
@@ -1,3 +1,3 @@
1
- import{readFileSync as E,writeFileSync as O,existsSync as v,mkdirSync as b,renameSync as J,rmSync as B}from"node:fs";import{dirname as M,join as p}from"node:path";import{categoryOf as T}from"./categories.mjs";import{CATEGORIES as Q,categoryOf as U}from"./categories.mjs";import{stateOf as W,STATE_LABEL as X}from"./status.mjs";const x=n=>{const t=String(n??"").split(/[/\\]/).pop();return/^[\w-][\w.-]*\.(png|jpe?g|webp|gif|webm)$/i.test(t)?t:""},I=n=>Math.max(n.nextId||0,n.comments.reduce((t,s)=>Math.max(t,s.id),0)+1);function L(n,{route:t,selector:s,rx:m,ry:o,text:f,label:g,viewport:y,breakpoint:N,browser:$,device:D,tag:A,classes:r,nth:d,category:l,suggest:e,assignee:c,ctx:u,logs:a,host:S,priority:h}){const i={id:I(n),route:t,selector:s,rx:m,ry:o,text:f,tag:A||"",classes:r||[],nth:Number.isInteger(d)?d:null,label:g||"",ctx:u||"",viewport:y||null,breakpoint:N||"desktop",browser:$||"",device:D||"",category:T(l).id,status:"open",note:"",replies:[],doneBy:null,images:[],...e?{suggest:e}:{},...c?{assignee:c}:{},...h?{priority:h}:{},...S?{host:S}:{},...a?.length?{logs:a}:{},createdAt:new Date().toISOString(),updatedAt:null};return n.comments.push(i),n.nextId=i.id+1,i}function C(n,t,s,m,o){const f=n.comments.find(g=>g.id===Number(t));return f?((f.replies||=[]).push({author:s,text:m,at:new Date().toISOString(),...o&&o!=="claude"?{agent:o}:{}}),f.updatedAt=new Date().toISOString(),f):null}function G(n,t,s,m){const o=n.comments.find(g=>g.id===Number(t)),f=s?o?.replies?.find(g=>g.at===s&&g.author==="you"):o;return f?(f.text=m,o.updatedAt=new Date().toISOString(),o):null}function R(n,t,s){const m=n.comments.find(o=>o.id===Number(t));return m?(Object.assign(m,s,{updatedAt:new Date().toISOString()}),m):null}function H(n){const t=p(n,"feedback","comments.json"),s=p(n,"feedback","images"),m=p(n,"feedback","archive"),o=p(m,"images"),f=r=>`feedback/images/${r}`;return{root:n,file:t,imageDir:s,archiveDir:m,archiveImageDir:o,load:()=>v(t)?JSON.parse(E(t,"utf8")):{comments:[]},save:r=>{b(M(t),{recursive:!0}),O(`${t}.tmp`,JSON.stringify(r,null,2)+`
2
- `),J(`${t}.tmp`,t)},attach:(r,d,l)=>{const e=r.comments.find(h=>h.id===Number(d)),c=/^data:(?:image|video)\/(png|jpeg|webp|gif|webm);base64,(.+)$/s.exec(l||"");if(!e||!c)return null;b(s,{recursive:!0});const u=c[1]==="jpeg"?"jpg":c[1];let a=Date.now();for(;v(p(s,`${e.id}-${a}.${u}`));)a++;const S=`${e.id}-${a}.${u}`;return O(p(s,S),Buffer.from(c[2],"base64")),(e.images||=[]).push(f(S)),e.updatedAt=new Date().toISOString(),f(S)},detach:(r,d,l)=>{const e=r.comments.find(a=>a.id===Number(d)),c=x(l),u=c&&e?.images?.find(a=>x(a)===c);return u?(B(p(s,c),{force:!0}),e.images=e.images.filter(a=>a!==u),e.updatedAt=new Date().toISOString(),!0):!1},remove:(r,d)=>{const l=r.comments.find(e=>e.id===Number(d));if(!l)return!1;for(const e of l.images||[])x(e)&&B(p(s,x(e)),{force:!0});return r.comments=r.comments.filter(e=>e.id!==Number(d)),!0},add:L,patch:R,reply:C,edit:G,nextId:I,prune:(r,{dry:d=!1,date:l=new Date().toISOString().slice(0,10)}={})=>{const e=r.comments.filter(i=>i.status==="done"),c=r.comments.filter(i=>i.status!=="done"),u=p(m,`comments-${l}.json`);if(d||!e.length)return{moving:e,keeping:c,archiveFile:u,images:e.flatMap(i=>i.images||[])};b(o,{recursive:!0});const a=[];for(const i of e)i.images=(i.images||[]).map(k=>{const w=x(k);if(!w)return k;const j=p(s,w),F=p(o,w);return v(j)&&J(j,F),a.push(w),`feedback/archive/images/${w}`});const S=v(u)?JSON.parse(E(u,"utf8")).comments||[]:[],h=new Set(S.map(i=>i.id));return O(u,JSON.stringify({comments:[...S,...e.filter(i=>!h.has(i.id))]},null,2)+`
3
- `),r.nextId=I(r),r.comments=c,{moving:e,keeping:c,archiveFile:u,images:a}}}}export{Q as CATEGORIES,X as STATE_LABEL,L as add,U as categoryOf,H as createStore,G as edit,I as nextId,R as patch,C as reply,x as shotName,W as stateOf};
1
+ import{readFileSync as E,writeFileSync as I,existsSync as O,mkdirSync as $,renameSync as J,rmSync as B}from"node:fs";import{dirname as T,join as l}from"node:path";import{categoryOf as L}from"./categories.mjs";import{CATEGORIES as V,categoryOf as W}from"./categories.mjs";import{stateOf as Y,STATE_LABEL as Z}from"./status.mjs";const h=r=>{const n=String(r??"").split(/[/\\]/).pop();return/^[\w-][\w.-]*\.(png|jpe?g|webp|gif|webm|m4a)$/i.test(n)?n:""},C=r=>{const n=/^data:(image|video|audio)\/(png|jpeg|webp|gif|webm|mp4)(?:;[\w=.-]+)*;base64,(.+)$/s.exec(String(r||""));if(!n)return null;const[,t,o,s]=n;return(t==="image"?!/^(png|jpeg|webp|gif)$/.test(o):t==="video"?o!=="webm":!/^(webm|mp4)$/.test(o))?null:{base64:s,voice:t==="audio",ext:o==="jpeg"?"jpg":o==="mp4"?"m4a":o,type:`${t}/${o}`}},F=(r,n,t)=>`${t.voice?"voice-":""}${r}-${n}.${t.ext}`,N=r=>Math.max(r.nextId||0,r.comments.reduce((n,t)=>Math.max(n,t.id),0)+1);function G(r,{route:n,selector:t,rx:o,ry:s,text:p,label:f,viewport:v,breakpoint:y,browser:D,device:k,tag:A,classes:i,nth:d,category:g,suggest:e,assignee:c,ctx:a,logs:m,host:w,priority:b,shared:u,stage:x}){const S={id:N(r),route:n,selector:t,rx:o,ry:s,text:p,tag:A||"",classes:i||[],nth:Number.isInteger(d)?d:null,label:f||"",ctx:a||"",viewport:v||null,breakpoint:y||"desktop",browser:D||"",device:k||"",category:L(g).id,status:"open",note:"",replies:[],doneBy:null,images:[],...e?{suggest:e}:{},...c?{assignee:c}:{},...b?{priority:b}:{},...x?{stage:x}:{},...u?{shared:!0}:{},...w?{host:w}:{},...m?.length?{logs:m}:{},createdAt:new Date().toISOString(),updatedAt:null};return r.comments.push(S),r.nextId=S.id+1,S}function R(r,n,t,o,s,p){const f=r.comments.find(v=>v.id===Number(n));return f?((f.replies||=[]).push({author:t,text:o,at:new Date().toISOString(),...s&&s!=="claude"?{agent:s}:{},...p?{by:p}:{}}),f.updatedAt=new Date().toISOString(),f):null}function _(r,n,t,o){const s=r.comments.find(f=>f.id===Number(n)),p=t?s?.replies?.find(f=>f.at===t&&f.author==="you"):s;return p?(p.text=o,s.updatedAt=new Date().toISOString(),s):null}function q(r,n,t){const o=r.comments.find(s=>s.id===Number(n));return o?(Object.assign(o,t,{updatedAt:new Date().toISOString()}),o):null}function P(r){const n=l(r,"feedback","comments.json"),t=l(r,"feedback","images"),o=l(r,"feedback","archive"),s=l(o,"images"),p=i=>`feedback/images/${i}`;return{root:r,file:n,imageDir:t,archiveDir:o,archiveImageDir:s,load:()=>O(n)?JSON.parse(E(n,"utf8")):{comments:[]},save:i=>{$(T(n),{recursive:!0}),I(`${n}.tmp`,JSON.stringify(i,null,2)+`
2
+ `),J(`${n}.tmp`,n)},attach:(i,d,g)=>{const e=i.comments.find(w=>w.id===Number(d)),c=C(g);if(!e||!c)return null;$(t,{recursive:!0});let a=Date.now();for(;O(l(t,F(e.id,a,c)));)a++;const m=F(e.id,a,c);return I(l(t,m),Buffer.from(c.base64,"base64")),(e.images||=[]).push(p(m)),e.updatedAt=new Date().toISOString(),p(m)},detach:(i,d,g)=>{const e=i.comments.find(m=>m.id===Number(d)),c=h(g),a=c&&e?.images?.find(m=>h(m)===c);return a?(B(l(t,c),{force:!0}),e.images=e.images.filter(m=>m!==a),e.updatedAt=new Date().toISOString(),!0):!1},remove:(i,d)=>{const g=i.comments.find(e=>e.id===Number(d));if(!g)return!1;for(const e of g.images||[])h(e)&&B(l(t,h(e)),{force:!0});return i.comments=i.comments.filter(e=>e.id!==Number(d)),!0},add:G,patch:q,reply:R,edit:_,nextId:N,prune:(i,{dry:d=!1,date:g=new Date().toISOString().slice(0,10)}={})=>{const e=i.comments.filter(u=>u.status==="done"),c=i.comments.filter(u=>u.status!=="done"),a=l(o,`comments-${g}.json`);if(d||!e.length)return{moving:e,keeping:c,archiveFile:a,images:e.flatMap(u=>u.images||[])};$(s,{recursive:!0});const m=[];for(const u of e)u.images=(u.images||[]).map(x=>{const S=h(x);if(!S)return x;const j=l(t,S),M=l(s,S);return O(j)&&J(j,M),m.push(S),`feedback/archive/images/${S}`});const w=O(a)?JSON.parse(E(a,"utf8")).comments||[]:[],b=new Set(w.map(u=>u.id));return I(a,JSON.stringify({comments:[...w,...e.filter(u=>!b.has(u.id))]},null,2)+`
3
+ `),i.nextId=N(i),i.comments=c,{moving:e,keeping:c,archiveFile:a,images:m}}}}export{V as CATEGORIES,Z as STATE_LABEL,G as add,W as categoryOf,P as createStore,_ as edit,F as mediaName,C as mediaOf,N as nextId,q as patch,R as reply,h as shotName,Y as stateOf};
@@ -1 +1 @@
1
- const d=e=>{const n=[{author:"you",text:e.text,at:e.createdAt,by:e.by,byName:e.byName,images:[]},...e.note?[{author:"claude",text:e.note,at:e.updatedAt,images:[]}]:[],...(e.replies||[]).map(t=>({...t,images:[]}))];for(const t of e.images||[]){const o=Number(/-(\d+)\.\w+$/.exec(t)?.[1]);let a=0;n.forEach((r,s)=>{Date.parse(r.at)<=o&&(a=s)}),n[a].images.push(t)}return n},p=e=>e.nickname||e.name||e.email,g=(e,n)=>{const t=String(e||"").toLowerCase();return t.includes("@")?[...new Set(n.filter(o=>[o.name,o.nickname,o.email].some(a=>{if(!a)return!1;const r=t.indexOf("@"+a.toLowerCase());return r!==-1&&!/[\p{L}\p{N}_.-]/u.test(t[r+a.length+1]??"")})).map(o=>o.email))]:[]},i={claude:"Claude",codex:"Codex",gemini:"Gemini",cursor:"Cursor",copilot:"Copilot",windsurf:"Windsurf",amp:"Amp",aider:"Aider",opencode:"OpenCode",agent:"AI agent"},l=e=>i[e]||(e?"AI agent":"Claude"),m=2*36e5,x=(e,n=Date.now())=>e?.working&&e.status!=="done"&&n-Date.parse(e.working.at)<m?e.working:null,u=e=>/@guest\.notemend\.com$/i.test(String(e||"")),c=e=>u(e.by)&&!e.approvedBy;export{i as AGENTS,m as WORKING_TTL,l as agentName,p as handleOf,c as held,u as isGuest,g as mentionsIn,d as threadOf,x as workingOn};
1
+ const w=e=>{const t=[{author:"you",text:e.text,at:e.createdAt,by:e.by,byName:e.byName,images:[]},...e.note?[{author:"claude",text:e.note,at:e.updatedAt,images:[]}]:[],...(e.replies||[]).map(n=>({...n,images:[]}))];for(const n of e.images||[]){const r=Number(/-(\d+)\.\w+$/.exec(n)?.[1]);let o=0;t.forEach((s,i)=>{Date.parse(s.at)<=r&&(o=i)}),t[o].images.push(n)}return t},h=e=>e.nickname||e.name||e.email,y=(e,t)=>{const n=String(e||"").toLowerCase();return n.includes("@")?[...new Set(t.filter(r=>[r.name,r.nickname,r.email].some(o=>{if(!o)return!1;const s=n.indexOf("@"+o.toLowerCase());return s!==-1&&!/[\p{L}\p{N}_.-]/u.test(n[s+o.length+1]??"")})).map(r=>r.email))]:[]},p={claude:"Claude",codex:"Codex",gemini:"Gemini",cursor:"Cursor",copilot:"Copilot",windsurf:"Windsurf",amp:"Amp",aider:"Aider",opencode:"OpenCode",agent:"AI agent"},l=e=>p[e]||(e?"AI agent":"Claude"),a=e=>typeof e=="string"&&e.startsWith("agent:")?e.slice(6):null,u=e=>e==="claude"||a(e)!=null,d=/^[^\s@]+@[^\s@]+$/,A=e=>{if(e==="claude")return e;if(typeof e!="string"||e.length>210)return null;const t=e.toLowerCase();return d.test(a(t)??t)?t:null},g=["new","approved","review"],m=e=>g.includes(e?.stage)?e.stage:u(e?.assignee)?"approved":"new",C=(e,t)=>{const n=a(e.assignee);return m(e)==="approved"&&(!e.assignee||e.assignee==="claude"||n!=null&&(t===void 0||n===t))&&!f(e)},b=(e,t,n=r=>r)=>l(e)+(t?` \xB7 ${n(t)}`:""),c=2*36e5,O=(e,t=Date.now())=>e?.working&&e.status!=="done"&&t-Date.parse(e.working.at)<c?e.working:null,x=e=>/@guest\.notemend\.com$/i.test(String(e||"")),f=e=>x(e.by)&&!e.approvedBy,k={plugin:"Claude Code: /plugin \u2192 Installed \u2192 notemend \u2192 Update now, then /reload-plugins",npx:'run it as npx -y notemend@latest (npx keeps an old copy of plain "notemend"), then restart the agent',project:"npm i -D notemend@latest in the site, then restart the agent",desktop:"Claude Desktop: download app.notemend.com/notemend.mcpb again and open it",source:"pull the notemend repository"},N=(e,t)=>{const n=String(e).split(".").map(Number),r=String(t).split(".").map(Number);for(let o=0;o<3;o++)if((n[o]||0)!==(r[o]||0))return(n[o]||0)>(r[o]||0);return!1};export{p as AGENTS,g as STAGES,k as UPDATE_HOW,c as WORKING_TTL,b as agentLabel,l as agentName,a as agentOwner,A as cleanAssignee,C as forAgentOf,h as handleOf,f as held,x as isGuest,y as mentionsIn,N as newer,m as stageOf,w as threadOf,u as toAgent,O as workingOn};
package/thread.d.ts CHANGED
@@ -7,7 +7,20 @@ export function mentionsIn(text: string, people: Mentionable[]): string[];
7
7
  export const AGENTS: Record<string, string>;
8
8
  /** The name to show for an agent's message: its `agent`, or Claude when there is none */
9
9
  export function agentName(id?: string | null): string;
10
- export type Working = { agent: string; by?: string | null; at: string };
10
+ /** Whose agent a thread is for: the email in 'agent:<email>', else null */
11
+ export function agentOwner(assignee?: string | null): string | null;
12
+ /** Given to an agent: the shared queue ('claude') or a person's own ('agent:<email>') */
13
+ export function toAgent(assignee?: string | null): boolean;
14
+ /** A valid assignee, lower-cased — 'claude', an email or 'agent:<email>' — else null */
15
+ export function cleanAssignee(v: unknown): string | null;
16
+ /** Where a thread stands: new, approved for an agent, or waiting for a person's review */
17
+ export const STAGES: string[];
18
+ export function stageOf(c: { stage?: string | null; assignee?: string | null } | null | undefined): "new" | "approved" | "review";
19
+ /** A thread for the agent running as `me` (null: a CI key, the shared queue only; undefined: a local store, all) */
20
+ export function forAgentOf(c: { assignee?: string | null; stage?: string | null; by?: string | null; approvedBy?: string | null }, me?: string | null): boolean;
21
+ /** "Claude · Ann": the agent, and the person it ran as */
22
+ export function agentLabel(agent?: string | null, owner?: string | null, nameOf?: (email: string) => string): string;
23
+ export type Working = { agent: string; by?: string | null; at: string; v?: string };
11
24
  /** How long a "working" mark holds before it counts as left behind, in ms */
12
25
  export const WORKING_TTL: number;
13
26
  /** The agent working on an open thread now, or null when nobody is or the mark is stale */
@@ -16,3 +29,7 @@ export function workingOn(c: { working?: Working | null; status?: string } | nul
16
29
  export function isGuest(email?: string | null): boolean;
17
30
  /** A guest's thread the studio has not approved yet: no agent takes it */
18
31
  export function held(c: { by?: string | null; approvedBy?: string | null }): boolean;
32
+ /** How each kind of install is brought up to date: plugin, npx, project, desktop, source */
33
+ export const UPDATE_HOW: Record<string, string>;
34
+ /** Is version a newer than b (x.y.z) */
35
+ export function newer(a: string, b: string): boolean;