notemend 0.1.1 → 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/README.md +34 -1
- package/bin/init.mjs +66 -24
- package/bin/notemend.mjs +20 -12
- package/package.json +1 -1
- package/src/client/api.js +1 -1
- package/src/client/notemend.css +1 -1
- package/src/client/run.js +65 -54
- package/src/server/middleware.mjs +1 -1
- package/src/store/cloud.mjs +3 -3
- package/src/store/comments.mjs +3 -3
- package/src/store/status.mjs +1 -1
- package/src/store/thread.mjs +1 -1
- package/thread.d.ts +4 -0
package/src/store/cloud.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import{readFileSync as
|
|
2
|
-
`),ae=e=>T(P(e),{force:!0}),U=()=>{try{const e=JSON.parse(
|
|
3
|
-
`,{mode:384})},ie=()=>T(E,{force:!0});async function x(e,{method:n="GET",token:
|
|
1
|
+
import{readFileSync as M,writeFileSync as j,existsSync as H,mkdirSync as O,rmSync as T}from"node:fs";import{homedir as q}from"node:os";import{dirname as F,join as k}from"node:path";import{createStore as K,add as R,reply as W,edit as z,patch as Y,shotName as _}from"./comments.mjs";const Z=process.env.NOTEMEND_APP||"https://app.notemend.com",S={url:process.env.NOTEMEND_URL||"https://xzhnjldvkirrydbyrdnm.supabase.co",key:process.env.NOTEMEND_KEY||"sb_publishable_b8h8jTHZ9KtbPvMuroLhqA_-yBqZwd6"},E=k(process.env.NOTEMEND_HOME||k(q(),".config","notemend"),"session.json"),P=e=>k(e,".notemend.json"),G=e=>{try{return JSON.parse(M(P(e),"utf8")).project||null}catch{return null}},se=(e,n)=>j(P(e),JSON.stringify({project:n},null,2)+`
|
|
2
|
+
`),ae=e=>T(P(e),{force:!0}),U=()=>{try{const e=JSON.parse(M(E,"utf8"));return e.url===S.url?e:null}catch{return null}},Q=e=>{O(F(E),{recursive:!0}),j(E,JSON.stringify(e,null,2)+`
|
|
3
|
+
`,{mode:384})},ie=()=>T(E,{force:!0});async function x(e,{method:n="GET",token:d,body:o,headers:f={},raw:i=!1}={}){const c=await fetch(`${S.url}${e}`,{method:n,headers:{apikey:S.key,authorization:`Bearer ${d||S.key}`,...o&&!i?{"content-type":"application/json"}:{},...f},body:i?o:o&&JSON.stringify(o)});if(!c.ok){const u=await c.text();let w=u;try{const l=JSON.parse(u);w=l.msg||l.message||l.error_description||l.error||u}catch{}const y=new Error(`${w} (${c.status})`);throw y.status=c.status,y}return c}const D=e=>{const n={url:S.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 Q(n),n},ce=e=>x("/auth/v1/otp",{method:"POST",body:{email:e,create_user:!0}}),ue=async(e,n)=>D(await(await x("/auth/v1/verify",{method:"POST",body:{email:e,token:n,type:"email"}})).json());async function le(e){const{createServer:n}=await import("node:http"),{randomBytes:d}=await import("node:crypto"),o=d(16).toString("hex"),f=(i,c)=>`<!doctype html><meta charset="utf-8"><title>${i}</title><body style="font:15px system-ui;display:grid;place-items:center;height:90vh;background:#0b0b0b;color:#ededed"><div style="text-align:center"><h2>${i}</h2><p style="color:#8f8f8f">${c}</p></div>`;return new Promise((i,c)=>{const u=n(async(l,b)=>{if(l.method!=="POST"||!l.url.startsWith("/callback")){b.writeHead(404).end();return}let $="";for await(const v of l)$+=v;const g=new URLSearchParams($);if(g.get("state")!==o){b.writeHead(403,{"content-type":"text/html"}).end(f("Not this run","Run npx notemend login again."));return}try{const v=await(await x("/auth/v1/user",{token:g.get("access_token")})).json(),B=D({user:v,access_token:g.get("access_token"),refresh_token:g.get("refresh_token"),expires_at:Number(g.get("expires_at"))});b.writeHead(200,{"content-type":"text/html"}).end(f("You are signed in","Back to the terminal \u2014 you can close this tab.")),y(),i(B)}catch(v){b.writeHead(400,{"content-type":"text/html"}).end(f("Could not sign in",v.message)),y(),c(v)}}),w=setTimeout(()=>{y(),c(new Error("No answer from the browser in 5 minutes."))},300*1e3),y=()=>{clearTimeout(w),u.close()};u.listen(0,"127.0.0.1",()=>{const l=u.address().port;console.log(`Waiting on 127.0.0.1:${l} \u2014 the browser page should show the same port.`),e(`${Z}/cli?port=${l}&state=${o}`)})})}async function me(e,n){const d=await A();if(!d)throw new Error("Sign in first: npx notemend login");return(await(await x("/rest/v1/projects",{method:"POST",token:d.access_token,headers:{prefer:"return=representation"},body:{name:e,...n?{site_url:n}:{}}})).json())[0]}async function A(){const e=U();if(!e)return null;if(e.expires_at-60>Date.now()/1e3)return e;try{const n=await x("/auth/v1/token?grant_type=refresh_token",{method:"POST",body:{refresh_token:e.refresh_token}});return D(await n.json())}catch(n){if(n.status===400||n.status===401)return null;throw n}}class J extends Error{}function V(e,n,d=null){const o=k(e,"feedback","images"),f=t=>`feedback/images/${t}`,i=new WeakMap,c=d?async()=>d:async()=>{const t=await A();if(!t)throw new Error("Not signed in to Notemend \u2014 run `npx notemend login`.");return t},u=async(t,r={})=>x(`/rest/v1/${t}`,{...r,token:(await c()).access_token}),w=async(t,r={})=>x(`/storage/v1/object/shots/${n}/${t}`,{...r,token:(await c()).access_token}),y=async()=>{const t=await(await u(`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 i.set(r,t[0].version),r},l=async()=>(await(await u(`stores?project_id=eq.${n}&select=version`)).json())[0]?.version??null,b=async t=>{const r=i.get(t),a=await(await u(`stores?project_id=eq.${n}&version=eq.${r}`,{method:"PATCH",body:{doc:t},headers:{prefer:"return=representation"}})).json();if(!a.length)throw new J("Someone else wrote first");i.set(t,a[0].version)},$=async(t,r,a)=>{const s=t.comments.find(L=>L.id===Number(r)),p=/^data:(?:image|video)\/(png|jpeg|webp|gif|webm);base64,(.+)$/s.exec(a||"");if(!s||!p)return null;const m=p[1]==="jpeg"?"jpg":p[1];let h=Date.now();for(;H(k(o,`${s.id}-${h}.${m}`));)h++;const N=`${s.id}-${h}.${m}`,C=Buffer.from(p[2],"base64");return await w(N,{method:"POST",body:C,raw:!0,headers:{"content-type":p[1]==="webm"?"video/webm":`image/${p[1]}`,"x-upsert":"true"}}),O(o,{recursive:!0}),j(k(o,N),C),(s.images||=[]).push(f(N)),s.updatedAt=new Date().toISOString(),f(N)},g=async t=>{_(t)&&(T(k(o,t),{force:!0}),await w(t,{method:"DELETE"}).catch(()=>{}))};return{cloud:!0,project:n,root:e,imageDir:o,load:y,version:l,save:b,attach:$,detach:async(t,r,a)=>{const s=t.comments.find(h=>h.id===Number(r)),p=_(a),m=p&&s?.images?.find(h=>_(h)===p);return m?(await g(p),s.images=s.images.filter(h=>h!==m),s.updatedAt=new Date().toISOString(),!0):!1},remove:async(t,r)=>{const a=t.comments.find(s=>s.id===Number(r));if(!a)return!1;for(const s of a.images||[])await g(_(s));return t.comments=t.comments.filter(s=>s.id!==Number(r)),!0},fetchImage:async t=>{if(!_(t)||_(t)!==t)return null;const r=k(o,t);if(H(r))return r;const a=await w(t).catch(()=>null);return a?(O(o,{recursive:!0}),j(r,Buffer.from(await a.arrayBuffer())),r):null},people:async()=>{const t=await c(),[r,a]=await Promise.all([u("profiles?select=email,name,nickname,avatar_url").then(m=>m.json()),u("rpc/project_people",{method:"POST",body:{p:n}}).then(m=>m.json()).catch(()=>null)]),s=Array.isArray(a)?new Set(a):null,p=s?r.filter(m=>s.has(m.email)):r;return{me:r.find(m=>m.email===t.email)??{email:t.email,name:"",avatar_url:null},team:p}},add:R,reply:W,edit:z,patch:Y}}let I=!1;async function de(e){const n=G(e);return n&&await A()?V(e,n):(n&&!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`.")),K(e))}async function pe(e,n){for(let d=0;;d++){const o=await e.load(),f=await n(o);try{return await e.save(o),f}catch(i){if(!(i instanceof J)||d>=4)throw i}}}export{S as BACKEND,Z as CABINET,J as Conflict,le as browserLogin,V as cloudStore,me as createProject,se as link,G as linkedProject,ie as logout,de as openStore,pe as retry,ce as sendCode,A 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 v,existsSync as x,mkdirSync as
|
|
2
|
-
`),J(`${t}.tmp`,t)},attach:(n,
|
|
3
|
-
`),n.nextId=y(n),n.comments=
|
|
1
|
+
import{readFileSync as E,writeFileSync as v,existsSync as x,mkdirSync as O,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 w=r=>{const t=String(r??"").split(/[/\\]/).pop();return/^[\w-][\w.-]*\.(png|jpe?g|webp|gif|webm)$/i.test(t)?t:""},y=r=>Math.max(r.nextId||0,r.comments.reduce((t,s)=>Math.max(t,s.id),0)+1);function L(r,{route:t,selector:s,rx:a,ry:o,text:u,label:g,viewport:I,breakpoint:N,browser:$,device:D,tag:A,classes:n,nth:d,category:l,suggest:e,assignee:i,ctx:m}){const c={id:y(r),route:t,selector:s,rx:a,ry:o,text:u,tag:A||"",classes:n||[],nth:Number.isInteger(d)?d:null,label:g||"",ctx:m||"",viewport:I||null,breakpoint:N||"desktop",browser:$||"",device:D||"",category:T(l).id,status:"open",note:"",replies:[],doneBy:null,images:[],...e?{suggest:e}:{},...i?{assignee:i}:{},createdAt:new Date().toISOString(),updatedAt:null};return r.comments.push(c),c}function C(r,t,s,a,o){const u=r.comments.find(g=>g.id===Number(t));return u?((u.replies||=[]).push({author:s,text:a,at:new Date().toISOString(),...o&&o!=="claude"?{agent:o}:{}}),u.updatedAt=new Date().toISOString(),u):null}function G(r,t,s,a){const o=r.comments.find(g=>g.id===Number(t)),u=s?o?.replies?.find(g=>g.at===s&&g.author==="you"):o;return u?(u.text=a,o.updatedAt=new Date().toISOString(),o):null}function R(r,t,s){const a=r.comments.find(o=>o.id===Number(t));return a?(Object.assign(a,s,{updatedAt:new Date().toISOString()}),a):null}function H(r){const t=p(r,"feedback","comments.json"),s=p(r,"feedback","images"),a=p(r,"feedback","archive"),o=p(a,"images"),u=n=>`feedback/images/${n}`;return{root:r,file:t,imageDir:s,archiveDir:a,archiveImageDir:o,load:()=>x(t)?JSON.parse(E(t,"utf8")):{comments:[]},save:n=>{O(M(t),{recursive:!0}),v(`${t}.tmp`,JSON.stringify(n,null,2)+`
|
|
2
|
+
`),J(`${t}.tmp`,t)},attach:(n,d,l)=>{const e=n.comments.find(b=>b.id===Number(d)),i=/^data:(?:image|video)\/(png|jpeg|webp|gif|webm);base64,(.+)$/s.exec(l||"");if(!e||!i)return null;O(s,{recursive:!0});const m=i[1]==="jpeg"?"jpg":i[1];let c=Date.now();for(;x(p(s,`${e.id}-${c}.${m}`));)c++;const S=`${e.id}-${c}.${m}`;return v(p(s,S),Buffer.from(i[2],"base64")),(e.images||=[]).push(u(S)),e.updatedAt=new Date().toISOString(),u(S)},detach:(n,d,l)=>{const e=n.comments.find(c=>c.id===Number(d)),i=w(l),m=i&&e?.images?.find(c=>w(c)===i);return m?(B(p(s,i),{force:!0}),e.images=e.images.filter(c=>c!==m),e.updatedAt=new Date().toISOString(),!0):!1},remove:(n,d)=>{const l=n.comments.find(e=>e.id===Number(d));if(!l)return!1;for(const e of l.images||[])w(e)&&B(p(s,w(e)),{force:!0});return n.comments=n.comments.filter(e=>e.id!==Number(d)),!0},add:L,patch:R,reply:C,edit:G,nextId:y,prune:(n,{dry:d=!1,date:l=new Date().toISOString().slice(0,10)}={})=>{const e=n.comments.filter(f=>f.status==="done"),i=n.comments.filter(f=>f.status!=="done"),m=p(a,`comments-${l}.json`);if(d||!e.length)return{moving:e,keeping:i,archiveFile:m,images:e.flatMap(f=>f.images||[])};O(o,{recursive:!0});const c=[];for(const f of e)f.images=(f.images||[]).map(k=>{const h=w(k);if(!h)return k;const j=p(s,h),F=p(o,h);return x(j)&&J(j,F),c.push(h),`feedback/archive/images/${h}`});const S=x(m)?JSON.parse(E(m,"utf8")).comments||[]:[],b=new Set(S.map(f=>f.id));return v(m,JSON.stringify({comments:[...S,...e.filter(f=>!b.has(f.id))]},null,2)+`
|
|
3
|
+
`),n.nextId=y(n),n.comments=i,{moving:e,keeping:i,archiveFile:m,images:c}}}}export{Q as CATEGORIES,X as STATE_LABEL,L as add,U as categoryOf,H as createStore,G as edit,y as nextId,R as patch,C as reply,w as shotName,W as stateOf};
|
package/src/store/status.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
const o=e=>e.status!=="done"?"open":e.doneBy==="claude"?"claude":"done",
|
|
1
|
+
const o=e=>e.status!=="done"?"open":e.doneBy==="claude"?"claude":"done",n={open:"Open",done:"Resolved",claude:"Resolved by an agent"};export{n as STATE_LABEL,o as stateOf};
|
package/src/store/thread.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
const m=e=>{const
|
|
1
|
+
const m=e=>{const o=[{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 a=Number(/-(\d+)\.\w+$/.exec(t)?.[1]);let n=0;o.forEach((r,s)=>{Date.parse(r.at)<=a&&(n=s)}),o[n].images.push(t)}return o},u=e=>e.nickname||e.name||e.email,d=(e,o)=>{const t=String(e||"").toLowerCase();return t.includes("@")?[...new Set(o.filter(a=>[a.name,a.nickname,a.email].some(n=>{if(!n)return!1;const r=t.indexOf("@"+n.toLowerCase());return r!==-1&&!/[\p{L}\p{N}_.-]/u.test(t[r+n.length+1]??"")})).map(a=>a.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");export{i as AGENTS,l as agentName,u as handleOf,d as mentionsIn,m as threadOf};
|
package/thread.d.ts
CHANGED
|
@@ -3,3 +3,7 @@ export type Mentionable = { email: string; name?: string | null; nickname?: stri
|
|
|
3
3
|
export function handleOf(p: Mentionable): string;
|
|
4
4
|
/** Everyone a text @mentions, by email */
|
|
5
5
|
export function mentionsIn(text: string, people: Mentionable[]): string[];
|
|
6
|
+
/** Display names of the coding agents, by id */
|
|
7
|
+
export const AGENTS: Record<string, string>;
|
|
8
|
+
/** The name to show for an agent's message: its `agent`, or Claude when there is none */
|
|
9
|
+
export function agentName(id?: string | null): string;
|