claude-code-session-manager 0.108.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-DC6VzovQ.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-DLmnlfQt.js → Library-B3rzEhvN.js} +24 -24
- package/dist/assets/{ListDetail-YylbvUj_.js → ListDetail-BAvci_aP.js} +1 -1
- package/dist/assets/{MarkdownEditor-DMPKWHTF.js → MarkdownEditor-DBWTGgRz.js} +1 -1
- package/dist/assets/{McpServers-Da-Ty5nc.js → McpServers-C-grBN21.js} +2 -2
- package/dist/assets/{Memory-C3rUQ_cg.js → Memory-BmM0Fz-C.js} +5 -5
- package/dist/assets/{Panel-CC3JTnOI.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-C06rFT34.js → SaveBar-DSMnbUVI.js} +1 -1
- package/dist/assets/Scheduler-CbIcJEfM.js +16 -0
- package/dist/assets/{ScopeSwitcher-Cp5mxblN.js → ScopeSwitcher-tHtloWbN.js} +1 -1
- package/dist/assets/Settings-CW3w7knp.js +6 -0
- package/dist/assets/{SkillReferenceGraph-DuWRmT_f.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-1r4ODLJ7.js → TiptapBody-C2LfNQXw.js} +1 -1
- package/dist/assets/{Toggle-BGT68XHP.js → Toggle-CTXTU1q5.js} +1 -1
- package/dist/assets/{ViewTabs-T0NBc8uv.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-3_AtCAyd.js → index-fJxuoRvi.js} +697 -697
- package/dist/assets/listSkills-C1aWKf0T.js +1 -0
- package/dist/assets/{settingsSchema-CvoNk3Ye.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/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/remoteE2eCrypto.cjs +109 -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/dist/assets/AgentLibrary-Co54GThg.js +0 -2
- package/dist/assets/DataModel-DZ0vyJau.js +0 -1
- package/dist/assets/History-Df3YwpPe.js +0 -2
- package/dist/assets/Hooks-DplG6roH.js +0 -3
- package/dist/assets/Permissions-XvBiMj1-.js +0 -3
- package/dist/assets/Plugins-CzHY1mgS.js +0 -2
- package/dist/assets/ProvenanceBadge-DaJIWVcg.js +0 -1
- package/dist/assets/Scheduler-BehadU-0.js +0 -16
- package/dist/assets/Settings-Cg9LIni0.js +0 -4
- package/dist/assets/Skills-CMmCZfIv.js +0 -3
- package/dist/assets/SystemPrompt-D_-MV025.js +0 -1
- package/dist/assets/TagLibrary-D9Tv3yOJ.js +0 -1
- package/dist/assets/listSkills-QORduPIk.js +0 -1
- package/dist/assets/useAgentsDirWatch-1C2CBBya.js +0 -1
- package/dist/assets/useConfigDirWatch-Cd5PnTkB.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-3_AtCAyd.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 |
|
|
@@ -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,
|
|
@@ -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 };
|
package/src/main/pty.cjs
CHANGED
|
@@ -111,6 +111,44 @@ class PtyManager {
|
|
|
111
111
|
this.outBuffers = new Map();
|
|
112
112
|
this.flushTimers = new Map(); // tabId -> Timeout
|
|
113
113
|
this.window = null;
|
|
114
|
+
this.outputListeners = new Map(); // tabId -> Set<{onData,onExit}>
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Observe a tab's coalesced output (the same string the renderer gets) and
|
|
119
|
+
* its exit from the main process. Returns an unsubscribe function.
|
|
120
|
+
* @param {string} tabId
|
|
121
|
+
* @param {(tabId: string, chunk: string) => void} onData
|
|
122
|
+
* @param {(tabId: string, info: {exitCode: number, signal?: number}) => void} [onExit]
|
|
123
|
+
*/
|
|
124
|
+
addOutputListener(tabId, onData, onExit) {
|
|
125
|
+
const entry = { onData, onExit };
|
|
126
|
+
let set = this.outputListeners.get(tabId);
|
|
127
|
+
if (!set) {
|
|
128
|
+
set = new Set();
|
|
129
|
+
this.outputListeners.set(tabId, set);
|
|
130
|
+
}
|
|
131
|
+
set.add(entry);
|
|
132
|
+
return () => {
|
|
133
|
+
const cur = this.outputListeners.get(tabId);
|
|
134
|
+
if (!cur) return;
|
|
135
|
+
cur.delete(entry);
|
|
136
|
+
if (cur.size === 0) this.outputListeners.delete(tabId);
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Fire every onExit listener for `tabId` once (errors isolated), then drop them. */
|
|
141
|
+
#notifyExit(tabId, info) {
|
|
142
|
+
const set = this.outputListeners.get(tabId);
|
|
143
|
+
this.outputListeners.delete(tabId);
|
|
144
|
+
if (!set) return;
|
|
145
|
+
for (const l of [...set]) {
|
|
146
|
+
try {
|
|
147
|
+
l.onExit?.(tabId, info);
|
|
148
|
+
} catch (err) {
|
|
149
|
+
console.warn('[pty] output listener onExit threw', err);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
114
152
|
}
|
|
115
153
|
|
|
116
154
|
/** Send any pending buffered output for `tabId` now, and clear its timer. */
|
|
@@ -124,6 +162,16 @@ class PtyManager {
|
|
|
124
162
|
if (pending) {
|
|
125
163
|
this.outBuffers.delete(tabId);
|
|
126
164
|
sendIfAlive(this.window, `pty:data:${tabId}`, pending);
|
|
165
|
+
const set = this.outputListeners.get(tabId);
|
|
166
|
+
if (set) {
|
|
167
|
+
for (const l of [...set]) {
|
|
168
|
+
try {
|
|
169
|
+
l.onData?.(tabId, pending);
|
|
170
|
+
} catch (err) {
|
|
171
|
+
console.warn('[pty] output listener onData threw', err);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
127
175
|
}
|
|
128
176
|
}
|
|
129
177
|
|
|
@@ -328,6 +376,7 @@ class PtyManager {
|
|
|
328
376
|
this.sessions.delete(tabId);
|
|
329
377
|
this.buffers.delete(tabId);
|
|
330
378
|
this.#dropOutput(tabId);
|
|
379
|
+
this.#notifyExit(tabId, { exitCode, signal });
|
|
331
380
|
return;
|
|
332
381
|
}
|
|
333
382
|
// Flush any still-buffered output BEFORE the exit event so the renderer
|
|
@@ -349,6 +398,7 @@ class PtyManager {
|
|
|
349
398
|
});
|
|
350
399
|
}
|
|
351
400
|
sendIfAlive(this.window, `pty:exit:${tabId}`, { exitCode, signal });
|
|
401
|
+
this.#notifyExit(tabId, { exitCode, signal });
|
|
352
402
|
this.sessions.delete(tabId);
|
|
353
403
|
this.buffers.delete(tabId);
|
|
354
404
|
});
|
|
@@ -411,6 +461,9 @@ class PtyManager {
|
|
|
411
461
|
this.sessions.delete(tabId);
|
|
412
462
|
this.buffers.delete(tabId);
|
|
413
463
|
this.#dropOutput(tabId);
|
|
464
|
+
// Listeners learn the tab is gone now; the suppressed exit event that
|
|
465
|
+
// follows finds none left, so each fires exactly once.
|
|
466
|
+
this.#notifyExit(tabId, { exitCode: 0, signal: undefined });
|
|
414
467
|
}
|
|
415
468
|
}
|
|
416
469
|
|