notemend 0.1.8 → 0.1.9
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/.claude-plugin/plugin.json +1 -1
- package/bin/notemend.mjs +14 -14
- package/package.json +1 -1
- package/src/client/anchor.js +1 -0
- package/src/client/boot.js +1 -1
- package/src/client/config.js +1 -1
- package/src/client/field-menu.js +1 -0
- package/src/client/inspect.js +1 -0
- package/src/client/markup.js +1 -0
- package/src/client/notemend.css +1 -1
- package/src/client/run.js +72 -71
- package/src/server/middleware.mjs +1 -1
- package/src/store/cloud.mjs +3 -3
- package/src/store/comments.mjs +3 -3
- package/src/store/thread.mjs +1 -1
- package/thread.d.ts +4 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
import{readFileSync as b,existsSync as D,statSync as W,readdirSync as z}from"node:fs";import{mask as K}from"../store/mask.mjs";import{join as k,isAbsolute as V}from"node:path";import{isIP as R}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 H,SHELLS as M,BROWSERS as G}from"../presets.mjs";const J={png:"image/png",jpg:"image/jpeg",jpeg:"image/jpeg",webp:"image/webp",gif:"image/gif",svg:"image/svg+xml",webm:"video/webm"},U=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=b(V(n)?n:k(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=>k(t,u)).find(D),l=[],m=(u,f)=>{for(const i of z(u,{withFileTypes:!0})){const c=i.name;if(i.isDirectory()){if(/^[_@]|^\(\./.test(c))continue;m(k(u,c),/^\(.+\)$/.test(c)?f:[...f,c])}else/^page\.(jsx?|tsx?|mdx?)$/.test(c)&&l.push({origin:"project",type:"page",pattern:"/"+f.join("/"),entrypoint:k(u,c)})}};return n&&m(n,[]),l}const rt=t=>t[0].toUpperCase()+t.slice(1).replace(/[-_]+/g," ");function st({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})),i=new Set(f.map(e=>e.route)),c=[...f,...Object.keys(m).filter(e=>!i.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,I=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",...I.map(e=>e.group).sort()])],g=I.sort((e,d)=>r.indexOf(e.group)-r.indexOf(d.group)||U(e.name)-U(d.name)||e.route.localeCompare(d.route)),s=(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??H[e.id]??H.desktop})),a=(n.devices??et).filter(e=>e.w>0&&e.h>0&&M.includes(e.shell)&&G.includes(e.browser)?!0:(console.warn(`[notemend] device "${e.id}" skipped: it needs w and h, shell (${M.join(", ")}) and browser (${G.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:s,devices:a,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 ot=t=>{const n=k(t,"notemend.config.mjs");let l=0,m={};return async()=>{if(!D(n))return{};const u=W(n).mtimeMs;if(u===l)return m;l=u;try{const{default:f,...i}=await import(/* webpackIgnore: true */ /* turbopackIgnore: true */ `${Z(n).href}?t=${u}`);m={...i,...f}}catch(f){console.warn(`[notemend] notemend.config.mjs: ${f.message}`)}return m}},at=t=>{const n=String(t.headers.host||"").toLowerCase(),l=n.startsWith("[")?n.slice(1,n.indexOf("]")):n.replace(/:\d+$/,"");return l.includes(":")?R(l)===6:!l||!l.includes(".")||R(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),it=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),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)),"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=ot(t),i=(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 s=async a=>{const e=await g(a);if(e.status>=400)throw Object.assign(new Error("not written"),{out:e});return e};try{if(r.cloud)return await X(r,s);const a=r.load(),e=await s(a);return r.save(a),e}catch(a){if(a.out)return a.out;throw a}},P=async(r,g)=>{if(r.method!=="GET"&&!u&&ut(r))return c(403,"Cross-origin write refused");const s=await m();if(r.method==="GET"){const o=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 i({version:h,comments:o?w.comments.filter(j=>j.route===o):w.comments,listening:w.listening??null,turbo:w.turbo??null})}let a=null,e=null;const d=s.cloud&&r.method!=="DELETE"?s.people().then(o=>{a=o,e=o.me},o=>{throw o}).catch(o=>o):Promise.resolve(),$=async()=>{const o=await d;if(o instanceof Error)throw o},B=o=>{if(e&&o){o.by=e.email,o.byName=e.name;const h=Y(o.text,a.team.filter(w=>w.email!==e.email));h.length&&(o.mentions=h)}return o};if(r.method==="POST"){const{image:o,images:h,...w}=await N(r),j=it(w);return j.text.trim()?j.route.startsWith("/")?E(s,async C=>{await $();const A=B(s.add(C,j));for(const O of[].concat(h??o??[]))await s.attach(C,A.id,O);return i(A,201)}):i({error:"route must be a path"},400):i({error:"empty comment"},400)}if(r.method==="PATCH"){const o=await N(r);if(o.turbo==="on"||o.turbo==="off")return E(s,async S=>(S.turbo={paused:o.turbo==="off",at:new Date().toISOString()},i({turbo:S.turbo})));const{id:h,reply:w,image:j,images:C,removeImage:A,edit:O,...F}=o,x=Number(h);if(!Number.isInteger(x))return i({error:"bad id"},400);const v=ct(F);return E(s,async S=>{if(await $(),O){const y=S.comments.find(T=>T.id===Number(x)),L=O.at?y?.replies?.find(T=>T.at===O.at):y;if(e&&L?.by&&L.by!==e.email||!(O.text?.trim?.()&&s.edit(S,x,O.at??null,O.text.trim().slice(0,1e4))))return i({error:`no message of yours to edit in #${x}`},404)}w?.trim?.()&&B(s.reply(S,x,"you",w.trim().slice(0,1e4))?.replies.at(-1));for(const y of[].concat(C??j??[]))await s.attach(S,x,y);for(const y of[].concat(A||[]))await s.detach(S,x,y);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 y=S.comments.find(L=>L.id===x);y&&y.status!=="done"&&Object.assign(v,{status:"done",doneBy:"you",working:null}),y?.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||!w?s.patch(S,x,v):S.comments.find(y=>y.id===Number(x));return _?i(_):i({error:`no comment #${x}`},404)})}if(r.method==="DELETE"){const o=Number(g.searchParams.get("id"));return E(s,async h=>await s.remove(h,o)?i({ok:!0,id:o}):i({error:`no comment #${o}`},404))}return c(405,"Method not allowed")},I=(r,g)=>{const s=g.replace(/^.*[/\\]/,""),a=s.split(".").pop()?.toLowerCase()??"";if(!/^[\w.-]+$/.test(s)||!J[a])return c(400,"Bad name");const e=k(r,s);return D(e)?{status:200,type:J[a],body:b(e)}:c(404,"Not found")};return async r=>{const g=new URL(r.url,"http://devbar"),s=g.pathname.slice(10);if(!u&&!at(r))return c(403,"Notemend answers local addresses only");try{if(s==="config"){const a=st({root:t,options:{...await f(),...n},routes:l()}),e=await m();return e.cloud&&Object.assign(a,await e.people().catch(()=>({}))),i(a)}if(s==="comments")return await P(r,g);if(s==="image"){const a=await m(),e=(g.searchParams.get("name")||"").replace(/^.*[/\\]/,"");return a.cloud&&/^[\w.-]+$/.test(e)&&await a.fetchImage(e).catch(()=>null),I(a.imageDir,e)}return c(404,"Not found")}catch(a){return a.status===400||a.status===413?c(a.status,a.message):(console.error("[notemend]",a),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 i=[];m.on("data",c=>i.push(c)),m.on("end",()=>u(Buffer.concat(i))),m.on("error",f)});return async function(u,f,i){if(!u.url||!u.url.startsWith("/__devbar/"))return i();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{st 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 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};
|
package/src/store/cloud.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import{readFileSync as D,writeFileSync as
|
|
2
|
-
`),ae=e=>$(
|
|
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(`${
|
|
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};
|
package/src/store/comments.mjs
CHANGED
|
@@ -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:
|
|
2
|
-
`),J(`${t}.tmp`,t)},attach:(r,d,l)=>{const e=r.comments.find(h=>h.id===Number(d)),
|
|
3
|
-
`),r.nextId=I(r),r.comments=
|
|
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};
|
package/src/store/thread.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
const
|
|
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};
|
package/thread.d.ts
CHANGED
|
@@ -12,3 +12,7 @@ export type Working = { agent: string; by?: string | null; at: string };
|
|
|
12
12
|
export const WORKING_TTL: number;
|
|
13
13
|
/** The agent working on an open thread now, or null when nobody is or the mark is stale */
|
|
14
14
|
export function workingOn(c: { working?: Working | null; status?: string } | null | undefined, now?: number): Working | null;
|
|
15
|
+
/** A guest's address: someone let in by link, without an account */
|
|
16
|
+
export function isGuest(email?: string | null): boolean;
|
|
17
|
+
/** A guest's thread the studio has not approved yet: no agent takes it */
|
|
18
|
+
export function held(c: { by?: string | null; approvedBy?: string | null }): boolean;
|