claude-code-session-manager 0.107.0 → 0.109.0
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/dist/assets/AgentLibrary-Br4Ispr4.js +2 -0
- package/dist/assets/{Button-CRLhoI2K.js → Button-C8Mc6MEP.js} +1 -1
- package/dist/assets/DataModel-Bz0MypYO.js +1 -0
- package/dist/assets/History-DT26Iy38.js +2 -0
- package/dist/assets/Hooks-e4xUGEf-.js +3 -0
- package/dist/assets/{Library-rqaJJJ1b.js → Library-B3rzEhvN.js} +24 -24
- package/dist/assets/{ListDetail-qI27OYfy.js → ListDetail-BAvci_aP.js} +1 -1
- package/dist/assets/{MarkdownEditor-BEFuHVNz.js → MarkdownEditor-DBWTGgRz.js} +1 -1
- package/dist/assets/{McpServers-Bz9O79rz.js → McpServers-C-grBN21.js} +2 -2
- package/dist/assets/{Memory-DobxIoZ-.js → Memory-BmM0Fz-C.js} +5 -5
- package/dist/assets/{Panel-Cyt1wpUf.js → Panel-Dc6F8wb4.js} +1 -1
- package/dist/assets/Permissions-m_NnldFh.js +3 -0
- package/dist/assets/Plugins-BB0TLUC9.js +2 -0
- package/dist/assets/ProvenanceBadge-k9PH0S2n.js +1 -0
- package/dist/assets/{SaveBar-BuYrg5E9.js → SaveBar-DSMnbUVI.js} +1 -1
- package/dist/assets/Scheduler-CbIcJEfM.js +16 -0
- package/dist/assets/{ScopeSwitcher-BM0gLPhd.js → ScopeSwitcher-tHtloWbN.js} +1 -1
- package/dist/assets/Settings-CW3w7knp.js +6 -0
- package/dist/assets/{SkillReferenceGraph-D0Hgdfxq.js → SkillReferenceGraph-Dndl1tzr.js} +14 -14
- package/dist/assets/Skills-CGnxroik.js +2 -0
- package/dist/assets/SystemPrompt-B3A3rHNp.js +1 -0
- package/dist/assets/TagLibrary-k4CTiAYo.js +1 -0
- package/dist/assets/{TiptapBody-CP_F_mwb.js → TiptapBody-C2LfNQXw.js} +1 -1
- package/dist/assets/{Toggle-haRPHS5N.js → Toggle-CTXTU1q5.js} +1 -1
- package/dist/assets/{ViewTabs-BHV3MA5u.js → ViewTabs-CfEfOSE1.js} +1 -1
- package/dist/assets/historyTotals-Db0xy2Fg.js +1 -0
- package/dist/assets/{index-tr1w3hHB.css → index-C8Smiz7z.css} +1 -1
- package/dist/assets/{index-v_3LvhMx.js → index-fJxuoRvi.js} +697 -697
- package/dist/assets/listSkills-C1aWKf0T.js +1 -0
- package/dist/assets/{settingsSchema-CE46TA3V.js → settingsSchema-xxO9p4yF.js} +1 -1
- package/dist/assets/useAgentsDirWatch-DLS1_moj.js +1 -0
- package/dist/index.html +2 -2
- package/package.json +5 -2
- package/plugins/session-manager-dev/skills/develop/standards.md +5 -0
- package/scripts/README.md +7 -0
- package/scripts/scheduler-mcp-server.cjs +38 -5
- package/src/main/agentLibrary.cjs +20 -9
- package/src/main/build-info.json +4 -4
- package/src/main/config.cjs +20 -10
- package/src/main/historyAggregator.cjs +46 -3
- package/src/main/index.cjs +2 -0
- package/src/main/ipcSchemas.cjs +2 -0
- package/src/main/lib/builtinMacros.cjs +2 -2
- package/src/main/lib/mcpToolCatalog.cjs +3 -1
- package/src/main/lib/remoteE2eCrypto.cjs +109 -0
- package/src/main/lib/shippedPersonaSeeds.cjs +11 -0
- package/src/main/pty.cjs +53 -0
- package/src/main/remoteClient.cjs +305 -0
- package/src/main/remoteClientIpc.cjs +101 -0
- package/src/main/webRemote.cjs +90 -108
- package/src/preload/api.d.ts +42 -5
- package/src/preload/index.cjs +27 -2
- package/src/seed/agents/demo-video-builder.md +69 -48
- package/dist/assets/AgentLibrary-Bjeq2-kp.js +0 -2
- package/dist/assets/DataModel-iPJw5jcG.js +0 -1
- package/dist/assets/History-DfPR3_6N.js +0 -2
- package/dist/assets/Hooks-Bw7H_DmC.js +0 -3
- package/dist/assets/Permissions-ChSNRUJJ.js +0 -3
- package/dist/assets/Plugins-CII221R2.js +0 -2
- package/dist/assets/ProvenanceBadge-C_sk5-v3.js +0 -1
- package/dist/assets/Scheduler-eixbMGEk.js +0 -16
- package/dist/assets/Settings-qD-0W-Ut.js +0 -4
- package/dist/assets/Skills-C3T_2_2o.js +0 -3
- package/dist/assets/SystemPrompt-B-xTy9Qn.js +0 -1
- package/dist/assets/TagLibrary-BH67tR5z.js +0 -1
- package/dist/assets/listSkills-QORduPIk.js +0 -1
- package/dist/assets/useAgentsDirWatch-BZTi2K2e.js +0 -1
- package/dist/assets/useConfigDirWatch-CjcswsWR.js +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const p=a=>a instanceof Error?a.message:String(a);async function y(a,w={}){const d=w.nested??!0,s=[],c=async t=>{try{const r=await window.api.config.listDir(t,{dirsOnly:!0});return r.ok?r.entries:(s.push({path:t,error:r.error??"failed to list directory"}),[])}catch(r){return s.push({path:t,error:p(r)}),[]}};let o=0;const l=[],u=async t=>{var r;o>=6&&await new Promise(n=>l.push(n)),o++;try{return await window.api.config.readText(t)}catch(n){return s.push({path:t,error:p(n)}),null}finally{o--,(r=l.shift())==null||r()}};return{entries:(await Promise.all((await c(a)).map(async t=>{const r=`${t.path}/SKILL.md`,n=await u(r);return n!=null&&n.exists?[{name:t.name,path:r,dir:t.path,text:n.text}]:d?(await Promise.all((await c(t.path)).map(async e=>{const f=`${e.path}/SKILL.md`,i=await u(f);return i!=null&&i.exists?{name:e.name,path:f,dir:e.path,text:i.text}:null}))).filter(e=>e!==null):[]}))).flat(),errors:s}}export{y as l};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{aw as b,ah as n,S as N,aN as O}from"./index-v_3LvhMx.js";import{r as K}from"./claude-settings-schema-COigsajx.js";function G({node:e,targetScope:r,onOverride:s,schema:a,rootLabel:m}){const[f,x]=b.useState(""),[u,o]=b.useState(!0),g=[],d=e.kind==="object"&&Object.keys(e.children).length===0,y=((a==null?void 0:a.rootKeys())??[]).length>0;return n.jsxs("div",{className:"h-full flex flex-col",children:[n.jsxs("div",{className:"shrink-0 border-b border-line bg-bg-elev px-3 py-1.5 flex items-center gap-2 text-xs",children:[n.jsxs("span",{className:"text-fg-faint",children:["merged · overrides → ",n.jsx("span",{className:"text-fg-dim",children:N[r].label})]}),y&&n.jsxs("label",{className:"flex items-center gap-1 text-fg-faint cursor-pointer",children:[n.jsx("input",{type:"checkbox",checked:u,onChange:t=>o(t.target.checked),className:"accent-accent"}),"show unset keys"]}),n.jsx("div",{className:"flex-1"}),n.jsx("input",{value:f,onChange:t=>x(t.target.value),placeholder:"filter keys or descriptions…",className:"bg-bg border border-line rounded px-2 py-0.5 text-xs text-fg w-56"})]}),n.jsx("div",{className:"flex-1 overflow-auto px-3 py-2 font-mono text-xs",children:d&&!u?n.jsx("div",{className:"text-fg-faint italic px-2 py-4",children:"no settings resolved — all scope files empty or missing"}):n.jsx(E,{node:e,path:g,label:m??"(root)",targetScope:r,onOverride:s,filter:f,depth:0,schema:a,showGhosts:u,defaultOpen:!0})})]})}function E(e){return e.node.kind==="object"?n.jsx(P,{...e,node:e.node}):e.node.kind==="array"?n.jsx(J,{...e,node:e.node}):n.jsx(R,{...e,node:e.node})}function A(e,r){return r.trim()?e.toLowerCase().includes(r.toLowerCase()):!0}function w(e,r,s){return!!(!s.trim()||e.some(a=>A(a,s))||r!=null&&r.description&&A(r.description,s))}function P({node:e,path:r,label:s,targetScope:a,onOverride:m,filter:f,depth:x,schema:u,showGhosts:o,defaultOpen:g}){const[d,h]=b.useState(g??x<2),y=(u==null?void 0:u.at(r))??null,t=new Set(Object.keys(e.children)),i=o?(y==null?void 0:y.knownChildren)??[]:[],c=Array.from(new Set([...t,...i])).sort(),l=b.useMemo(()=>f.trim()?c.filter(p=>{const v=(u==null?void 0:u.at([...r,p]))??null;return w([...r,p],v,f)?!0:t.has(p)?L(e.children[p],[...r,p],f,u):!1}):c,[c.join(","),f,e,r.join("."),u]);if(f.trim()&&l.length===0&&!w(r,y,f))return null;if(r.length===0)return n.jsx("div",{children:l.map(p=>j(p))});return n.jsxs("div",{children:[n.jsxs("div",{className:"flex items-center gap-1 py-0.5 hover:bg-bg-elev/50 rounded cursor-pointer",style:{paddingLeft:x*14},onClick:()=>h(!d),children:[n.jsx("span",{className:"text-fg-faint w-3 shrink-0",children:d?"▾":"▸"}),n.jsx("span",{className:"text-fg",children:s}),n.jsxs("span",{className:"text-fg-faint",children:[" {",t.size,i.length>t.size?`/${new Set([...t,...i]).size}`:"","}"]}),(y==null?void 0:y.description)&&n.jsxs("span",{className:"text-fg-faint truncate max-w-md",title:y.description,children:["— ",y.description]})]}),d&&l.map(p=>j(p))]});function j(p){if(t.has(p))return n.jsx(E,{node:e.children[p],path:[...r,p],label:p,targetScope:a,onOverride:m,filter:f,depth:x+1,schema:u,showGhosts:o},p);const v=(u==null?void 0:u.at([...r,p]))??null;return v?n.jsx(I,{path:[...r,p],label:p,info:v,targetScope:a,onSet:m,depth:x+1,filter:f},p):null}}function L(e,r,s,a){const m=(a==null?void 0:a.at(r))??null;if(w(r,m,s))return!0;if(e.kind!=="object")return!1;for(const f of Object.keys(e.children))if(L(e.children[f],[...r,f],s,a))return!0;return!1}function J({node:e,path:r,label:s,filter:a,depth:m,schema:f}){const[x,u]=b.useState(!0),o=(f==null?void 0:f.at(r))??null;return w(r,o,a)?n.jsxs("div",{children:[n.jsxs("div",{className:"flex items-center gap-1 py-0.5 hover:bg-bg-elev/50 rounded cursor-pointer",style:{paddingLeft:m*14},onClick:()=>u(!x),children:[n.jsx("span",{className:"text-fg-faint w-3 shrink-0",children:x?"▾":"▸"}),n.jsx("span",{className:"text-fg",children:s}),n.jsxs("span",{className:"text-fg-faint",children:[" [",e.items.length,"]"]}),n.jsx(S,{tone:"warn",children:"accumulated"}),(o==null?void 0:o.description)&&n.jsxs("span",{className:"text-fg-faint truncate max-w-md",title:o.description,children:["— ",o.description]})]}),x&&e.items.map((g,d)=>n.jsxs("div",{className:"flex items-start gap-2 py-0.5",style:{paddingLeft:(m+1)*14},children:[n.jsx("span",{className:"text-fg-faint w-3 shrink-0"}),n.jsxs("span",{className:"text-fg-faint",children:[d,":"]}),n.jsx("span",{className:"text-fg-dim truncate max-w-xl",children:k(g.value)}),n.jsx(C,{scope:g.fromScope})]},d))]}):null}function R({node:e,path:r,label:s,targetScope:a,onOverride:m,filter:f,depth:x,schema:u}){const[o,g]=b.useState(!1),d=(u==null?void 0:u.at(r))??null;if(!w(r,d,f))return null;const h=e.winner!==a,y=e.shadowed.length>0;return n.jsxs("div",{children:[n.jsxs("div",{className:"group flex items-center gap-2 py-0.5 hover:bg-bg-elev/50 rounded",style:{paddingLeft:x*14},children:[n.jsx("span",{className:`w-3 shrink-0 ${y?"text-fg-faint cursor-pointer":""}`,onClick:()=>y&&g(!o),children:y?o?"▾":"▸":""}),n.jsxs("span",{className:"text-fg",children:[s,":"]}),n.jsx("span",{className:"text-fg-dim truncate max-w-md",children:k(e.value)}),n.jsx(C,{scope:e.winner}),(d==null?void 0:d.deprecated)&&n.jsx(S,{tone:"warn",children:"deprecated"}),(d==null?void 0:d.default)!==void 0&&_(e.value,d.default)&&n.jsx(S,{tone:"dim",children:"= default"}),e.typeMismatch&&n.jsx("span",{className:"text-yellow-500/80",title:"shadowed scopes have a different value type",children:"⚠"}),(d==null?void 0:d.description)&&n.jsxs("span",{className:"text-fg-faint truncate max-w-md",title:d.description,children:["— ",d.description]}),n.jsx("div",{className:"flex-1"}),(d==null?void 0:d.enum)&&n.jsx(F,{values:d.enum}),n.jsx("button",{disabled:!h,onClick:()=>m(r,e.value),className:`opacity-0 group-hover:opacity-100 px-1.5 py-0 text-[10px] border rounded transition-opacity ${h?"border-accent/50 text-accent hover:bg-accent hover:text-bg":"border-line text-fg-faint cursor-not-allowed"}`,title:h?`copy into ${N[a].label} draft`:`already winning in ${N[a].label}`,children:"override"})]}),o&&e.shadowed.map((t,i)=>n.jsxs("div",{className:"flex items-center gap-2 py-0.5 text-fg-faint line-through",style:{paddingLeft:(x+1)*14+12},children:[n.jsx("span",{children:"shadowed:"}),n.jsx("span",{className:"truncate max-w-xl",children:k(t.value)}),n.jsx(C,{scope:t.scope,dim:!0})]},i))]})}function I({path:e,label:r,info:s,targetScope:a,onSet:m,depth:f,filter:x}){if(!w(e,s,x))return null;const u=s.default!==void 0?s.default:M(s.type),o=Array.isArray(s.type)?s.type.join("|"):s.type??"?";return n.jsxs("div",{className:"group flex items-center gap-2 py-0.5 hover:bg-bg-elev/50 rounded opacity-60",style:{paddingLeft:f*14},children:[n.jsx("span",{className:"w-3 shrink-0"}),n.jsx("span",{className:"text-fg-dim",children:r}),n.jsx(S,{tone:"dim",children:o}),s.required&&n.jsx(S,{tone:"warn",children:"required"}),s.default!==void 0&&n.jsxs("span",{className:"text-fg-faint",children:["default: ",k(s.default)]}),s.description&&n.jsxs("span",{className:"text-fg-faint truncate max-w-md",title:s.description,children:["— ",s.description]}),n.jsx("div",{className:"flex-1"}),s.enum&&n.jsx(F,{values:s.enum}),n.jsx("button",{onClick:()=>m(e,u),className:"opacity-0 group-hover:opacity-100 px-1.5 py-0 text-[10px] border border-accent/50 rounded text-accent hover:bg-accent hover:text-bg transition-opacity",title:`write to ${N[a].label} draft`,children:"set"})]})}function _(e,r){return e===r?!0:typeof e!=typeof r?!1:e&&r&&typeof e=="object"?JSON.stringify(e)===JSON.stringify(r):!1}function M(e){switch(Array.isArray(e)?e[0]:e){case"boolean":return!1;case"integer":case"number":return 0;case"string":return"";case"array":return[];case"object":return{};case"null":return null;default:return null}}function k(e){if(typeof e=="string")return JSON.stringify(e);if(e===null)return"null";if(typeof e=="object"){const r=JSON.stringify(e);return r.length>100?r.slice(0,100)+"…":r}return String(e)}function C({scope:e,dim:r}){const s=e==="user"?"bg-bg-hi text-fg-dim border-line":e==="project"?"bg-blue-950/30 text-blue-400/80 border-blue-900/40":"bg-purple-950/30 text-purple-400/80 border-purple-900/40";return n.jsx("span",{className:`text-[10px] px-1 py-0 rounded border uppercase tracking-wide shrink-0 ${s} ${r?"opacity-60":""}`,children:N[e].label})}function S({children:e,tone:r="default"}){const s={default:"bg-bg-hi text-fg-dim border-line",warn:"bg-yellow-950/30 text-yellow-500/80 border-yellow-900/40",dim:"bg-bg-elev text-fg-faint border-line"}[r];return n.jsx("span",{className:`text-[10px] px-1 py-0 rounded border uppercase tracking-wide shrink-0 ${s}`,children:e})}function F({values:e}){const r=e.map(s=>String(s)).join(" | ");return n.jsxs("span",{className:"text-[10px] text-fg-faint truncate max-w-xs",title:`allowed: ${r}`,children:["∈ ",r.length>30?r.slice(0,30)+"…":r]})}function V(e,r,s){const a=O(o=>o.files),m=O(o=>o.loadJson),f=O(o=>o.watchFile),x=O(o=>o.unwatchFile),u=b.useMemo(()=>{const o={};for(const g of e.scopes){const d=e.resolve(g,r??"",s);d&&(o[g]=d)}return o},[e,r,s]);return b.useEffect(()=>{const o=Object.values(u).filter(Boolean);return o.forEach(g=>{a[g]||m(g),f(g)}),()=>{o.forEach(g=>x(g))}},[JSON.stringify(u)]),u}function B(e){const r=e??{},s={...r.$defs??{},...r.definitions??{}},a=t=>{if(!t)return{};if(!t.$ref)return x(t);const i=/^#\/(\$defs|definitions)\/(.+)$/.exec(t.$ref);if(!i)return x(t);const c=s[i[2]];return c?a({...c,...u(t)}):x(t)},m=new Set(["__proto__","constructor","prototype"]),f=t=>{if(!t)return{};const i={};for(const[c,l]of Object.entries(t))m.has(c)||(i[c]=l);return i},x=t=>{if(!t.allOf||t.allOf.length===0)return t;const i={...t};delete i.allOf;for(const c of t.allOf){const l=a(c);Object.assign(i,{properties:{...f(i.properties),...f(l.properties)},required:q([...i.required??[],...l.required??[]])}),!i.description&&l.description&&(i.description=l.description),i.default===void 0&&l.default!==void 0&&(i.default=l.default),!i.type&&l.type&&(i.type=l.type)}return i},u=t=>{const{$ref:i,...c}=t;return c},o=(t,i)=>{let c=a(t);for(const l of i){if(!c)return null;if(/^\d+$/.test(l)){const v=Array.isArray(c.items)?c.items[Number(l)]:c.items;c=v?a(v):null;continue}const j=c.properties;if(j&&!m.has(l)&&l in j){c=a(j[l]);continue}const p=c.additionalProperties;if(p&&typeof p=="object"){c=a(p);continue}return null}return c},g=(t,i,c)=>{if(!t)return null;const l={};if(t.description&&(l.description=t.description),t.type&&(l.type=t.type),t.default!==void 0&&(l.default=t.default),t.enum&&(l.enum=t.enum),t.const!==void 0&&(l.const=t.const),t.deprecated&&(l.deprecated=!0),t.examples&&(l.examples=t.examples),t.properties&&(l.knownChildren=Object.keys(t.properties)),c&&(i!=null&&i.includes(c))&&(l.required=!0),t.items&&!Array.isArray(t.items)){const j=a(t.items),p=g(j,void 0,null);p&&(l.itemsInfo=p)}return(t.additionalProperties===!0||typeof t.additionalProperties=="object")&&(l.open=!0),l},d=t=>i=>{if(i.length===0)return g(a(t),void 0,null);const c=o(t,i.slice(0,-1));if(!c)return null;const l=i[i.length-1],j=o(c,[l]);return g(j,c.required,l)},h=t=>{const i=a(t);return Object.keys(i.properties??{})},y=t=>({at:d(t),rootKeys:()=>h(t),rootedAt:i=>{const c=o(t,i);return y(c??{})}});return y(r)}function q(e){return Array.from(new Set(e))}let $=null;function D(){return $||($=B(K)),$}export{G as E,D as s,V as u};
|
|
1
|
+
import{aD as b,an as n,S as N,aW as O}from"./index-fJxuoRvi.js";import{r as K}from"./claude-settings-schema-COigsajx.js";function D({node:e,targetScope:r,onOverride:s,schema:a,rootLabel:m}){const[f,x]=b.useState(""),[u,o]=b.useState(!0),g=[],d=e.kind==="object"&&Object.keys(e.children).length===0,y=((a==null?void 0:a.rootKeys())??[]).length>0;return n.jsxs("div",{className:"h-full flex flex-col",children:[n.jsxs("div",{className:"shrink-0 border-b border-line bg-bg-elev px-3 py-1.5 flex items-center gap-2 text-xs",children:[n.jsxs("span",{className:"text-fg-faint",children:["merged · overrides → ",n.jsx("span",{className:"text-fg-dim",children:N[r].label})]}),y&&n.jsxs("label",{className:"flex items-center gap-1 text-fg-faint cursor-pointer",children:[n.jsx("input",{type:"checkbox",checked:u,onChange:t=>o(t.target.checked),className:"accent-accent"}),"show unset keys"]}),n.jsx("div",{className:"flex-1"}),n.jsx("input",{value:f,onChange:t=>x(t.target.value),placeholder:"filter keys or descriptions…",className:"bg-bg border border-line rounded px-2 py-0.5 text-xs text-fg w-56"})]}),n.jsx("div",{className:"flex-1 overflow-auto px-3 py-2 font-mono text-xs",children:d&&!u?n.jsx("div",{className:"text-fg-faint italic px-2 py-4",children:"no settings resolved — all scope files empty or missing"}):n.jsx(E,{node:e,path:g,label:m??"(root)",targetScope:r,onOverride:s,filter:f,depth:0,schema:a,showGhosts:u,defaultOpen:!0})})]})}function E(e){return e.node.kind==="object"?n.jsx(P,{...e,node:e.node}):e.node.kind==="array"?n.jsx(J,{...e,node:e.node}):n.jsx(R,{...e,node:e.node})}function A(e,r){return r.trim()?e.toLowerCase().includes(r.toLowerCase()):!0}function w(e,r,s){return!!(!s.trim()||e.some(a=>A(a,s))||r!=null&&r.description&&A(r.description,s))}function P({node:e,path:r,label:s,targetScope:a,onOverride:m,filter:f,depth:x,schema:u,showGhosts:o,defaultOpen:g}){const[d,h]=b.useState(g??x<2),y=(u==null?void 0:u.at(r))??null,t=new Set(Object.keys(e.children)),i=o?(y==null?void 0:y.knownChildren)??[]:[],c=Array.from(new Set([...t,...i])).sort(),l=b.useMemo(()=>f.trim()?c.filter(p=>{const v=(u==null?void 0:u.at([...r,p]))??null;return w([...r,p],v,f)?!0:t.has(p)?L(e.children[p],[...r,p],f,u):!1}):c,[c.join(","),f,e,r.join("."),u]);if(f.trim()&&l.length===0&&!w(r,y,f))return null;if(r.length===0)return n.jsx("div",{children:l.map(p=>j(p))});return n.jsxs("div",{children:[n.jsxs("div",{className:"flex items-center gap-1 py-0.5 hover:bg-bg-elev/50 rounded cursor-pointer",style:{paddingLeft:x*14},onClick:()=>h(!d),children:[n.jsx("span",{className:"text-fg-faint w-3 shrink-0",children:d?"▾":"▸"}),n.jsx("span",{className:"text-fg",children:s}),n.jsxs("span",{className:"text-fg-faint",children:[" {",t.size,i.length>t.size?`/${new Set([...t,...i]).size}`:"","}"]}),(y==null?void 0:y.description)&&n.jsxs("span",{className:"text-fg-faint truncate max-w-md",title:y.description,children:["— ",y.description]})]}),d&&l.map(p=>j(p))]});function j(p){if(t.has(p))return n.jsx(E,{node:e.children[p],path:[...r,p],label:p,targetScope:a,onOverride:m,filter:f,depth:x+1,schema:u,showGhosts:o},p);const v=(u==null?void 0:u.at([...r,p]))??null;return v?n.jsx(I,{path:[...r,p],label:p,info:v,targetScope:a,onSet:m,depth:x+1,filter:f},p):null}}function L(e,r,s,a){const m=(a==null?void 0:a.at(r))??null;if(w(r,m,s))return!0;if(e.kind!=="object")return!1;for(const f of Object.keys(e.children))if(L(e.children[f],[...r,f],s,a))return!0;return!1}function J({node:e,path:r,label:s,filter:a,depth:m,schema:f}){const[x,u]=b.useState(!0),o=(f==null?void 0:f.at(r))??null;return w(r,o,a)?n.jsxs("div",{children:[n.jsxs("div",{className:"flex items-center gap-1 py-0.5 hover:bg-bg-elev/50 rounded cursor-pointer",style:{paddingLeft:m*14},onClick:()=>u(!x),children:[n.jsx("span",{className:"text-fg-faint w-3 shrink-0",children:x?"▾":"▸"}),n.jsx("span",{className:"text-fg",children:s}),n.jsxs("span",{className:"text-fg-faint",children:[" [",e.items.length,"]"]}),n.jsx(S,{tone:"warn",children:"accumulated"}),(o==null?void 0:o.description)&&n.jsxs("span",{className:"text-fg-faint truncate max-w-md",title:o.description,children:["— ",o.description]})]}),x&&e.items.map((g,d)=>n.jsxs("div",{className:"flex items-start gap-2 py-0.5",style:{paddingLeft:(m+1)*14},children:[n.jsx("span",{className:"text-fg-faint w-3 shrink-0"}),n.jsxs("span",{className:"text-fg-faint",children:[d,":"]}),n.jsx("span",{className:"text-fg-dim truncate max-w-xl",children:k(g.value)}),n.jsx(C,{scope:g.fromScope})]},d))]}):null}function R({node:e,path:r,label:s,targetScope:a,onOverride:m,filter:f,depth:x,schema:u}){const[o,g]=b.useState(!1),d=(u==null?void 0:u.at(r))??null;if(!w(r,d,f))return null;const h=e.winner!==a,y=e.shadowed.length>0;return n.jsxs("div",{children:[n.jsxs("div",{className:"group flex items-center gap-2 py-0.5 hover:bg-bg-elev/50 rounded",style:{paddingLeft:x*14},children:[n.jsx("span",{className:`w-3 shrink-0 ${y?"text-fg-faint cursor-pointer":""}`,onClick:()=>y&&g(!o),children:y?o?"▾":"▸":""}),n.jsxs("span",{className:"text-fg",children:[s,":"]}),n.jsx("span",{className:"text-fg-dim truncate max-w-md",children:k(e.value)}),n.jsx(C,{scope:e.winner}),(d==null?void 0:d.deprecated)&&n.jsx(S,{tone:"warn",children:"deprecated"}),(d==null?void 0:d.default)!==void 0&&_(e.value,d.default)&&n.jsx(S,{tone:"dim",children:"= default"}),e.typeMismatch&&n.jsx("span",{className:"text-yellow-500/80",title:"shadowed scopes have a different value type",children:"⚠"}),(d==null?void 0:d.description)&&n.jsxs("span",{className:"text-fg-faint truncate max-w-md",title:d.description,children:["— ",d.description]}),n.jsx("div",{className:"flex-1"}),(d==null?void 0:d.enum)&&n.jsx(F,{values:d.enum}),n.jsx("button",{disabled:!h,onClick:()=>m(r,e.value),className:`opacity-0 group-hover:opacity-100 px-1.5 py-0 text-[10px] border rounded transition-opacity ${h?"border-accent/50 text-accent hover:bg-accent hover:text-bg":"border-line text-fg-faint cursor-not-allowed"}`,title:h?`copy into ${N[a].label} draft`:`already winning in ${N[a].label}`,children:"override"})]}),o&&e.shadowed.map((t,i)=>n.jsxs("div",{className:"flex items-center gap-2 py-0.5 text-fg-faint line-through",style:{paddingLeft:(x+1)*14+12},children:[n.jsx("span",{children:"shadowed:"}),n.jsx("span",{className:"truncate max-w-xl",children:k(t.value)}),n.jsx(C,{scope:t.scope,dim:!0})]},i))]})}function I({path:e,label:r,info:s,targetScope:a,onSet:m,depth:f,filter:x}){if(!w(e,s,x))return null;const u=s.default!==void 0?s.default:M(s.type),o=Array.isArray(s.type)?s.type.join("|"):s.type??"?";return n.jsxs("div",{className:"group flex items-center gap-2 py-0.5 hover:bg-bg-elev/50 rounded opacity-60",style:{paddingLeft:f*14},children:[n.jsx("span",{className:"w-3 shrink-0"}),n.jsx("span",{className:"text-fg-dim",children:r}),n.jsx(S,{tone:"dim",children:o}),s.required&&n.jsx(S,{tone:"warn",children:"required"}),s.default!==void 0&&n.jsxs("span",{className:"text-fg-faint",children:["default: ",k(s.default)]}),s.description&&n.jsxs("span",{className:"text-fg-faint truncate max-w-md",title:s.description,children:["— ",s.description]}),n.jsx("div",{className:"flex-1"}),s.enum&&n.jsx(F,{values:s.enum}),n.jsx("button",{onClick:()=>m(e,u),className:"opacity-0 group-hover:opacity-100 px-1.5 py-0 text-[10px] border border-accent/50 rounded text-accent hover:bg-accent hover:text-bg transition-opacity",title:`write to ${N[a].label} draft`,children:"set"})]})}function _(e,r){return e===r?!0:typeof e!=typeof r?!1:e&&r&&typeof e=="object"?JSON.stringify(e)===JSON.stringify(r):!1}function M(e){switch(Array.isArray(e)?e[0]:e){case"boolean":return!1;case"integer":case"number":return 0;case"string":return"";case"array":return[];case"object":return{};case"null":return null;default:return null}}function k(e){if(typeof e=="string")return JSON.stringify(e);if(e===null)return"null";if(typeof e=="object"){const r=JSON.stringify(e);return r.length>100?r.slice(0,100)+"…":r}return String(e)}function C({scope:e,dim:r}){const s=e==="user"?"bg-bg-hi text-fg-dim border-line":e==="project"?"bg-blue-950/30 text-blue-400/80 border-blue-900/40":"bg-purple-950/30 text-purple-400/80 border-purple-900/40";return n.jsx("span",{className:`text-[10px] px-1 py-0 rounded border uppercase tracking-wide shrink-0 ${s} ${r?"opacity-60":""}`,children:N[e].label})}function S({children:e,tone:r="default"}){const s={default:"bg-bg-hi text-fg-dim border-line",warn:"bg-yellow-950/30 text-yellow-500/80 border-yellow-900/40",dim:"bg-bg-elev text-fg-faint border-line"}[r];return n.jsx("span",{className:`text-[10px] px-1 py-0 rounded border uppercase tracking-wide shrink-0 ${s}`,children:e})}function F({values:e}){const r=e.map(s=>String(s)).join(" | ");return n.jsxs("span",{className:"text-[10px] text-fg-faint truncate max-w-xs",title:`allowed: ${r}`,children:["∈ ",r.length>30?r.slice(0,30)+"…":r]})}function G(e,r,s){const a=O(o=>o.files),m=O(o=>o.loadJson),f=O(o=>o.watchFile),x=O(o=>o.unwatchFile),u=b.useMemo(()=>{const o={};for(const g of e.scopes){const d=e.resolve(g,r??"",s);d&&(o[g]=d)}return o},[e,r,s]);return b.useEffect(()=>{const o=Object.values(u).filter(Boolean);return o.forEach(g=>{a[g]||m(g),f(g)}),()=>{o.forEach(g=>x(g))}},[JSON.stringify(u)]),u}function B(e){const r=e??{},s={...r.$defs??{},...r.definitions??{}},a=t=>{if(!t)return{};if(!t.$ref)return x(t);const i=/^#\/(\$defs|definitions)\/(.+)$/.exec(t.$ref);if(!i)return x(t);const c=s[i[2]];return c?a({...c,...u(t)}):x(t)},m=new Set(["__proto__","constructor","prototype"]),f=t=>{if(!t)return{};const i={};for(const[c,l]of Object.entries(t))m.has(c)||(i[c]=l);return i},x=t=>{if(!t.allOf||t.allOf.length===0)return t;const i={...t};delete i.allOf;for(const c of t.allOf){const l=a(c);Object.assign(i,{properties:{...f(i.properties),...f(l.properties)},required:q([...i.required??[],...l.required??[]])}),!i.description&&l.description&&(i.description=l.description),i.default===void 0&&l.default!==void 0&&(i.default=l.default),!i.type&&l.type&&(i.type=l.type)}return i},u=t=>{const{$ref:i,...c}=t;return c},o=(t,i)=>{let c=a(t);for(const l of i){if(!c)return null;if(/^\d+$/.test(l)){const v=Array.isArray(c.items)?c.items[Number(l)]:c.items;c=v?a(v):null;continue}const j=c.properties;if(j&&!m.has(l)&&l in j){c=a(j[l]);continue}const p=c.additionalProperties;if(p&&typeof p=="object"){c=a(p);continue}return null}return c},g=(t,i,c)=>{if(!t)return null;const l={};if(t.description&&(l.description=t.description),t.type&&(l.type=t.type),t.default!==void 0&&(l.default=t.default),t.enum&&(l.enum=t.enum),t.const!==void 0&&(l.const=t.const),t.deprecated&&(l.deprecated=!0),t.examples&&(l.examples=t.examples),t.properties&&(l.knownChildren=Object.keys(t.properties)),c&&(i!=null&&i.includes(c))&&(l.required=!0),t.items&&!Array.isArray(t.items)){const j=a(t.items),p=g(j,void 0,null);p&&(l.itemsInfo=p)}return(t.additionalProperties===!0||typeof t.additionalProperties=="object")&&(l.open=!0),l},d=t=>i=>{if(i.length===0)return g(a(t),void 0,null);const c=o(t,i.slice(0,-1));if(!c)return null;const l=i[i.length-1],j=o(c,[l]);return g(j,c.required,l)},h=t=>{const i=a(t);return Object.keys(i.properties??{})},y=t=>({at:d(t),rootKeys:()=>h(t),rootedAt:i=>{const c=o(t,i);return y(c??{})}});return y(r)}function q(e){return Array.from(new Set(e))}let $=null;function V(){return $||($=B(K)),$}export{D as E,V as s,G as u};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{V as e,b0 as a,aX as n}from"./index-fJxuoRvi.js";const c=new Set(e.map(t=>t.tag));function l(t){return t.filter(o=>c.has(o))}function s(t,o={}){return{name:t.name,originalName:t.name,description:t.description??"",tools:t.tools,model:t.model??"inherit",effort:t.effort??void 0,color:t.color??"",tags:l(t.tags),projects:t.projects,action:t.action??void 0,actionLabel:t.actionLabel??void 0,title:t.title??void 0,body:t.body,...o}}function d(t,o){const i=a();n([i?`${i}/.claude/agents`:null,t?`${t}/.claude/agents`:null],o)}export{s as a,l as t,d as u};
|
package/dist/index.html
CHANGED
|
@@ -7,10 +7,10 @@
|
|
|
7
7
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
8
8
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
9
9
|
<link href="https://fonts.googleapis.com/css2?family=Newsreader:ital,opsz,wght@0,6..72,400;0,6..72,500;0,6..72,600;0,6..72,700;1,6..72,400&family=Geist:wght@300;400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
|
10
|
-
<script type="module" crossorigin src="./assets/index-
|
|
10
|
+
<script type="module" crossorigin src="./assets/index-fJxuoRvi.js"></script>
|
|
11
11
|
<link rel="modulepreload" crossorigin href="./assets/monaco-editor-BsIlgG7c.js">
|
|
12
12
|
<link rel="stylesheet" crossorigin href="./assets/monaco-editor-Cu5D__1H.css">
|
|
13
|
-
<link rel="stylesheet" crossorigin href="./assets/index-
|
|
13
|
+
<link rel="stylesheet" crossorigin href="./assets/index-C8Smiz7z.css">
|
|
14
14
|
</head>
|
|
15
15
|
<body class="bg-bg text-fg font-sans antialiased">
|
|
16
16
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-code-session-manager",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.109.0",
|
|
4
4
|
"description": "Local cockpit for the Claude Code CLI — multi-tab terminal, full config surface, scheduler, voice dictation, and live observability.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/main/index.cjs",
|
|
@@ -67,7 +67,9 @@
|
|
|
67
67
|
"lint:main-ts-check": "node scripts/check-main-ts-check.cjs",
|
|
68
68
|
"lint:filenames": "node scripts/check-tracked-filenames.cjs",
|
|
69
69
|
"lint:renderer-storage": "node scripts/check-renderer-storage.cjs",
|
|
70
|
-
"lint": "
|
|
70
|
+
"lint:test-smells": "node scripts/test-quality/smells.cjs --check tests/quality/smell-allowlist.json",
|
|
71
|
+
"lint:page-stewards": "node scripts/check-page-stewards.cjs",
|
|
72
|
+
"lint": "npm run lint:selectors && npm run lint:hooks && npm run lint:docs && npm run lint:paths && npm run lint:any && npm run lint:main-ts-check && npm run lint:filenames && npm run lint:renderer-storage && npm run lint:test-smells && npm run lint:page-stewards",
|
|
71
73
|
"sync:settings-schema": "node scripts/sync-settings-schema.cjs"
|
|
72
74
|
},
|
|
73
75
|
"keywords": [
|
|
@@ -147,6 +149,7 @@
|
|
|
147
149
|
"@types/react": "^18.3.12",
|
|
148
150
|
"@types/react-dom": "^18.3.1",
|
|
149
151
|
"@vitejs/plugin-react": "^4.3.3",
|
|
152
|
+
"@vitest/coverage-v8": "4.1.6",
|
|
150
153
|
"@xterm/addon-fit": "^0.10.0",
|
|
151
154
|
"@xterm/addon-web-links": "^0.11.0",
|
|
152
155
|
"@xterm/xterm": "^5.5.0",
|
|
@@ -39,6 +39,11 @@
|
|
|
39
39
|
- Do not write production code without a test asserting the behavior it adds.
|
|
40
40
|
- (Interactive sessions: the `test-driven-development` skill has the full red-green-refactor
|
|
41
41
|
workflow. Headless PRD runs can't load it — the three rules above are the load-bearing core.)
|
|
42
|
+
- Put the red test in the nearest existing scenario test for that workflow.
|
|
43
|
+
- Create a new test file only for a new module or a new workflow.
|
|
44
|
+
- Name tests and files by behaviour, never by PRD id.
|
|
45
|
+
- Never assert on production source text; a static invariant is a `scripts/check-*.cjs` lint.
|
|
46
|
+
- Full rubric (what is bloat, how to delete safely): the `## Test value — good test vs bloat` section of `tests/README.md`.
|
|
42
47
|
|
|
43
48
|
## Visual design (UI/visual acceptance criteria)
|
|
44
49
|
|
package/scripts/README.md
CHANGED
|
@@ -10,6 +10,8 @@ scripts/lib was moved into src/main/lib (2026-09); nothing under src/ may requir
|
|
|
10
10
|
| --- | --- | --- |
|
|
11
11
|
| audit-ops-hygiene.cjs | manual; named in `ops-sweep` SKILL.md | yes |
|
|
12
12
|
| bench/main-bench.cjs | `npm run bench` (main-process micro-benches, plain Node); renderer half is `npm run bench:renderer` (`tests/bench/`, `vitest.bench.config.ts`, not in `test:unit`) | no |
|
|
13
|
+
| page-surface.cjs | manual and by the page steward personas `.claude/agents/page-*.md`; derives a page's owned/shared files, window.api methods, main handlers and tests | no |
|
|
14
|
+
| check-page-stewards.cjs | `npm run lint:page-stewards` (in `lint`) — every nav page has a valid `.claude/agents/page-<key>.md` steward persona; ratchets PENDING | no |
|
|
13
15
|
| check-conditional-hooks.cjs | `npm run lint:hooks` (in `lint`) | no |
|
|
14
16
|
| check-epic-transcripts.cjs | manual read-only diagnostic (`--json`); also surfaced as a non-fatal warning in `npm run health` | no |
|
|
15
17
|
| check-doc-hierarchy.cjs | `npm run lint:docs` (in `lint`) | no |
|
|
@@ -32,6 +34,11 @@ scripts/lib was moved into src/main/lib (2026-09); nothing under src/ may requir
|
|
|
32
34
|
| sm-ps.cjs | manual process-tree inspector | no |
|
|
33
35
|
| sync-settings-schema.cjs | `npm run sync:settings-schema` | no |
|
|
34
36
|
| write-build-info.cjs | npm `prepack` and `prepublishOnly` | no |
|
|
37
|
+
| test-quality/smells.cjs | `npm run lint:test-smells` (in `lint`) — static test-smell scan; ratchets source-text/no-assert against `tests/quality/smell-allowlist.json` | no |
|
|
38
|
+
| test-quality/coverage-map.cjs | manual, dev-only — per-test-file v8 coverage and unique-coverage attribution | no |
|
|
39
|
+
| test-quality/mutants.cjs | manual, dev-only — seeded mutation sampler; which test files kill which mutants | no |
|
|
40
|
+
| test-quality/coverage-guard.cjs | manual, dev-only — `--base <ref>`; fails if a test-only diff drops covered statements/branches | no |
|
|
41
|
+
| test-quality/report.cjs | manual, dev-only — keep/review/merge/bloat verdict per test file from the coverage map | no |
|
|
35
42
|
| hooks/guard-destructive-git.cjs | `.claude/settings.json` PreToolUse + shim | yes |
|
|
36
43
|
| hooks/guard-inline-implementation.cjs | `.claude/settings.json` PreToolUse + shim | yes |
|
|
37
44
|
| hooks/guard-prd-writes.cjs | `.claude/settings.json` PreToolUse + shim | yes |
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* feedback_open_session({ ... }) -> POST /admin/feedback/open-session
|
|
13
13
|
*
|
|
14
14
|
* project_home_write({ cwd?, html }) -> POST /admin/project-home/write
|
|
15
|
-
* project_demo_video_write({ cwd?, html }) -> POST /admin/project-home/demo-video/write
|
|
15
|
+
* project_demo_video_write({ cwd?, html? | htmlPath? }) -> POST /admin/project-home/demo-video/write
|
|
16
16
|
*
|
|
17
17
|
* macro_list({ cwd? }) -> GET /admin/macros?cwd=<encoded>
|
|
18
18
|
* macro_save({ cwd?, id?, label, agentName, tag, prompt }) -> POST /admin/macros/save
|
|
@@ -55,6 +55,24 @@ function withPointer(text) {
|
|
|
55
55
|
return str.endsWith(HELP_POINTER) ? str : str + HELP_POINTER;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
const MAX_DEMO_VIDEO_BYTES = 2 * 1024 * 1024;
|
|
59
|
+
|
|
60
|
+
/** Reads a local .html/.htm file for project_demo_video_write; errors carry only the reason, never file contents. */
|
|
61
|
+
async function readDemoVideoHtmlFile(p) {
|
|
62
|
+
if (typeof p !== 'string' || !path.isAbsolute(p)) return { error: 'htmlPath must be an absolute path' };
|
|
63
|
+
if (!/\.html?$/i.test(p)) return { error: 'htmlPath must end in .html or .htm' };
|
|
64
|
+
try {
|
|
65
|
+
const st = await fsp.stat(p);
|
|
66
|
+
if (!st.isFile()) return { error: 'htmlPath is not a regular file' };
|
|
67
|
+
if (st.size > MAX_DEMO_VIDEO_BYTES) return { error: `htmlPath file exceeds ${MAX_DEMO_VIDEO_BYTES} bytes` };
|
|
68
|
+
const html = await fsp.readFile(p, 'utf8');
|
|
69
|
+
if (html.length === 0) return { error: 'htmlPath file is empty' };
|
|
70
|
+
return { html };
|
|
71
|
+
} catch (err) {
|
|
72
|
+
return { error: `cannot read htmlPath: ${err && err.code ? err.code : 'read failed'}` };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
58
76
|
function errorResult(text) {
|
|
59
77
|
return { content: [{ type: 'text', text: withPointer(text) }], isError: true };
|
|
60
78
|
}
|
|
@@ -396,8 +414,12 @@ const TOOLS = [
|
|
|
396
414
|
+ 'any http(s)/// src or href (no network-capable APIs — the document is CSP-fenced after it is written); '
|
|
397
415
|
+ 'or missing a <meta name="sm-demo-duration" content="N"> tag with 5 <= N <= 30.',
|
|
398
416
|
},
|
|
417
|
+
htmlPath: {
|
|
418
|
+
type: 'string',
|
|
419
|
+
description: 'Absolute path to a local .html/.htm file holding the complete document (same rules as html; <= 2MB). '
|
|
420
|
+
+ 'Prefer this when the document embeds a base64 narration/image data URI — the server reads the file, so the large payload never passes through the model. Give exactly one of html / htmlPath.',
|
|
421
|
+
},
|
|
399
422
|
},
|
|
400
|
-
required: ['html'],
|
|
401
423
|
},
|
|
402
424
|
},
|
|
403
425
|
{
|
|
@@ -675,11 +697,22 @@ async function handleCallTool(request) {
|
|
|
675
697
|
return { content: [{ type: 'text', text: JSON.stringify(result) }] };
|
|
676
698
|
}
|
|
677
699
|
if (name === 'project_demo_video_write') {
|
|
678
|
-
|
|
679
|
-
|
|
700
|
+
const hasHtml = !!args && args.html !== undefined && args.html !== null && args.html !== '';
|
|
701
|
+
const hasPath = !!args && args.htmlPath !== undefined && args.htmlPath !== null && args.htmlPath !== '';
|
|
702
|
+
if (hasHtml === hasPath) {
|
|
703
|
+
return errorResult('provide exactly one of html or htmlPath');
|
|
704
|
+
}
|
|
705
|
+
let html;
|
|
706
|
+
if (hasPath) {
|
|
707
|
+
const loaded = await readDemoVideoHtmlFile(args.htmlPath);
|
|
708
|
+
if (loaded.error) return errorResult(loaded.error);
|
|
709
|
+
html = loaded.html;
|
|
710
|
+
} else {
|
|
711
|
+
if (typeof args.html !== 'string') return errorResult('missing required argument: html');
|
|
712
|
+
html = args.html;
|
|
680
713
|
}
|
|
681
714
|
const cwd = resolveCwdArg(args);
|
|
682
|
-
const result = await adminRequest('POST', '/admin/project-home/demo-video/write', { cwd, html
|
|
715
|
+
const result = await adminRequest('POST', '/admin/project-home/demo-video/write', { cwd, html });
|
|
683
716
|
return { content: [{ type: 'text', text: JSON.stringify(result) }] };
|
|
684
717
|
}
|
|
685
718
|
if (name === 'macro_list') {
|
|
@@ -163,19 +163,30 @@ async function savePersona({
|
|
|
163
163
|
return saveOverlay({ name, projectName, model, effort, loadSessions, validatePath, writeTextAtomic });
|
|
164
164
|
}
|
|
165
165
|
const target = validatePath(path.join(globalDir, `${name}.md`));
|
|
166
|
-
//
|
|
167
|
-
//
|
|
168
|
-
// existing file
|
|
169
|
-
// `
|
|
166
|
+
// A caller that omits seedVersion/title/projects/action/actionLabel (the
|
|
167
|
+
// Agent Library form has no seedVersion field) must not erase what the
|
|
168
|
+
// existing file holds — carry each undefined one over. An explicit value,
|
|
169
|
+
// including `projects: []`, still wins. `effort` is deliberately not
|
|
170
|
+
// carried: omitted means "write no effort line".
|
|
170
171
|
let resolvedSeedVersion = seedVersion;
|
|
171
|
-
|
|
172
|
+
let resolvedTitle = title;
|
|
173
|
+
let resolvedProjects = projects;
|
|
174
|
+
let resolvedAction = action;
|
|
175
|
+
let resolvedActionLabel = actionLabel;
|
|
176
|
+
if ([seedVersion, title, projects, action, actionLabel].some((v) => v === undefined)) {
|
|
172
177
|
try {
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
178
|
+
const { fm } = splitFrontmatter(fsSync.readFileSync(target, 'utf8'));
|
|
179
|
+
if (resolvedSeedVersion === undefined) {
|
|
180
|
+
const existingVersion = Number(fm.seedVersion);
|
|
181
|
+
if (Number.isFinite(existingVersion)) resolvedSeedVersion = existingVersion;
|
|
182
|
+
}
|
|
183
|
+
if (resolvedTitle === undefined && fm.title) resolvedTitle = fm.title;
|
|
184
|
+
if (resolvedProjects === undefined && fm.projects) resolvedProjects = parseTools(fm.projects);
|
|
185
|
+
if (resolvedAction === undefined && fm.action) resolvedAction = decodeLine(fm.action);
|
|
186
|
+
if (resolvedActionLabel === undefined && fm.actionLabel) resolvedActionLabel = decodeLine(fm.actionLabel);
|
|
176
187
|
} catch { /* no existing file (or unreadable) — nothing to carry over */ }
|
|
177
188
|
}
|
|
178
|
-
const text = serializePersona({ name, description, tools, model, effort, color, tags, projects, action, actionLabel, title, seedVersion: resolvedSeedVersion, body });
|
|
189
|
+
const text = serializePersona({ name, description, tools, model, effort, color, tags, projects: resolvedProjects, action: resolvedAction, actionLabel: resolvedActionLabel, title: resolvedTitle, seedVersion: resolvedSeedVersion, body });
|
|
179
190
|
await writeTextAtomic(target, text);
|
|
180
191
|
if (originalName && originalName !== name) {
|
|
181
192
|
const oldReal = validatePath(path.join(globalDir, `${originalName}.md`));
|
package/src/main/build-info.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.
|
|
3
|
-
"gitSha": "
|
|
4
|
-
"gitShortSha": "
|
|
2
|
+
"version": "0.109.0",
|
|
3
|
+
"gitSha": "14869c8ae8d91422ea84f5b1e1fb483349544a80",
|
|
4
|
+
"gitShortSha": "14869c8a",
|
|
5
5
|
"gitBranch": "HEAD",
|
|
6
|
-
"builtAt": "2026-10-
|
|
6
|
+
"builtAt": "2026-10-10T21:41:36.398Z",
|
|
7
7
|
"dirty": false
|
|
8
8
|
}
|
package/src/main/config.cjs
CHANGED
|
@@ -27,9 +27,9 @@ const { expandHome } = require('./lib/expandHome.cjs');
|
|
|
27
27
|
const { listReferencedFiles } = require('./lib/importReferences.cjs');
|
|
28
28
|
const atomicFs = require('./lib/atomicFs.cjs');
|
|
29
29
|
|
|
30
|
-
/** Map<
|
|
30
|
+
/** Map<watchKey, {watcher, refCount}> — one chokidar watcher per path (+ depth, see watchKeyOf). */
|
|
31
31
|
const watchers = new Map();
|
|
32
|
-
/** Map<senderId, Map<
|
|
32
|
+
/** Map<senderId, Map<watchKey, count>> — per-sender watch contributions for reload teardown. */
|
|
33
33
|
const senderWatches = new Map();
|
|
34
34
|
let window = null;
|
|
35
35
|
|
|
@@ -380,26 +380,34 @@ function normalizePathKey(p) {
|
|
|
380
380
|
return path.resolve(expandHome(p));
|
|
381
381
|
}
|
|
382
382
|
|
|
383
|
+
/** Watcher-map key: the resolved path, plus a depth suffix for non-default (bounded) watches. */
|
|
384
|
+
function watchKeyOf(key, depth) {
|
|
385
|
+
return depth === undefined ? key : `${key}\u0000d${depth}`;
|
|
386
|
+
}
|
|
387
|
+
|
|
383
388
|
/**
|
|
384
389
|
* Watch one or more absolute paths. Each call increments a refcount per path.
|
|
385
390
|
* Unwatch decrements; when refcount hits zero the underlying watcher is closed.
|
|
386
391
|
* Fires IPC event `config:changed` with {path, mtimeMs, kind} on any change.
|
|
387
392
|
*/
|
|
388
|
-
function watch(paths, senderId) {
|
|
393
|
+
function watch(paths, senderId, opts) {
|
|
394
|
+
const depth = opts?.depth;
|
|
389
395
|
for (const rawPath of paths) {
|
|
390
396
|
validatePath(expandHome(rawPath)); // security check — throws if out of bounds
|
|
391
397
|
const key = normalizePathKey(rawPath);
|
|
398
|
+
const mapKey = watchKeyOf(key, depth);
|
|
392
399
|
if (senderId != null) {
|
|
393
400
|
if (!senderWatches.has(senderId)) senderWatches.set(senderId, new Map());
|
|
394
401
|
const sm = senderWatches.get(senderId);
|
|
395
|
-
sm.set(
|
|
402
|
+
sm.set(mapKey, (sm.get(mapKey) || 0) + 1);
|
|
396
403
|
}
|
|
397
|
-
const existing = watchers.get(
|
|
404
|
+
const existing = watchers.get(mapKey);
|
|
398
405
|
if (existing) {
|
|
399
406
|
existing.refCount++;
|
|
400
407
|
continue;
|
|
401
408
|
}
|
|
402
409
|
const w = chokidar.watch(key, {
|
|
410
|
+
...(depth === undefined ? {} : { depth }),
|
|
403
411
|
ignoreInitial: true,
|
|
404
412
|
persistent: true,
|
|
405
413
|
awaitWriteFinish: { stabilityThreshold: 50, pollInterval: 25 },
|
|
@@ -420,13 +428,13 @@ function watch(paths, senderId) {
|
|
|
420
428
|
w.on('error', (err) => {
|
|
421
429
|
console.warn('[config] watcher error for', key, err.message);
|
|
422
430
|
});
|
|
423
|
-
watchers.set(
|
|
431
|
+
watchers.set(mapKey, { watcher: w, refCount: 1 });
|
|
424
432
|
}
|
|
425
433
|
}
|
|
426
434
|
|
|
427
|
-
function unwatch(paths, senderId) {
|
|
435
|
+
function unwatch(paths, senderId, opts) {
|
|
428
436
|
for (const rawPath of paths) {
|
|
429
|
-
const key = normalizePathKey(rawPath);
|
|
437
|
+
const key = watchKeyOf(normalizePathKey(rawPath), opts?.depth);
|
|
430
438
|
if (senderId != null) {
|
|
431
439
|
const sm = senderWatches.get(senderId);
|
|
432
440
|
if (sm) {
|
|
@@ -489,8 +497,8 @@ function registerConfigHandlers() {
|
|
|
489
497
|
return { ok: false, error: e.message };
|
|
490
498
|
}
|
|
491
499
|
}));
|
|
492
|
-
ipcMain.on('config:watch', (e, { paths }) => { try { s.configWatch.parse(paths); watch(paths, e.sender.id); } catch (err) { logs.writeLine({ level: 'warn', scope: 'config', message: 'config:watch schema reject', meta: { error: err?.message } }); } });
|
|
493
|
-
ipcMain.on('config:unwatch', (e, { paths }) => { try { s.configWatch.parse(paths); unwatch(paths, e.sender.id); } catch (err) { logs.writeLine({ level: 'warn', scope: 'config', message: 'config:unwatch schema reject', meta: { error: err?.message } }); } });
|
|
500
|
+
ipcMain.on('config:watch', (e, { paths, opts }) => { try { s.configWatch.parse(paths); s.configWatchOpts.parse(opts); watch(paths, e.sender.id, opts); } catch (err) { logs.writeLine({ level: 'warn', scope: 'config', message: 'config:watch schema reject', meta: { error: err?.message } }); } });
|
|
501
|
+
ipcMain.on('config:unwatch', (e, { paths, opts }) => { try { s.configWatch.parse(paths); s.configWatchOpts.parse(opts); unwatch(paths, e.sender.id, opts); } catch (err) { logs.writeLine({ level: 'warn', scope: 'config', message: 'config:unwatch schema reject', meta: { error: err?.message } }); } });
|
|
494
502
|
}
|
|
495
503
|
|
|
496
504
|
module.exports = {
|
|
@@ -498,6 +506,8 @@ module.exports = {
|
|
|
498
506
|
registerConfigHandlers,
|
|
499
507
|
closeAllWatchers,
|
|
500
508
|
releaseWatchesForSender,
|
|
509
|
+
watch,
|
|
510
|
+
unwatch,
|
|
501
511
|
// exported for tests / direct use
|
|
502
512
|
readJson,
|
|
503
513
|
readText,
|
|
@@ -6,6 +6,7 @@ const path = require('node:path');
|
|
|
6
6
|
const os = require('node:os');
|
|
7
7
|
const { schemas } = require('./ipcSchemas.cjs');
|
|
8
8
|
const historyRollup = require('./lib/historyRollup.cjs');
|
|
9
|
+
const { sendIfAlive } = require('./lib/sendToRenderer.cjs');
|
|
9
10
|
|
|
10
11
|
const PROJECTS_DIR = path.join(os.homedir(), '.claude', 'projects');
|
|
11
12
|
const PARSE_BUDGET_MS = 2_000;
|
|
@@ -52,6 +53,47 @@ function resolvePricingKey(modelId) {
|
|
|
52
53
|
// Shared implementation: lib/lruCache.cjs (re-exported below for existing callers).
|
|
53
54
|
const { LRUCache } = require('./lib/lruCache.cjs');
|
|
54
55
|
|
|
56
|
+
// ── history:updated broadcast ─────────────────────────────────────────────────
|
|
57
|
+
// Open History pages re-fetch on this event. Throttled leading+trailing so a
|
|
58
|
+
// backfill burst announces its first and last write only.
|
|
59
|
+
const HISTORY_UPDATED_THROTTLE_MS = 5000;
|
|
60
|
+
let historyNotifyTimer = null;
|
|
61
|
+
let historyNotifyPending = false;
|
|
62
|
+
|
|
63
|
+
function broadcastHistoryUpdated() {
|
|
64
|
+
try {
|
|
65
|
+
const { BrowserWindow } = require('electron');
|
|
66
|
+
for (const win of BrowserWindow.getAllWindows()) sendIfAlive(win, 'history:updated', {});
|
|
67
|
+
} catch {
|
|
68
|
+
// Never let a failed broadcast break aggregation.
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function notifyHistoryUpdated() {
|
|
73
|
+
if (historyNotifyTimer) {
|
|
74
|
+
historyNotifyPending = true;
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
broadcastHistoryUpdated();
|
|
78
|
+
const arm = () => {
|
|
79
|
+
historyNotifyTimer = setTimeout(() => {
|
|
80
|
+
historyNotifyTimer = null;
|
|
81
|
+
if (historyNotifyPending) {
|
|
82
|
+
historyNotifyPending = false;
|
|
83
|
+
broadcastHistoryUpdated();
|
|
84
|
+
arm();
|
|
85
|
+
}
|
|
86
|
+
}, HISTORY_UPDATED_THROTTLE_MS);
|
|
87
|
+
if (historyNotifyTimer.unref) historyNotifyTimer.unref();
|
|
88
|
+
};
|
|
89
|
+
arm();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function appendRollupAndNotify(entries) {
|
|
93
|
+
await historyRollup.appendRollupDays(entries);
|
|
94
|
+
if (entries.length) notifyHistoryUpdated();
|
|
95
|
+
}
|
|
96
|
+
|
|
55
97
|
/**
|
|
56
98
|
* Cache for parseJSONL results.
|
|
57
99
|
* Entry shape: { mtimeMs: number, size: number, readOffset: number, inode: number, result: AggrResult }
|
|
@@ -574,7 +616,7 @@ async function finalizeClosedDays({ budgetMs, dryRun = false } = {}) {
|
|
|
574
616
|
}
|
|
575
617
|
|
|
576
618
|
if (!dryRun) {
|
|
577
|
-
await
|
|
619
|
+
await appendRollupAndNotify(entries);
|
|
578
620
|
}
|
|
579
621
|
return { finalizedDates: partial ? [] : Array.from(dates), partial };
|
|
580
622
|
}
|
|
@@ -741,7 +783,7 @@ async function refreshIntradayToday() {
|
|
|
741
783
|
|
|
742
784
|
const entries = [];
|
|
743
785
|
for (const b of buckets.values()) entries.push(...bucketToRollupLines(b));
|
|
744
|
-
if (entries.length) await
|
|
786
|
+
if (entries.length) await appendRollupAndNotify(entries);
|
|
745
787
|
|
|
746
788
|
return { date: today, projectsUpdated: buckets.size, strategy: INTRADAY_STRATEGY };
|
|
747
789
|
}
|
|
@@ -859,7 +901,7 @@ async function aggregate(req) {
|
|
|
859
901
|
if (b.date < today) backfillEntries.push(...bucketToRollupLines(b));
|
|
860
902
|
}
|
|
861
903
|
if (backfillEntries.length) {
|
|
862
|
-
await
|
|
904
|
+
await appendRollupAndNotify(backfillEntries);
|
|
863
905
|
}
|
|
864
906
|
|
|
865
907
|
const rollupRows = rollupMapToRows(rollupMap, finalizedDates);
|
|
@@ -951,6 +993,7 @@ module.exports = {
|
|
|
951
993
|
finalizeClosedDays,
|
|
952
994
|
decodeCwd,
|
|
953
995
|
refreshIntradayToday,
|
|
996
|
+
appendRollupAndNotify,
|
|
954
997
|
computeIntradayBuckets,
|
|
955
998
|
// exported for tests
|
|
956
999
|
scanAggrLines,
|
package/src/main/index.cjs
CHANGED
|
@@ -129,6 +129,7 @@ const heapSnapshot = require('./heapSnapshot.cjs');
|
|
|
129
129
|
const filesIpc = require('./files.cjs');
|
|
130
130
|
const { registerDocEditHandlers, attachWindow: attachDocEditWindow } = require('./docEdit.cjs');
|
|
131
131
|
const webRemote = require('./webRemote.cjs');
|
|
132
|
+
const { registerRemoteClientHandlers } = require('./remoteClientIpc.cjs');
|
|
132
133
|
const { listExchanges } = require('./exchanges.cjs');
|
|
133
134
|
const { resolveClaudeBin } = require('./lib/claudeBin.cjs');
|
|
134
135
|
const { checkInsideHome, assertInsideHome } = require('./lib/insideHome.cjs');
|
|
@@ -1098,6 +1099,7 @@ git.register(ipcMain);
|
|
|
1098
1099
|
filesIpc.registerFilesHandlers();
|
|
1099
1100
|
registerDocEditHandlers();
|
|
1100
1101
|
webRemote.registerRemoteHandlers();
|
|
1102
|
+
registerRemoteClientHandlers(() => mainWindow);
|
|
1101
1103
|
chatRunner.registerChatHandlers();
|
|
1102
1104
|
|
|
1103
1105
|
// Direct in-process PRD authoring for a queued PromptTicket classified
|
package/src/main/ipcSchemas.cjs
CHANGED
|
@@ -139,6 +139,7 @@ const configListDir = z.object({
|
|
|
139
139
|
});
|
|
140
140
|
|
|
141
141
|
const configWatch = z.array(z.string().min(1).max(4096));
|
|
142
|
+
const configWatchOpts = z.object({ depth: z.number().int().min(0).max(2) }).strict().optional();
|
|
142
143
|
|
|
143
144
|
const configParseImports = z.object({ path: z.string().min(1).max(4096) });
|
|
144
145
|
|
|
@@ -1211,6 +1212,7 @@ module.exports = {
|
|
|
1211
1212
|
configWriteText,
|
|
1212
1213
|
configListDir,
|
|
1213
1214
|
configWatch,
|
|
1215
|
+
configWatchOpts,
|
|
1214
1216
|
configParseImports,
|
|
1215
1217
|
sessionsPayload,
|
|
1216
1218
|
layoutEnvelope,
|
|
@@ -30,9 +30,9 @@ const BUILTIN_MACROS = Object.freeze([
|
|
|
30
30
|
tag: 'project-home-builder',
|
|
31
31
|
surface: 'project-home',
|
|
32
32
|
projects: ['*'],
|
|
33
|
-
builtinVersion:
|
|
33
|
+
builtinVersion: 3,
|
|
34
34
|
prompt:
|
|
35
|
-
'Generate a 30-second demo video for this project as one self-contained HTML/JavaScript animation that presents its main goals and key features, grounded only in what you read in the repository, with a matching on-device narration track per the audio policy in the persona. Save it with a single project_demo_video_write call.',
|
|
35
|
+
'Generate a 30-second demo video for this project as one self-contained HTML/JavaScript animation that presents its main goals and key features, grounded only in what you read in the repository, with a matching on-device narration track: narrate with Kokoro (kokoro-onnx) using a natural female (af_heart) or male (am_michael) voice per the audio policy in the persona. Save it with a single project_demo_video_write call passing htmlPath.',
|
|
36
36
|
}),
|
|
37
37
|
]);
|
|
38
38
|
|
|
@@ -279,7 +279,8 @@ const MCP_TOOL_CATALOG = [
|
|
|
279
279
|
whenToUse: 'Use when asked to generate or refresh the demo video. Compose the whole document yourself '
|
|
280
280
|
+ '(inline <style>/<script>, data: URIs for assets, CSS/JS-driven animation) from what is really true about '
|
|
281
281
|
+ 'the project, declare its length with <meta name="sm-demo-duration" content="N"> (5-30 seconds), then '
|
|
282
|
-
+ 'pass the
|
|
282
|
+
+ 'pass the document as `html` or — preferred when it embeds a base64 audio/image data URI — write it to a '
|
|
283
|
+
+ 'local .html file and pass its absolute path as `htmlPath`.',
|
|
283
284
|
whenNotToUse: 'Never fabricate content — every claim must trace to something concrete in the project. The '
|
|
284
285
|
+ 'document must be self-contained and non-network-capable: the call is rejected for empty input, more than '
|
|
285
286
|
+ '2MB, any <script src>, <link href="http…">, @import, or url(http…) (no remote references); any '
|
|
@@ -291,6 +292,7 @@ const MCP_TOOL_CATALOG = [
|
|
|
291
292
|
html: '<!DOCTYPE html><html><head><meta name="sm-demo-duration" content="30"></head><body><h1>Example</h1></body></html>',
|
|
292
293
|
},
|
|
293
294
|
notes: "cwd is optional — defaults to the calling session's own project root (SM_PROJECT_ROOT or process.cwd()) when omitted. "
|
|
295
|
+
+ 'Give exactly one of `html` / `htmlPath` (absolute .html/.htm file, <= 2MB); prefer `htmlPath` for base64 data URIs. '
|
|
294
296
|
+ 'The document is CSP-stamped after it is written, so the viewer sandbox is the real network fence — these checks are defence in depth.',
|
|
295
297
|
},
|
|
296
298
|
{
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// E2E encryption helpers (P-256 ECDH + AES-256-GCM, ADR §5.2).
|
|
4
|
+
// Shared by the agent side (webRemote.cjs) and the desktop client role.
|
|
5
|
+
|
|
6
|
+
const crypto = require('node:crypto');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Generate a P-256 ECDH keypair. Public key is SPKI DER base64url; private key
|
|
10
|
+
* is PKCS8 DER base64url. Both are stored in web-remote.json at 0600.
|
|
11
|
+
*/
|
|
12
|
+
function generateE2EKeyPair() {
|
|
13
|
+
const { privateKey, publicKey } = crypto.generateKeyPairSync('ec', {
|
|
14
|
+
namedCurve: 'P-256',
|
|
15
|
+
publicKeyEncoding: { type: 'spki', format: 'der' },
|
|
16
|
+
privateKeyEncoding: { type: 'pkcs8', format: 'der' },
|
|
17
|
+
});
|
|
18
|
+
return {
|
|
19
|
+
e2ePrivateKey: privateKey.toString('base64url'),
|
|
20
|
+
e2ePublicKey: publicKey.toString('base64url'),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Derive an AES-256-GCM session key from two P-256 public keys via ECDH + HKDF.
|
|
26
|
+
* @param myPrivateKeyB64 Agent's PKCS8 private key, base64url DER
|
|
27
|
+
* @param peerPublicKeyB64 Browser's SPKI public key, base64url DER
|
|
28
|
+
* @param deviceId Used as HKDF salt for domain separation
|
|
29
|
+
* @returns 32-byte Buffer (AES-256-GCM key)
|
|
30
|
+
*/
|
|
31
|
+
function deriveSessionKey(myPrivateKeyB64, peerPublicKeyB64, deviceId) {
|
|
32
|
+
const myPrivKey = crypto.createPrivateKey({
|
|
33
|
+
key: Buffer.from(myPrivateKeyB64, 'base64url'),
|
|
34
|
+
format: 'der',
|
|
35
|
+
type: 'pkcs8',
|
|
36
|
+
});
|
|
37
|
+
const peerPubKey = crypto.createPublicKey({
|
|
38
|
+
key: Buffer.from(peerPublicKeyB64, 'base64url'),
|
|
39
|
+
format: 'der',
|
|
40
|
+
type: 'spki',
|
|
41
|
+
});
|
|
42
|
+
const sharedSecret = crypto.diffieHellman({ privateKey: myPrivKey, publicKey: peerPubKey });
|
|
43
|
+
// HKDF: salt = deviceId bytes for domain separation, info = fixed protocol label.
|
|
44
|
+
const salt = Buffer.from(deviceId, 'utf8');
|
|
45
|
+
const info = Buffer.from('sm-e2e-v1', 'utf8');
|
|
46
|
+
return Buffer.from(crypto.hkdfSync('sha256', sharedSecret, salt, info, 32));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Derive a 6-digit Short Authentication String from the ECDH shared secret.
|
|
51
|
+
* Uses a separate HKDF info label ('sm-sas-v1') so the SAS is independent of
|
|
52
|
+
* the session key. Both sides compute the same value; user confirms they match.
|
|
53
|
+
*/
|
|
54
|
+
function deriveSas(myPrivateKeyB64, peerPublicKeyB64, deviceId) {
|
|
55
|
+
const myPrivKey = crypto.createPrivateKey({
|
|
56
|
+
key: Buffer.from(myPrivateKeyB64, 'base64url'),
|
|
57
|
+
format: 'der',
|
|
58
|
+
type: 'pkcs8',
|
|
59
|
+
});
|
|
60
|
+
const peerPubKey = crypto.createPublicKey({
|
|
61
|
+
key: Buffer.from(peerPublicKeyB64, 'base64url'),
|
|
62
|
+
format: 'der',
|
|
63
|
+
type: 'spki',
|
|
64
|
+
});
|
|
65
|
+
const sharedSecret = crypto.diffieHellman({ privateKey: myPrivKey, publicKey: peerPubKey });
|
|
66
|
+
const salt = Buffer.from(deviceId, 'utf8');
|
|
67
|
+
const info = Buffer.from('sm-sas-v1', 'utf8');
|
|
68
|
+
const sasBytes = Buffer.from(crypto.hkdfSync('sha256', sharedSecret, salt, info, 3));
|
|
69
|
+
const sasNum = ((sasBytes[0] << 16) | (sasBytes[1] << 8) | sasBytes[2]) % 1_000_000;
|
|
70
|
+
return sasNum.toString().padStart(6, '0');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Encrypt a plaintext JSON string into an AES-256-GCM box.
|
|
75
|
+
* @returns { nonce: base64url, ciphertext: base64url } — the nonce is 12 random bytes
|
|
76
|
+
*/
|
|
77
|
+
function encryptBox(plaintext, sessionKey) {
|
|
78
|
+
const nonce = crypto.randomBytes(12);
|
|
79
|
+
const cipher = crypto.createCipheriv('aes-256-gcm', sessionKey, nonce);
|
|
80
|
+
const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
|
|
81
|
+
const tag = cipher.getAuthTag(); // 16-byte GCM authentication tag
|
|
82
|
+
return {
|
|
83
|
+
nonce: nonce.toString('base64url'),
|
|
84
|
+
// Append the GCM tag to the ciphertext so decryptBox can locate it.
|
|
85
|
+
ciphertext: Buffer.concat([encrypted, tag]).toString('base64url'),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Decrypt an AES-256-GCM box produced by encryptBox (or the browser's equivalent).
|
|
91
|
+
* @returns Decrypted UTF-8 string, or null if authentication fails.
|
|
92
|
+
*/
|
|
93
|
+
function decryptBox(nonceB64, ciphertextB64, sessionKey) {
|
|
94
|
+
try {
|
|
95
|
+
const nonce = Buffer.from(nonceB64, 'base64url');
|
|
96
|
+
const data = Buffer.from(ciphertextB64, 'base64url');
|
|
97
|
+
if (data.length < 16) return null; // too short to contain tag
|
|
98
|
+
const tag = data.subarray(data.length - 16);
|
|
99
|
+
const ciphertext = data.subarray(0, data.length - 16);
|
|
100
|
+
const decipher = crypto.createDecipheriv('aes-256-gcm', sessionKey, nonce);
|
|
101
|
+
decipher.setAuthTag(tag);
|
|
102
|
+
const plain = Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString('utf8');
|
|
103
|
+
return plain;
|
|
104
|
+
} catch {
|
|
105
|
+
return null; // authentication failed — drop the message
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
module.exports = { generateE2EKeyPair, deriveSessionKey, deriveSas, encryptBox, decryptBox };
|