notemend 0.1.7 → 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/init.mjs +6 -6
- package/bin/notemend.mjs +18 -14
- package/commands/turbo.md +22 -0
- package/package.json +3 -2
- 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 +19 -12
- package/src/client/notemend.css +1 -1
- package/src/client/run.js +68 -58
- 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/store.d.ts +2 -0
- package/thread.d.ts +4 -0
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:
|
|
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
|
|
2
|
-
`),J(`${t}.tmp`,t)},attach:(r,d,l)=>{const e=r.comments.find(
|
|
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/store.d.ts
CHANGED
|
@@ -15,6 +15,8 @@ export interface Comment {
|
|
|
15
15
|
doneBy: string | null; doneAgent?: string | null;
|
|
16
16
|
images: string[]; suggest?: { from: string; to: string }; assignee?: string;
|
|
17
17
|
working?: Working | null; logs?: LogLine[];
|
|
18
|
+
/** the copy of the site it was written on — live, staging, localhost — as host[:port] */
|
|
19
|
+
host?: string;
|
|
18
20
|
by?: string; byName?: string; createdAt: string; updatedAt: string | null;
|
|
19
21
|
}
|
|
20
22
|
export interface Db { comments: Comment[]; nextId?: number }
|
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;
|