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.
Files changed (68) hide show
  1. package/dist/assets/AgentLibrary-Br4Ispr4.js +2 -0
  2. package/dist/assets/{Button-CRLhoI2K.js → Button-C8Mc6MEP.js} +1 -1
  3. package/dist/assets/DataModel-Bz0MypYO.js +1 -0
  4. package/dist/assets/History-DT26Iy38.js +2 -0
  5. package/dist/assets/Hooks-e4xUGEf-.js +3 -0
  6. package/dist/assets/{Library-rqaJJJ1b.js → Library-B3rzEhvN.js} +24 -24
  7. package/dist/assets/{ListDetail-qI27OYfy.js → ListDetail-BAvci_aP.js} +1 -1
  8. package/dist/assets/{MarkdownEditor-BEFuHVNz.js → MarkdownEditor-DBWTGgRz.js} +1 -1
  9. package/dist/assets/{McpServers-Bz9O79rz.js → McpServers-C-grBN21.js} +2 -2
  10. package/dist/assets/{Memory-DobxIoZ-.js → Memory-BmM0Fz-C.js} +5 -5
  11. package/dist/assets/{Panel-Cyt1wpUf.js → Panel-Dc6F8wb4.js} +1 -1
  12. package/dist/assets/Permissions-m_NnldFh.js +3 -0
  13. package/dist/assets/Plugins-BB0TLUC9.js +2 -0
  14. package/dist/assets/ProvenanceBadge-k9PH0S2n.js +1 -0
  15. package/dist/assets/{SaveBar-BuYrg5E9.js → SaveBar-DSMnbUVI.js} +1 -1
  16. package/dist/assets/Scheduler-CbIcJEfM.js +16 -0
  17. package/dist/assets/{ScopeSwitcher-BM0gLPhd.js → ScopeSwitcher-tHtloWbN.js} +1 -1
  18. package/dist/assets/Settings-CW3w7knp.js +6 -0
  19. package/dist/assets/{SkillReferenceGraph-D0Hgdfxq.js → SkillReferenceGraph-Dndl1tzr.js} +14 -14
  20. package/dist/assets/Skills-CGnxroik.js +2 -0
  21. package/dist/assets/SystemPrompt-B3A3rHNp.js +1 -0
  22. package/dist/assets/TagLibrary-k4CTiAYo.js +1 -0
  23. package/dist/assets/{TiptapBody-CP_F_mwb.js → TiptapBody-C2LfNQXw.js} +1 -1
  24. package/dist/assets/{Toggle-haRPHS5N.js → Toggle-CTXTU1q5.js} +1 -1
  25. package/dist/assets/{ViewTabs-BHV3MA5u.js → ViewTabs-CfEfOSE1.js} +1 -1
  26. package/dist/assets/historyTotals-Db0xy2Fg.js +1 -0
  27. package/dist/assets/{index-tr1w3hHB.css → index-C8Smiz7z.css} +1 -1
  28. package/dist/assets/{index-v_3LvhMx.js → index-fJxuoRvi.js} +697 -697
  29. package/dist/assets/listSkills-C1aWKf0T.js +1 -0
  30. package/dist/assets/{settingsSchema-CE46TA3V.js → settingsSchema-xxO9p4yF.js} +1 -1
  31. package/dist/assets/useAgentsDirWatch-DLS1_moj.js +1 -0
  32. package/dist/index.html +2 -2
  33. package/package.json +5 -2
  34. package/plugins/session-manager-dev/skills/develop/standards.md +5 -0
  35. package/scripts/README.md +7 -0
  36. package/scripts/scheduler-mcp-server.cjs +38 -5
  37. package/src/main/agentLibrary.cjs +20 -9
  38. package/src/main/build-info.json +4 -4
  39. package/src/main/config.cjs +20 -10
  40. package/src/main/historyAggregator.cjs +46 -3
  41. package/src/main/index.cjs +2 -0
  42. package/src/main/ipcSchemas.cjs +2 -0
  43. package/src/main/lib/builtinMacros.cjs +2 -2
  44. package/src/main/lib/mcpToolCatalog.cjs +3 -1
  45. package/src/main/lib/remoteE2eCrypto.cjs +109 -0
  46. package/src/main/lib/shippedPersonaSeeds.cjs +11 -0
  47. package/src/main/pty.cjs +53 -0
  48. package/src/main/remoteClient.cjs +305 -0
  49. package/src/main/remoteClientIpc.cjs +101 -0
  50. package/src/main/webRemote.cjs +90 -108
  51. package/src/preload/api.d.ts +42 -5
  52. package/src/preload/index.cjs +27 -2
  53. package/src/seed/agents/demo-video-builder.md +69 -48
  54. package/dist/assets/AgentLibrary-Bjeq2-kp.js +0 -2
  55. package/dist/assets/DataModel-iPJw5jcG.js +0 -1
  56. package/dist/assets/History-DfPR3_6N.js +0 -2
  57. package/dist/assets/Hooks-Bw7H_DmC.js +0 -3
  58. package/dist/assets/Permissions-ChSNRUJJ.js +0 -3
  59. package/dist/assets/Plugins-CII221R2.js +0 -2
  60. package/dist/assets/ProvenanceBadge-C_sk5-v3.js +0 -1
  61. package/dist/assets/Scheduler-eixbMGEk.js +0 -16
  62. package/dist/assets/Settings-qD-0W-Ut.js +0 -4
  63. package/dist/assets/Skills-C3T_2_2o.js +0 -3
  64. package/dist/assets/SystemPrompt-B-xTy9Qn.js +0 -1
  65. package/dist/assets/TagLibrary-BH67tR5z.js +0 -1
  66. package/dist/assets/listSkills-QORduPIk.js +0 -1
  67. package/dist/assets/useAgentsDirWatch-BZTi2K2e.js +0 -1
  68. 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-v_3LvhMx.js"></script>
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-tr1w3hHB.css">
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.107.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": "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",
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
- if (!args || typeof args.html !== 'string' || args.html.length === 0) {
679
- return errorResult('missing required argument: html');
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: args.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
- // The Agent Library editor's form has no `seedVersion` field, so a save
167
- // through it never passes one explicitly — carry over whatever stamp the
168
- // existing file already has rather than silently erasing it. An explicit
169
- // `seedVersion` argument (e.g. the seeder's own future callers) still wins.
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
- if (resolvedSeedVersion === undefined) {
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 existing = splitFrontmatter(fsSync.readFileSync(target, 'utf8'));
174
- const existingVersion = Number(existing.fm.seedVersion);
175
- if (Number.isFinite(existingVersion)) resolvedSeedVersion = existingVersion;
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`));
@@ -1,8 +1,8 @@
1
1
  {
2
- "version": "0.107.0",
3
- "gitSha": "a18e184a574f54d301d80aeeb88e477d88741be9",
4
- "gitShortSha": "a18e184a",
2
+ "version": "0.109.0",
3
+ "gitSha": "14869c8ae8d91422ea84f5b1e1fb483349544a80",
4
+ "gitShortSha": "14869c8a",
5
5
  "gitBranch": "HEAD",
6
- "builtAt": "2026-10-09T18:00:32.692Z",
6
+ "builtAt": "2026-10-10T21:41:36.398Z",
7
7
  "dirty": false
8
8
  }
@@ -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<absPath, {watcher, refCount}> — one chokidar watcher per path. */
30
+ /** Map<watchKey, {watcher, refCount}> — one chokidar watcher per path (+ depth, see watchKeyOf). */
31
31
  const watchers = new Map();
32
- /** Map<senderId, Map<absPath, count>> — per-sender watch contributions for reload teardown. */
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(key, (sm.get(key) || 0) + 1);
402
+ sm.set(mapKey, (sm.get(mapKey) || 0) + 1);
396
403
  }
397
- const existing = watchers.get(key);
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(key, { watcher: w, refCount: 1 });
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 historyRollup.appendRollupDays(entries);
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 historyRollup.appendRollupDays(entries);
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 historyRollup.appendRollupDays(backfillEntries);
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,
@@ -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
@@ -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: 2,
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 full document as `html`.',
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 };