notemend 0.1.5 → 0.1.6
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 +17 -0
- package/.mcp.json +8 -0
- package/README.md +7 -0
- package/bin/init.mjs +6 -5
- package/bin/mcp.mjs +2 -2
- package/bin/notemend.mjs +16 -14
- package/hooks/hooks.json +10 -0
- package/package.json +10 -3
- package/presets.d.ts +5 -1
- package/src/client/boot.js +1 -1
- package/src/client/logs.js +1 -0
- package/src/client/notemend.css +1 -1
- package/src/client/run.js +43 -41
- package/src/server/middleware.mjs +1 -1
- package/src/store/cloud.mjs +3 -3
- package/src/store/comments.mjs +3 -3
- package/src/store/mask.mjs +1 -0
- package/src/store/thread.mjs +1 -1
- package/store.d.ts +52 -0
- package/thread.d.ts +5 -0
- package/types.d.ts +1 -1
package/src/store/cloud.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import{readFileSync as
|
|
2
|
-
`),ae=e
|
|
3
|
-
`,{mode:384})},ie=()
|
|
1
|
+
import{readFileSync as D,writeFileSync as S,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 Y,patch as Z,shotName as v}from"./comments.mjs";const M=process.env.NOTEMEND_APP||"https://app.notemend.com",g={url:process.env.NOTEMEND_URL||"https://xzhnjldvkirrydbyrdnm.supabase.co",key:process.env.NOTEMEND_KEY||"sb_publishable_b8h8jTHZ9KtbPvMuroLhqA_-yBqZwd6"},b=h(process.env.NOTEMEND_HOME||h(L(),".config","notemend"),"session.json"),N=e=>h(e,".notemend.json"),G=e=>{try{return JSON.parse(D(N(e),"utf8")).project||null}catch{return null}},se=(e,n)=>S(N(e),JSON.stringify({project:n},null,2)+`
|
|
2
|
+
`),ae=e=>$(N(e),{force:!0}),R=()=>{try{const e=JSON.parse(D(b,"utf8"));return e.url===g.url?e:null}catch{return null}},W=e=>{j(F(b),{recursive:!0}),S(b,JSON.stringify(e,null,2)+`
|
|
3
|
+
`,{mode:384})},ie=()=>$(b,{force:!0});async function w(e,{method:n="GET",token:a,body:s,headers:l={},raw:i=!1}={}){const m=await fetch(`${g.url}${e}`,{method:n,headers:{apikey:g.key,authorization:`Bearer ${a||g.key}`,...s&&!i?{"content-type":"application/json"}:{},...l},body:i?s:s&&JSON.stringify(s)});if(!m.ok){const d=await m.text();let y=d;try{const k=JSON.parse(d);y=k.msg||k.message||k.error_description||k.error||d}catch{}const _=new Error(`${y} (${m.status})`);throw _.status=m.status,_}return m}const E=e=>{const n={url:g.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 W(n),n},ce=e=>w("/auth/v1/otp",{method:"POST",body:{email:e,create_user:!0}}),ue=async(e,n)=>E(await(await w("/auth/v1/verify",{method:"POST",body:{email:e,token:n,type:"email"}})).json());async function le(e){const n=await fetch(`${M}/api/cli/device`,{method:"POST"}),a=await n.json().catch(()=>({}));if(!n.ok)throw new Error(a.error||`The cabinet said ${n.status}`);console.log(`Your code: ${a.code}`),e(a.url);const s=Date.now()+a.expires_in*1e3;for(;Date.now()<s;){await new Promise(d=>setTimeout(d,a.interval*1e3));const l=await fetch(`${M}/api/cli/device?device=${a.device}`).catch(()=>null);if(!l||l.status===202)continue;const i=await l.json().catch(()=>({}));if(!l.ok)throw new Error(i.error||`The cabinet said ${l.status}`);const m=await(await w("/auth/v1/user",{token:i.access_token})).json();return E({user:m,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 fe(e,n){const a=await O();if(!a)throw new Error("Sign in first: npx notemend login");return(await(await w("/rest/v1/projects",{method:"POST",token:a.access_token,headers:{prefer:"return=representation"},body:{name:e,...n?{site_url:n}:{}}})).json())[0]}async function O(){const e=R();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 E(await n.json())}catch(n){if(n.status===400||n.status===401)return null;throw n}}class B extends Error{}function Q(e,n,a=null){const s=h(e,"feedback","images"),l=t=>`feedback/images/${t}`,i=new WeakMap,m=a?async()=>a:async()=>{const t=await O();if(!t)throw new Error("Not signed in to Notemend \u2014 run `npx notemend login`.");return t},d=async(t,r={})=>w(`/rest/v1/${t}`,{...r,token:(await m()).access_token}),y=async(t,r={})=>w(`/storage/v1/object/shots/${n}/${t}`,{...r,token:(await m()).access_token}),_=async()=>{const t=await(await d(`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},k=async()=>(await(await d(`stores?project_id=eq.${n}&select=version`)).json())[0]?.version??null,q=async t=>{const r=i.get(t),c=await(await d(`stores?project_id=eq.${n}&version=eq.${r}`,{method:"PATCH",body:{doc:t},headers:{prefer:"return=representation"}})).json();if(!c.length)throw new B("Someone else wrote first");i.set(t,c[0].version)},J=async(t,r,c)=>{const o=t.comments.find(I=>I.id===Number(r)),f=/^data:(?:image|video)\/(png|jpeg|webp|gif|webm);base64,(.+)$/s.exec(c||"");if(!o||!f)return null;const u=f[1]==="jpeg"?"jpg":f[1];let p=Date.now();for(;A(h(s,`${o.id}-${p}.${u}`));)p++;const x=`${o.id}-${p}.${u}`,P=Buffer.from(f[2],"base64");return await y(x,{method:"POST",body:P,raw:!0,headers:{"content-type":f[1]==="webm"?"video/webm":`image/${f[1]}`,"x-upsert":"true"}}),j(s,{recursive:!0}),S(h(s,x),P),(o.images||=[]).push(l(x)),o.updatedAt=new Date().toISOString(),l(x)},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:_,version:k,save:q,attach:J,detach:async(t,r,c)=>{const o=t.comments.find(p=>p.id===Number(r)),f=v(c),u=f&&o?.images?.find(p=>v(p)===f);return u?(await T(f),o.images=o.images.filter(p=>p!==u),o.updatedAt=new Date().toISOString(),!0):!1},remove:async(t,r)=>{const c=t.comments.find(o=>o.id===Number(r));if(!c)return!1;for(const o of c.images||[])await T(v(o));return t.comments=t.comments.filter(o=>o.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 c=await y(t).catch(()=>null);return c?(j(s,{recursive:!0}),S(r,Buffer.from(await c.arrayBuffer())),r):null},people:async()=>{const t=await m(),[r,c]=await Promise.all([d("profiles?select=email,name,nickname,avatar_url").then(u=>u.json()),d("rpc/project_people",{method:"POST",body:{p:n}}).then(u=>u.json()).catch(()=>null)]),o=Array.isArray(c)?new Set(c):null,f=o?r.filter(u=>o.has(u.email)):r;return{me:r.find(u=>u.email===t.email)??{email:t.email,name:"",avatar_url:null},team:f}},add:K,reply:z,edit:Y,patch:Z}}let C=!1;async function me(e){const n=G(e);return n&&await O()?Q(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 a=0;;a++){const s=await e.load(),l=await n(s);try{return await e.save(s),l}catch(i){if(!(i instanceof B)||a>=4)throw i}}}export{g as BACKEND,M as CABINET,B as Conflict,Q as cloudStore,fe as createProject,le as deviceLogin,se as link,G 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
|
|
2
|
-
`),J(`${t}.tmp`,t)},attach:(
|
|
3
|
-
`),
|
|
1
|
+
import{readFileSync as E,writeFileSync as O,existsSync as x,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 w=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:a,ry:o,text:u,label:S,viewport:y,breakpoint:N,browser:$,device:D,tag:A,classes:r,nth:d,category:l,suggest:e,assignee:i,ctx:m,logs:c}){const g={id:I(n),route:t,selector:s,rx:a,ry:o,text:u,tag:A||"",classes:r||[],nth:Number.isInteger(d)?d:null,label:S||"",ctx:m||"",viewport:y||null,breakpoint:N||"desktop",browser:$||"",device:D||"",category:T(l).id,status:"open",note:"",replies:[],doneBy:null,images:[],...e?{suggest:e}:{},...i?{assignee:i}:{},...c?.length?{logs:c}:{},createdAt:new Date().toISOString(),updatedAt:null};return n.comments.push(g),n.nextId=g.id+1,g}function C(n,t,s,a,o){const u=n.comments.find(S=>S.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(n,t,s,a){const o=n.comments.find(S=>S.id===Number(t)),u=s?o?.replies?.find(S=>S.at===s&&S.author==="you"):o;return u?(u.text=a,o.updatedAt=new Date().toISOString(),o):null}function R(n,t,s){const a=n.comments.find(o=>o.id===Number(t));return a?(Object.assign(a,s,{updatedAt:new Date().toISOString()}),a):null}function H(n){const t=p(n,"feedback","comments.json"),s=p(n,"feedback","images"),a=p(n,"feedback","archive"),o=p(a,"images"),u=r=>`feedback/images/${r}`;return{root:n,file:t,imageDir:s,archiveDir:a,archiveImageDir:o,load:()=>x(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(v=>v.id===Number(d)),i=/^data:(?:image|video)\/(png|jpeg|webp|gif|webm);base64,(.+)$/s.exec(l||"");if(!e||!i)return null;b(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 g=`${e.id}-${c}.${m}`;return O(p(s,g),Buffer.from(i[2],"base64")),(e.images||=[]).push(u(g)),e.updatedAt=new Date().toISOString(),u(g)},detach:(r,d,l)=>{const e=r.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:(r,d)=>{const l=r.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 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(f=>f.status==="done"),i=r.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||[])};b(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 g=x(m)?JSON.parse(E(m,"utf8")).comments||[]:[],v=new Set(g.map(f=>f.id));return O(m,JSON.stringify({comments:[...g,...e.filter(f=>!v.has(f.id))]},null,2)+`
|
|
3
|
+
`),r.nextId=I(r),r.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,I as nextId,R as patch,C as reply,w as shotName,W as stateOf};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const s=/([?&#;](?:[^=&#;]*(?:token|key|secret|passw|auth|session|sig|code|jwt|credential)[^=&#;]*)=)[^&#;\s]*/gi,a=e=>String(e??"").replace(/(\/\/)[^/@\s]+@/g,"$1\u2022\u2022\u2022@").replace(s,"$1\u2022\u2022\u2022").replace(/\b(Bearer|Basic|Token)\s+[\w.~+/=-]+/gi,"$1 \u2022\u2022\u2022").replace(/\beyJ[\w-]+\.[\w-]+\.[\w-]+/g,"\u2022\u2022\u2022");export{a as mask};
|
package/src/store/thread.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
const
|
|
1
|
+
const u=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},d=e=>e.nickname||e.name||e.email,p=(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,g=(e,n=Date.now())=>e?.working&&e.status!=="done"&&n-Date.parse(e.working.at)<m?e.working:null;export{i as AGENTS,m as WORKING_TTL,l as agentName,d as handleOf,p as mentionsIn,u as threadOf,g as workingOn};
|
package/store.d.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { Working } from './thread.js';
|
|
2
|
+
|
|
3
|
+
/** One reply in a thread: `author: 'claude'` is any agent, `agent` says which */
|
|
4
|
+
export interface Reply { author: 'you' | 'claude'; text: string; at: string; by?: string; byName?: string; agent?: string; mentions?: string[] }
|
|
5
|
+
/** A line the page logged before the comment was written (the bar's logs) */
|
|
6
|
+
export interface LogLine { at: string; kind: 'console' | 'error' | 'rejection' | 'network'; text: string }
|
|
7
|
+
|
|
8
|
+
/** A thread as feedback/comments.json and the cloud keep it */
|
|
9
|
+
export interface Comment {
|
|
10
|
+
id: number; route: string; selector: string; rx: number; ry: number; text: string;
|
|
11
|
+
tag: string; classes: string[]; nth: number | null; label: string; ctx: string;
|
|
12
|
+
viewport: { w: number; h: number } | null; breakpoint: string; browser: string; device: string;
|
|
13
|
+
category: string; status: 'open' | 'done'; note: string; replies: Reply[];
|
|
14
|
+
/** who closed it: 'claude' (an agent, via the CLI) or 'you' (a person) */
|
|
15
|
+
doneBy: string | null; doneAgent?: string | null;
|
|
16
|
+
images: string[]; suggest?: { from: string; to: string }; assignee?: string;
|
|
17
|
+
working?: Working | null; logs?: LogLine[];
|
|
18
|
+
by?: string; byName?: string; createdAt: string; updatedAt: string | null;
|
|
19
|
+
}
|
|
20
|
+
export interface Db { comments: Comment[]; nextId?: number }
|
|
21
|
+
export type NewComment = Partial<Omit<Comment, 'id' | 'status' | 'replies' | 'images' | 'createdAt' | 'updatedAt'>> & Pick<Comment, 'route' | 'selector' | 'rx' | 'ry' | 'text'>;
|
|
22
|
+
|
|
23
|
+
export interface Category { id: string; label: string; color: string }
|
|
24
|
+
export const CATEGORIES: Category[];
|
|
25
|
+
export function categoryOf(id?: string | null): Category;
|
|
26
|
+
/** open, done (by a person) or claude (by an agent, waiting for a look) */
|
|
27
|
+
export function stateOf(c: Pick<Comment, 'status' | 'doneBy'>): 'open' | 'done' | 'claude';
|
|
28
|
+
export const STATE_LABEL: Record<'open' | 'done' | 'claude', string>;
|
|
29
|
+
|
|
30
|
+
/** A screenshot's plain file name from a stored path, or '' when it is not one we wrote */
|
|
31
|
+
export function shotName(path: string): string;
|
|
32
|
+
/** The next thread's number: never one a deleted or archived thread had */
|
|
33
|
+
export function nextId(db: Db): number;
|
|
34
|
+
export function add(db: Db, comment: NewComment): Comment;
|
|
35
|
+
export function reply(db: Db, id: number | string, author: 'you' | 'claude', text: string, agent?: string): Comment | null;
|
|
36
|
+
/** Reword the comment (`at` null) or one of your replies (by when it was written) */
|
|
37
|
+
export function edit(db: Db, id: number | string, at: string | null, text: string): Comment | null;
|
|
38
|
+
export function patch(db: Db, id: number | string, changes: Partial<Comment>): Comment | null;
|
|
39
|
+
|
|
40
|
+
/** One project's store on disk: <root>/feedback/comments.json and feedback/images/ */
|
|
41
|
+
export function createStore(root: string): {
|
|
42
|
+
root: string; file: string; imageDir: string; archiveDir: string; archiveImageDir: string;
|
|
43
|
+
load(): Db;
|
|
44
|
+
save(db: Db): void;
|
|
45
|
+
/** file a data: URL screenshot (or webm) under the comment; returns its path or null */
|
|
46
|
+
attach(db: Db, id: number | string, dataUrl: string): string | null;
|
|
47
|
+
detach(db: Db, id: number | string, name: string): boolean;
|
|
48
|
+
remove(db: Db, id: number | string): boolean;
|
|
49
|
+
/** move resolved threads to feedback/archive/, leaving the id floor behind */
|
|
50
|
+
prune(db: Db, opts?: { dry?: boolean; date?: string }): { moving: Comment[]; keeping: Comment[]; archiveFile: string; images: string[] };
|
|
51
|
+
add: typeof add; patch: typeof patch; reply: typeof reply; edit: typeof edit; nextId: typeof nextId;
|
|
52
|
+
};
|
package/thread.d.ts
CHANGED
|
@@ -7,3 +7,8 @@ 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 };
|
|
11
|
+
/** How long a "working" mark holds before it counts as left behind, in ms */
|
|
12
|
+
export const WORKING_TTL: number;
|
|
13
|
+
/** The agent working on an open thread now, or null when nobody is or the mark is stale */
|
|
14
|
+
export function workingOn(c: { working?: Working | null; status?: string } | null | undefined, now?: number): Working | null;
|
package/types.d.ts
CHANGED
|
@@ -57,4 +57,4 @@ export interface NotemendOptions {
|
|
|
57
57
|
/** Everything but `enabled` can also live in `notemend.config.mjs` at the project root
|
|
58
58
|
* (named exports or a default object), re-read on every page load. Options passed
|
|
59
59
|
* here win over the file. */
|
|
60
|
-
export default function notemend(options?: NotemendOptions):
|
|
60
|
+
export default function notemend(options?: NotemendOptions): import('astro').AstroIntegration;
|