claude-code-session-manager 0.106.0 → 0.107.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-CKOZzc2W.js → AgentLibrary-Bjeq2-kp.js} +1 -1
- package/dist/assets/{Button-yuqFpQsK.js → Button-CRLhoI2K.js} +1 -1
- package/dist/assets/{DataModel-Bwdc_gaJ.js → DataModel-iPJw5jcG.js} +1 -1
- package/dist/assets/{History-D1Z0lydo.js → History-DfPR3_6N.js} +1 -1
- package/dist/assets/{Hooks-CyD6l4Is.js → Hooks-Bw7H_DmC.js} +1 -1
- package/dist/assets/{Library-fnod5cPe.js → Library-rqaJJJ1b.js} +1 -1
- package/dist/assets/{ListDetail-BKO68SJ4.js → ListDetail-qI27OYfy.js} +1 -1
- package/dist/assets/{MarkdownEditor-DrYMnMBy.js → MarkdownEditor-BEFuHVNz.js} +1 -1
- package/dist/assets/{McpServers-CgfItWhF.js → McpServers-Bz9O79rz.js} +1 -1
- package/dist/assets/{Memory-4M3gO3_r.js → Memory-DobxIoZ-.js} +1 -1
- package/dist/assets/{Panel-CKbXo-LT.js → Panel-Cyt1wpUf.js} +1 -1
- package/dist/assets/{Permissions-Bc9EpjXL.js → Permissions-ChSNRUJJ.js} +1 -1
- package/dist/assets/{Plugins-B_-PlEIx.js → Plugins-CII221R2.js} +2 -2
- package/dist/assets/{ProvenanceBadge-Cxev7XSA.js → ProvenanceBadge-C_sk5-v3.js} +1 -1
- package/dist/assets/{SaveBar-DiqscG6q.js → SaveBar-BuYrg5E9.js} +1 -1
- package/dist/assets/Scheduler-eixbMGEk.js +16 -0
- package/dist/assets/{ScopeSwitcher-BsM4-jHQ.js → ScopeSwitcher-BM0gLPhd.js} +1 -1
- package/dist/assets/Settings-qD-0W-Ut.js +4 -0
- package/dist/assets/{SkillReferenceGraph-B9YlOuPo.js → SkillReferenceGraph-D0Hgdfxq.js} +1 -1
- package/dist/assets/{Skills-Dtf-z_zN.js → Skills-C3T_2_2o.js} +1 -1
- package/dist/assets/{SystemPrompt-HJDn0E8C.js → SystemPrompt-B-xTy9Qn.js} +1 -1
- package/dist/assets/{TagLibrary-DLGOfauU.js → TagLibrary-BH67tR5z.js} +1 -1
- package/dist/assets/{TiptapBody-Cdmv1CuM.js → TiptapBody-CP_F_mwb.js} +1 -1
- package/dist/assets/{Toggle-CQFhOo1F.js → Toggle-haRPHS5N.js} +1 -1
- package/dist/assets/{ViewTabs-CxjDCHjH.js → ViewTabs-BHV3MA5u.js} +1 -1
- package/dist/assets/{index-DkJv4l6L.css → index-tr1w3hHB.css} +1 -1
- package/dist/assets/{index-Blo38vfX.js → index-v_3LvhMx.js} +3 -3
- package/dist/assets/{settingsSchema-B0uoL_1R.js → settingsSchema-CE46TA3V.js} +1 -1
- package/dist/assets/useAgentsDirWatch-BZTi2K2e.js +1 -0
- package/dist/assets/{useConfigDirWatch-Dxv7JbCU.js → useConfigDirWatch-CjcswsWR.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +2 -1
- package/scripts/scheduler-mcp-server.cjs +14 -2
- package/src/main/build-info.json +4 -4
- package/src/main/index.cjs +8 -0
- package/src/main/ipcSchemas.cjs +93 -0
- package/src/main/lib/e2eStateMachine.cjs +39 -0
- package/src/main/lib/mcpToolCatalog.cjs +5 -5
- package/src/main/lib/usageCircuit.cjs +15 -0
- package/src/main/scheduler.cjs +57 -11
- package/src/main/usage.cjs +10 -2
- package/src/main/webRemote.cjs +1364 -0
- package/src/preload/api.d.ts +64 -1
- package/src/preload/index.cjs +39 -0
- package/dist/assets/Scheduler-CCcJzuu8.js +0 -16
- package/dist/assets/Settings-C3-VL-T8.js +0 -3
- package/dist/assets/useAgentsDirWatch-e3TJUFPP.js +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
import{aw as b,ah as n,S as N,aN as O}from"./index-Blo38vfX.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{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};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{aS as u}from"./index-v_3LvhMx.js";import{u as a}from"./useConfigDirWatch-CjcswsWR.js";function r(e,s){const o=u();a([o?`${o}/.claude/agents`:null,e?`${e}/.claude/agents`:null],s)}export{r as u};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{aw as c}from"./index-
|
|
1
|
+
import{aw as c}from"./index-v_3LvhMx.js";function s(t){const n=t.replace(/\/{2,}/g,"/");return n.length>1?n.replace(/\/+$/,""):n}function u(t,n){const r=c.useRef(n);r.current=n;const e=Array.from(new Set(t.filter(o=>!!o).map(s))),i=e.slice().sort().join("\0");c.useEffect(()=>{if(e.length===0)return;window.api.config.watch(e);const o=window.api.config.onChanged(a=>{e.includes(a.path)&&r.current()});return()=>{o(),window.api.config.unwatch(e)}},[i])}export{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-v_3LvhMx.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-tr1w3hHB.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.107.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",
|
|
@@ -132,6 +132,7 @@
|
|
|
132
132
|
"chokidar": "^4.0.1",
|
|
133
133
|
"electron": "42.11.10",
|
|
134
134
|
"node-pty": "^1.2.0-beta.12",
|
|
135
|
+
"ws": "^8.18.0",
|
|
135
136
|
"zod": "^3.23.8"
|
|
136
137
|
},
|
|
137
138
|
"devDependencies": {
|
|
@@ -143,7 +143,12 @@ const TOOLS = [
|
|
|
143
143
|
{
|
|
144
144
|
name: 'scheduler_pause',
|
|
145
145
|
description: descriptionFor('scheduler_pause'),
|
|
146
|
-
inputSchema: {
|
|
146
|
+
inputSchema: {
|
|
147
|
+
type: 'object',
|
|
148
|
+
properties: {
|
|
149
|
+
reason: { type: 'string', description: 'Why you are pausing — shown to the human.' },
|
|
150
|
+
},
|
|
151
|
+
},
|
|
147
152
|
},
|
|
148
153
|
{
|
|
149
154
|
name: 'scheduler_resume',
|
|
@@ -476,7 +481,14 @@ async function handleCallTool(request) {
|
|
|
476
481
|
return { content: [{ type: 'text', text: JSON.stringify(result) }] };
|
|
477
482
|
}
|
|
478
483
|
if (name === 'scheduler_pause' || name === 'scheduler_resume') {
|
|
479
|
-
const
|
|
484
|
+
const body = name === 'scheduler_pause'
|
|
485
|
+
? {
|
|
486
|
+
originClaudeSessionId: process.env.SM_CHAT_SESSION_ID ?? null,
|
|
487
|
+
cwd: process.cwd(),
|
|
488
|
+
reason: args && typeof args.reason === 'string' ? args.reason : undefined,
|
|
489
|
+
}
|
|
490
|
+
: {};
|
|
491
|
+
const result = await adminRequest('POST', `/admin/scheduler/${name === 'scheduler_pause' ? 'pause' : 'resume'}`, body);
|
|
480
492
|
return { content: [{ type: 'text', text: JSON.stringify(result) }] };
|
|
481
493
|
}
|
|
482
494
|
if (name === 'scheduler_list_jobs') {
|
package/src/main/build-info.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.
|
|
3
|
-
"gitSha": "
|
|
4
|
-
"gitShortSha": "
|
|
2
|
+
"version": "0.107.0",
|
|
3
|
+
"gitSha": "a18e184a574f54d301d80aeeb88e477d88741be9",
|
|
4
|
+
"gitShortSha": "a18e184a",
|
|
5
5
|
"gitBranch": "HEAD",
|
|
6
|
-
"builtAt": "2026-10-
|
|
6
|
+
"builtAt": "2026-10-09T18:00:32.692Z",
|
|
7
7
|
"dirty": false
|
|
8
8
|
}
|
package/src/main/index.cjs
CHANGED
|
@@ -128,6 +128,7 @@ const git = require('./git.cjs');
|
|
|
128
128
|
const heapSnapshot = require('./heapSnapshot.cjs');
|
|
129
129
|
const filesIpc = require('./files.cjs');
|
|
130
130
|
const { registerDocEditHandlers, attachWindow: attachDocEditWindow } = require('./docEdit.cjs');
|
|
131
|
+
const webRemote = require('./webRemote.cjs');
|
|
131
132
|
const { listExchanges } = require('./exchanges.cjs');
|
|
132
133
|
const { resolveClaudeBin } = require('./lib/claudeBin.cjs');
|
|
133
134
|
const { checkInsideHome, assertInsideHome } = require('./lib/insideHome.cjs');
|
|
@@ -395,6 +396,7 @@ async function performReboot() {
|
|
|
395
396
|
scheduler.attachWindow(mainWindow);
|
|
396
397
|
watchers.attachWindow(mainWindow);
|
|
397
398
|
pluginInstall.attachWindow(mainWindow);
|
|
399
|
+
webRemote.attachWindow(mainWindow);
|
|
398
400
|
chatRunner.attachWindow(mainWindow);
|
|
399
401
|
promptSessionEvents.attachWindow(mainWindow);
|
|
400
402
|
attachDocEditWindow(mainWindow);
|
|
@@ -1095,6 +1097,7 @@ agentMemory.registerAgentMemoryHandlers();
|
|
|
1095
1097
|
git.register(ipcMain);
|
|
1096
1098
|
filesIpc.registerFilesHandlers();
|
|
1097
1099
|
registerDocEditHandlers();
|
|
1100
|
+
webRemote.registerRemoteHandlers();
|
|
1098
1101
|
chatRunner.registerChatHandlers();
|
|
1099
1102
|
|
|
1100
1103
|
// Direct in-process PRD authoring for a queued PromptTicket classified
|
|
@@ -1454,6 +1457,7 @@ app.whenReady().then(async () => {
|
|
|
1454
1457
|
scheduler.attachWindow(mainWindow);
|
|
1455
1458
|
watchers.attachWindow(mainWindow);
|
|
1456
1459
|
pluginInstall.attachWindow(mainWindow);
|
|
1460
|
+
webRemote.attachWindow(mainWindow);
|
|
1457
1461
|
chatRunner.attachWindow(mainWindow);
|
|
1458
1462
|
promptSessionEvents.attachWindow(mainWindow);
|
|
1459
1463
|
attachDocEditWindow(mainWindow);
|
|
@@ -1484,6 +1488,9 @@ app.whenReady().then(async () => {
|
|
|
1484
1488
|
seedDevPlugin({ logger: console, writeLog: logs.writeLine }).catch((e) => {
|
|
1485
1489
|
logs.writeLine({ scope: 'seed-dev-plugin', level: 'error', message: 'seed failed', meta: { error: e?.message } });
|
|
1486
1490
|
});
|
|
1491
|
+
webRemote.init().catch((e) => {
|
|
1492
|
+
logs.writeLine({ scope: 'webRemote', level: 'error', message: 'init failed', meta: { error: e?.message } });
|
|
1493
|
+
});
|
|
1487
1494
|
// First-boot default: seed the bundled Architect + Dev Lead Agent personas
|
|
1488
1495
|
// into ~/.claude/agents/ so the Agent Library isn't empty on a fresh
|
|
1489
1496
|
// install. One-shot + idempotent; never overwrites an existing persona;
|
|
@@ -1594,6 +1601,7 @@ let teardownDone = false;
|
|
|
1594
1601
|
function runShutdownCleanup() {
|
|
1595
1602
|
if (teardownDone) return; // idempotent — will-quit may still fire after an app.exit path
|
|
1596
1603
|
teardownDone = true;
|
|
1604
|
+
webRemote.destroy();
|
|
1597
1605
|
// Mark a clean exit so the next boot can distinguish a graceful quit from an
|
|
1598
1606
|
// OOM-kill / native crash (which leaves the sentinel `open`).
|
|
1599
1607
|
crashDiagnostics.markCleanShutdown();
|
package/src/main/ipcSchemas.cjs
CHANGED
|
@@ -30,6 +30,17 @@ const ptyTabId = z.object({ tabId: z.string().min(1).max(128) });
|
|
|
30
30
|
// have a live PTY? Bounded so the renderer can't ask about unbounded lists.
|
|
31
31
|
const ptyAlive = z.object({ tabIds: z.array(z.string().min(1).max(128)).max(500) });
|
|
32
32
|
|
|
33
|
+
// v2 mobile: subscribe to a session's live state + summary. cwd is needed to
|
|
34
|
+
// locate the transcript JSONL (transcriptPath); validated against home-dir boundary
|
|
35
|
+
// in webRemote before any fs access.
|
|
36
|
+
const sessionSubscribe = z.object({
|
|
37
|
+
// tabId becomes a transcript FILENAME (`<tabId>.jsonl`) — restrict to a
|
|
38
|
+
// session-id charset (no '/', no '.', so it can't traverse out of the project
|
|
39
|
+
// transcript dir). sessionId is a UUID, which satisfies this.
|
|
40
|
+
tabId: z.string().min(1).max(128).regex(/^[A-Za-z0-9][A-Za-z0-9_-]*$/),
|
|
41
|
+
cwd: z.string().min(1).max(4096),
|
|
42
|
+
});
|
|
43
|
+
|
|
33
44
|
// 64 KiB cap per pty:write — typewriter input is bounded; a renderer firing
|
|
34
45
|
// megabytes per call is either a bug or an attack. Block it at the boundary.
|
|
35
46
|
const PTY_WRITE_MAX_BYTES = 64 * 1024;
|
|
@@ -932,6 +943,23 @@ const chatExternalSend = z.object({
|
|
|
932
943
|
),
|
|
933
944
|
});
|
|
934
945
|
|
|
946
|
+
// ──────────────────────────────────────────── Web Remote
|
|
947
|
+
// OTP is 8 uppercase alphanumeric chars (case-insensitive entry, normalised to upper in handler).
|
|
948
|
+
const WEB_REMOTE_OTP_RE = /^[A-Z0-9]{8}$/i;
|
|
949
|
+
const DEVICE_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
950
|
+
|
|
951
|
+
const webRemotePair = z.object({
|
|
952
|
+
otp: z.string().regex(WEB_REMOTE_OTP_RE),
|
|
953
|
+
}).strict();
|
|
954
|
+
|
|
955
|
+
const webRemoteRevokeDevice = z.object({
|
|
956
|
+
deviceId: z.string().regex(DEVICE_ID_RE),
|
|
957
|
+
}).strict();
|
|
958
|
+
|
|
959
|
+
const webRemoteAuditTail = z.object({
|
|
960
|
+
lines: z.number().int().min(1).max(500).optional(),
|
|
961
|
+
}).strict();
|
|
962
|
+
|
|
935
963
|
// ──────────────────────────────────────────── History
|
|
936
964
|
const DATE_YYYY_MM_DD = /^\d{4}-\d{2}-\d{2}$/;
|
|
937
965
|
|
|
@@ -1093,13 +1121,78 @@ function validated(schema, handler) {
|
|
|
1093
1121
|
};
|
|
1094
1122
|
}
|
|
1095
1123
|
|
|
1124
|
+
// ──────────────────────────────────────────── Web Remote command allowlist
|
|
1125
|
+
// Commands are split into three tiers:
|
|
1126
|
+
// READ_COMMANDS — return data; allowed when remoteEnabled=true.
|
|
1127
|
+
// SAS_GATED_READS — return sensitive user data (sessions, PRDs, logs,
|
|
1128
|
+
// transcript summaries); additionally require
|
|
1129
|
+
// _e2eAuthenticated=true (SAS confirmed by user).
|
|
1130
|
+
// A compromised relay cannot exfiltrate this data from
|
|
1131
|
+
// a session that has not been SAS-confirmed.
|
|
1132
|
+
// MUTATE_COMMANDS — write files, spawn processes, or mutate persisted
|
|
1133
|
+
// state; gated behind remoteControlEnabled=true AND
|
|
1134
|
+
// _e2eAuthenticated=true.
|
|
1135
|
+
// ALLOWED_COMMANDS is the union, kept for existing import compatibility.
|
|
1136
|
+
//
|
|
1137
|
+
// Ungated READ_COMMANDS (justify each):
|
|
1138
|
+
// cmd:app:version — exposes only the app semver string; no user data.
|
|
1139
|
+
// cmd:session:unsubscribe — teardown lifecycle; returns nothing sensitive.
|
|
1140
|
+
const READ_COMMANDS = new Set([
|
|
1141
|
+
'cmd:app:version',
|
|
1142
|
+
// v2 mobile: unsubscribe is a teardown lifecycle call with no data payload.
|
|
1143
|
+
'cmd:session:unsubscribe',
|
|
1144
|
+
]);
|
|
1145
|
+
|
|
1146
|
+
// Sensitive reads — return user data; require SAS confirmation same as MUTATE.
|
|
1147
|
+
const SAS_GATED_READS = new Set([
|
|
1148
|
+
'cmd:sessions:load',
|
|
1149
|
+
'cmd:history:aggregate',
|
|
1150
|
+
// subscribe initiates a live stream of session state/summary — sensitive.
|
|
1151
|
+
'cmd:session:subscribe',
|
|
1152
|
+
// NOTE: cmd:exchanges:list is intentionally NOT allowlisted — webRemote.cjs has
|
|
1153
|
+
// no dispatch handler for it, so an allowlist entry would only fail closed with
|
|
1154
|
+
// an opaque reject. Re-add here together with the handler when remote exchanges
|
|
1155
|
+
// are wired, so the allowlist always mirrors an actual capability.
|
|
1156
|
+
//
|
|
1157
|
+
// Scheduler/Epics commands (cmd:schedule:*) were removed here deliberately
|
|
1158
|
+
// (core scheduler/Epics redesign) — Remote no longer reaches into scheduler
|
|
1159
|
+
// internals at all; a future Remote rebuild will talk to Scheduler/Epics
|
|
1160
|
+
// through whatever higher-level surface that redesign lands on, not this
|
|
1161
|
+
// per-command allowlist.
|
|
1162
|
+
]);
|
|
1163
|
+
|
|
1164
|
+
const MUTATE_COMMANDS = new Set([
|
|
1165
|
+
'cmd:sessions:save',
|
|
1166
|
+
'cmd:pty:spawn',
|
|
1167
|
+
'cmd:pty:write',
|
|
1168
|
+
// pty:kill terminates a live session; pty:resize drives the geometry of the
|
|
1169
|
+
// user's interactive PTY — both write live process state, so they are gated
|
|
1170
|
+
// behind remoteControlEnabled + SAS like every other mutation. A read-only
|
|
1171
|
+
// mobile mirror has no business killing or resizing the desktop's session.
|
|
1172
|
+
'cmd:pty:kill',
|
|
1173
|
+
'cmd:pty:resize',
|
|
1174
|
+
// Pushes a prompt into an open tab's chat queue — a real mutation of that
|
|
1175
|
+
// tab's session, same tier as pty:write.
|
|
1176
|
+
'cmd:chat:send',
|
|
1177
|
+
]);
|
|
1178
|
+
|
|
1179
|
+
const ALLOWED_COMMANDS = new Set([...READ_COMMANDS, ...SAS_GATED_READS, ...MUTATE_COMMANDS]);
|
|
1180
|
+
|
|
1096
1181
|
module.exports = {
|
|
1097
1182
|
// Centralized slug regex — used by scheduler.cjs and queueOps.cjs for
|
|
1098
1183
|
// direct test()/match() containment checks alongside the zod parses.
|
|
1099
1184
|
SCHEDULE_SLUG_RE,
|
|
1100
1185
|
SCHEDULE_RUN_ID_RE,
|
|
1101
1186
|
PRD_CREATE_SLUG_RE,
|
|
1187
|
+
READ_COMMANDS,
|
|
1188
|
+
SAS_GATED_READS,
|
|
1189
|
+
MUTATE_COMMANDS,
|
|
1190
|
+
ALLOWED_COMMANDS,
|
|
1102
1191
|
schemas: {
|
|
1192
|
+
webRemotePair,
|
|
1193
|
+
webRemoteRevokeDevice,
|
|
1194
|
+
webRemoteAuditTail,
|
|
1195
|
+
sessionSubscribe,
|
|
1103
1196
|
ptySpawn,
|
|
1104
1197
|
ptyTabId,
|
|
1105
1198
|
ptyAlive,
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure E2E session state machine for the web-remote relay.
|
|
3
|
+
* No Electron, no I/O — importable in unit tests.
|
|
4
|
+
*
|
|
5
|
+
* State transitions:
|
|
6
|
+
* idle → pending_sas : successful deriveSessionKey + deriveSas
|
|
7
|
+
* idle → failed : crypto derivation error
|
|
8
|
+
* pending_sas → authenticated : user confirms SAS
|
|
9
|
+
* pending_sas → failed : deriveSas threw after sessionKey succeeded
|
|
10
|
+
* any → idle : disconnect / reset
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** @returns {{ state: string, sessionKey: Buffer|null, pendingSas: string|null }} */
|
|
14
|
+
function makeState(state = 'idle', sessionKey = null, pendingSas = null) {
|
|
15
|
+
return { state, sessionKey, pendingSas };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Attempt to confirm the SAS. Pure — does not mutate; returns the next state.
|
|
20
|
+
* @param {{ state: string, sessionKey: Buffer|null, pendingSas: string|null }} e2eState
|
|
21
|
+
* @returns {{ ok: boolean, error?: string, next: { state: string, sessionKey: Buffer|null, pendingSas: string|null } }}
|
|
22
|
+
*/
|
|
23
|
+
function confirmSas(e2eState) {
|
|
24
|
+
if (e2eState.state !== 'pending_sas') {
|
|
25
|
+
const errorMap = {
|
|
26
|
+
idle: 'no_e2e_session',
|
|
27
|
+
failed: 'e2e_failed',
|
|
28
|
+
authenticated: 'already_authenticated',
|
|
29
|
+
};
|
|
30
|
+
const error = errorMap[e2eState.state] ?? 'unexpected_state';
|
|
31
|
+
return { ok: false, error, next: e2eState };
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
ok: true,
|
|
35
|
+
next: makeState('authenticated', e2eState.sessionKey, null),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
module.exports = { makeState, confirmSas };
|
|
@@ -50,11 +50,11 @@ const MCP_TOOL_CATALOG = [
|
|
|
50
50
|
{
|
|
51
51
|
name: 'scheduler_pause',
|
|
52
52
|
group: 'scheduler',
|
|
53
|
-
purpose: "Pause
|
|
54
|
-
whenToUse: 'Use to stop
|
|
55
|
-
whenNotToUse: 'Do not use to cancel a running job (use scheduler_cancel_job) or as a workaround for a stuck queue.
|
|
56
|
-
exampleArgs: {},
|
|
57
|
-
notes: '
|
|
53
|
+
purpose: "Pause NEW job dispatch for every project on this machine (machine-wide, not per-repo) via the session-manager app's admin API.",
|
|
54
|
+
whenToUse: 'Use only when the human asks you to stop dispatch everywhere, or for a machine-wide emergency. Pass a `reason`. Running jobs are never killed.',
|
|
55
|
+
whenNotToUse: "NEVER use it to protect one repo's git/branch state or a single Epic's work — it freezes every project's scheduler. Do not use it to cancel a running job (use scheduler_cancel_job) or as a workaround for a stuck queue.",
|
|
56
|
+
exampleArgs: { reason: 'machine-wide emergency: disk full' },
|
|
57
|
+
notes: 'An agent pause auto-expires after 30 minutes, is cleared on app restart, and is audited with the caller (session id + cwd). A human pause outranks it and is only cleared by scheduler_resume.',
|
|
58
58
|
},
|
|
59
59
|
{
|
|
60
60
|
name: 'scheduler_resume',
|
|
@@ -137,6 +137,20 @@ function degradedBudget(lastGoodPayload, executorEvidence = {}) {
|
|
|
137
137
|
return { utilization, concurrencyCap };
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
+
/**
|
|
141
|
+
* Budget while the METER (not the account) is rate-limited: a 429 from the
|
|
142
|
+
* usage endpoint says nothing about account usage, so a recent-enough cached
|
|
143
|
+
* payload is trusted as-is. Returns `{ utilization, degraded: false }` from
|
|
144
|
+
* the cache, or null (no/old cache) so the caller falls back to degradedBudget().
|
|
145
|
+
*/
|
|
146
|
+
function meterFailureBudget({ cachedPayload, staleSinceMs, now = Date.now(), maxStaleMs = 30 * 60 * 1000 } = {}) {
|
|
147
|
+
if (!cachedPayload || !Number.isFinite(staleSinceMs)) return null;
|
|
148
|
+
if (now - staleSinceMs > maxStaleMs) return null;
|
|
149
|
+
const window = bindingWindow(cachedPayload);
|
|
150
|
+
if (!Number.isFinite(window.utilization)) return null;
|
|
151
|
+
return { utilization: window.utilization, degraded: false, name: window.name };
|
|
152
|
+
}
|
|
153
|
+
|
|
140
154
|
/** Wraps `fn` so concurrent callers awaiting an in-flight call share one promise. */
|
|
141
155
|
function singleFlight(fn) {
|
|
142
156
|
let inFlight = null;
|
|
@@ -229,6 +243,7 @@ module.exports = {
|
|
|
229
243
|
isResetFresh,
|
|
230
244
|
bindingWindow,
|
|
231
245
|
degradedBudget,
|
|
246
|
+
meterFailureBudget,
|
|
232
247
|
degradedConcurrencyCap,
|
|
233
248
|
singleFlight,
|
|
234
249
|
};
|
package/src/main/scheduler.cjs
CHANGED
|
@@ -78,7 +78,7 @@ const { detectRateLimitInLog } = require('./lib/rateLimitDetect.cjs');
|
|
|
78
78
|
const { stripAppOwnedChurn } = require('./lib/jobDirtFilter.cjs');
|
|
79
79
|
const { GATE_AUTHORITY_VERDICTS, decideGateAuthority } = require('./lib/gateAuthority.cjs');
|
|
80
80
|
const { resolveBindingRateLimitReset } = require('./lib/rateLimitWindow.cjs');
|
|
81
|
-
const { isResetFresh, bindingWindow, degradedBudget } = require('./lib/usageCircuit.cjs');
|
|
81
|
+
const { isResetFresh, bindingWindow, degradedBudget, meterFailureBudget } = require('./lib/usageCircuit.cjs');
|
|
82
82
|
const { computeQueueHealth } = require('./lib/queueHealth.cjs');
|
|
83
83
|
const { createLoadGate, topCpuConsumers } = require('./lib/loadGate.cjs');
|
|
84
84
|
const { openLog, withChildAndLog } = require('./lib/childWithLog.cjs');
|
|
@@ -3499,8 +3499,11 @@ function recordObservedReset(resetIso) {
|
|
|
3499
3499
|
/** Pure: this poll/executor cycle's conservative budget while the meter is degraded. */
|
|
3500
3500
|
function computeDegradedBudget() {
|
|
3501
3501
|
return degradedBudget(lastGoodUsagePayload, {
|
|
3502
|
-
|
|
3503
|
-
|
|
3502
|
+
// Only an executor-observed rate limit (the paused.reason === 'rate_limit'
|
|
3503
|
+
// path) may pin utilization; a 429 from the METER says nothing about the
|
|
3504
|
+
// account. That pause already holds dispatch itself, so no evidence here.
|
|
3505
|
+
observed429: false,
|
|
3506
|
+
resetsAt: null,
|
|
3504
3507
|
now: Date.now(),
|
|
3505
3508
|
configuredCap: sessionSlots.snapshot().total,
|
|
3506
3509
|
});
|
|
@@ -4047,8 +4050,11 @@ function nextRapidRateLimitCount(prevCount, { rateLimited, durationMs }) {
|
|
|
4047
4050
|
* either, which used to leave an indefinite pause with no resume timer at
|
|
4048
4051
|
* all (a queue that never comes back on its own) — this covers both.
|
|
4049
4052
|
*/
|
|
4053
|
+
const AGENT_PAUSE_TTL_MS = 30 * 60_000;
|
|
4054
|
+
|
|
4050
4055
|
function computeEffectiveResumeAt(reason, resumeAtIso, nowMs = Date.now()) {
|
|
4051
4056
|
if (resumeAtIso && isResetFresh(resumeAtIso, nowMs)) return resumeAtIso;
|
|
4057
|
+
if (reason === 'agent') return new Date(nowMs + AGENT_PAUSE_TTL_MS).toISOString();
|
|
4052
4058
|
if (reason === 'network' || reason === 'rate_limit') {
|
|
4053
4059
|
return new Date(nowMs + 30 * 60_000).toISOString();
|
|
4054
4060
|
}
|
|
@@ -4067,15 +4073,16 @@ function computeResumeDelay(effectiveResumeAtIso, nowMs = Date.now()) {
|
|
|
4067
4073
|
}
|
|
4068
4074
|
|
|
4069
4075
|
async function setPaused(reason, resumeAtIso, opts = {}) {
|
|
4070
|
-
const { observedAt = null, force = false } = opts;
|
|
4076
|
+
const { observedAt = null, force = false, by = null } = opts;
|
|
4071
4077
|
const isManual = reason === 'manual';
|
|
4078
|
+
const isAgent = reason === 'agent';
|
|
4072
4079
|
// Honor manual-override cooldown: if the user cleared a pause within the
|
|
4073
4080
|
// last 5 minutes, suppress auto-pause re-engagement UNLESS this pause is
|
|
4074
4081
|
// backed by a fresh observation (a run that started after the clear) or is
|
|
4075
4082
|
// forced (the rapid-repeat hard pause, which the cooldown cannot suppress).
|
|
4076
4083
|
// A user-initiated 'manual' pause is never an auto-detection, so the cooldown
|
|
4077
4084
|
// (which exists to ignore STALE auto-detections) does not apply to it.
|
|
4078
|
-
if (!isManual && isCooldownSuppressed({ pauseClearedManuallyAt, now: Date.now(), observedAt, force })) {
|
|
4085
|
+
if (!isManual && !isAgent && isCooldownSuppressed({ pauseClearedManuallyAt, now: Date.now(), observedAt, force })) {
|
|
4079
4086
|
console.log(`[scheduler] setPaused(${reason}) suppressed by manual override cooldown`);
|
|
4080
4087
|
return;
|
|
4081
4088
|
}
|
|
@@ -4088,6 +4095,7 @@ async function setPaused(reason, resumeAtIso, opts = {}) {
|
|
|
4088
4095
|
// 'manual' never arms a resume timer: only schedule:resume / run-now clears it.
|
|
4089
4096
|
const effectiveResumeAt = isManual ? null : computeEffectiveResumeAt(reason, resumeAtIso);
|
|
4090
4097
|
|
|
4098
|
+
let committed = null;
|
|
4091
4099
|
const kept = await mutate((s) => {
|
|
4092
4100
|
// A user pause outranks every auto-pause (rate_limit/auth/network): the
|
|
4093
4101
|
// auto path must not overwrite it, or its resume timer would auto-clear it.
|
|
@@ -4097,12 +4105,15 @@ async function setPaused(reason, resumeAtIso, opts = {}) {
|
|
|
4097
4105
|
} else {
|
|
4098
4106
|
s.paused = { reason, since: new Date().toISOString(), resumeAt: effectiveResumeAt || null };
|
|
4099
4107
|
}
|
|
4108
|
+
if (by && isAgent) s.paused.by = by;
|
|
4109
|
+
committed = { reason: s.paused.reason, resumeAt: s.paused.resumeAt ?? null, by: s.paused.by ?? null };
|
|
4100
4110
|
return false;
|
|
4101
4111
|
});
|
|
4102
4112
|
if (kept) {
|
|
4103
4113
|
console.log(`[scheduler] setPaused(${reason}) ignored: a manual pause is in force`);
|
|
4104
4114
|
return;
|
|
4105
4115
|
}
|
|
4116
|
+
if (committed) appendAuditEvent('scheduler_pause', committed);
|
|
4106
4117
|
await broadcast({ flush: true });
|
|
4107
4118
|
cancelToken.cancelled = true;
|
|
4108
4119
|
if (resumeTimer) { clearTimeout(resumeTimer); resumeTimer = null; }
|
|
@@ -4123,6 +4134,7 @@ async function setPaused(reason, resumeAtIso, opts = {}) {
|
|
|
4123
4134
|
async function clearPause(source) {
|
|
4124
4135
|
if (resumeTimer) { clearTimeout(resumeTimer); resumeTimer = null; }
|
|
4125
4136
|
const humanOverride = source === 'manual' || source === 'run-now';
|
|
4137
|
+
let clearedReason = null;
|
|
4126
4138
|
const wasPaused = await mutate((s) => {
|
|
4127
4139
|
// A human Resume / Run now also re-closes every launch circuit breaker:
|
|
4128
4140
|
// the operator is asserting the environment is fixed (CLI updated,
|
|
@@ -4135,9 +4147,11 @@ async function clearPause(source) {
|
|
|
4135
4147
|
}
|
|
4136
4148
|
if (!s.paused) return false;
|
|
4137
4149
|
console.log(`[scheduler] clearPause (${source || 'manual'})`);
|
|
4150
|
+
clearedReason = s.paused.reason ?? null;
|
|
4138
4151
|
s.paused = null;
|
|
4139
4152
|
return true;
|
|
4140
4153
|
});
|
|
4154
|
+
if (wasPaused) appendAuditEvent('scheduler_resume', { source: source || 'manual', clearedReason });
|
|
4141
4155
|
// Un-cancel the tick guard on every recovery path, not just runDueJobs().
|
|
4142
4156
|
applyPauseCleared(wasPaused, cancelToken);
|
|
4143
4157
|
if (wasPaused) lastPauseClearedAt = Date.now();
|
|
@@ -10377,8 +10391,17 @@ async function pollLoop() {
|
|
|
10377
10391
|
// Don't update firstNon429FailureAt — 429s don't count toward the 30-min network-pause threshold.
|
|
10378
10392
|
backoffMs = nextBackoffMs(backoffMs);
|
|
10379
10393
|
backoffNextAt = Date.now() + backoffMs;
|
|
10380
|
-
|
|
10381
|
-
|
|
10394
|
+
const cachedBudget = meterFailureBudget({ cachedPayload: r.cached, staleSinceMs: r.staleSince, now: Date.now() });
|
|
10395
|
+
if (cachedBudget) {
|
|
10396
|
+
// Meter 429 ≠ account limit: trust the recent cached reading, full slot pool.
|
|
10397
|
+
lastGoodUsagePayload = r.cached;
|
|
10398
|
+
cachedUtilization = cachedBudget.utilization;
|
|
10399
|
+
cachedBindingWindowName = cachedBudget.name;
|
|
10400
|
+
degradedConcurrencyCapValue = null;
|
|
10401
|
+
} else {
|
|
10402
|
+
applyDegradedBudget();
|
|
10403
|
+
}
|
|
10404
|
+
console.log(`[scheduler] billing meter rate-limited (HTTP 429) — firing on ${cachedBudget ? 'cached' : 'degraded'} budget (util=${cachedUtilization}%, cap=${degradedConcurrencyCapValue}) (failure #${consecutiveFailures}); retry in ${backoffMs / 1000}s`);
|
|
10382
10405
|
warnFailureStreakIfNeeded();
|
|
10383
10406
|
persistSchedulerState();
|
|
10384
10407
|
const cur = await readQueue();
|
|
@@ -12948,7 +12971,10 @@ async function init() {
|
|
|
12948
12971
|
// happens when the app was closed across the reset window.
|
|
12949
12972
|
const boot = await readQueue();
|
|
12950
12973
|
await clearStaleDrainAtBoot(boot);
|
|
12951
|
-
if (boot.paused && boot.paused.
|
|
12974
|
+
if (boot.paused && (boot.paused.reason === 'manual' || boot.paused.reason === 'agent')) {
|
|
12975
|
+
console.log(`[scheduler] boot: clearing persisted '${boot.paused.reason}' pause so dispatch runs`);
|
|
12976
|
+
await clearPause('boot');
|
|
12977
|
+
} else if (boot.paused && boot.paused.resumeAt && new Date(boot.paused.resumeAt).getTime() <= Date.now()) {
|
|
12952
12978
|
await clearPause('boot-elapsed');
|
|
12953
12979
|
} else if (boot.paused && boot.paused.resumeAt) {
|
|
12954
12980
|
// Re-arm the resume timer (lost across restart).
|
|
@@ -13293,8 +13319,15 @@ const remote = {
|
|
|
13293
13319
|
// User-initiated pause/resume — the admin-route/MCP twins of the
|
|
13294
13320
|
// schedule:pause / schedule:resume IPC handlers, through the same setPaused /
|
|
13295
13321
|
// clearPause. Pause stops NEW dispatch only; running jobs are never touched.
|
|
13296
|
-
async pause() {
|
|
13297
|
-
|
|
13322
|
+
async pause(opts = {}) {
|
|
13323
|
+
const str = (v, max = 300) => (typeof v === 'string' && v ? v.slice(0, max) : null);
|
|
13324
|
+
await setPaused('agent', null, {
|
|
13325
|
+
by: {
|
|
13326
|
+
originClaudeSessionId: str(opts.originClaudeSessionId),
|
|
13327
|
+
cwd: str(opts.cwd, 1000),
|
|
13328
|
+
reason: str(opts.reason),
|
|
13329
|
+
},
|
|
13330
|
+
});
|
|
13298
13331
|
return { ok: true };
|
|
13299
13332
|
},
|
|
13300
13333
|
|
|
@@ -13650,7 +13683,20 @@ function registerAdminRoutes(adminHttp, remoteObj = remote) {
|
|
|
13650
13683
|
});
|
|
13651
13684
|
|
|
13652
13685
|
adminHttp.registerRoute('POST', '/admin/scheduler/pause', async (req, res) => {
|
|
13653
|
-
|
|
13686
|
+
const raw = await readBody(req);
|
|
13687
|
+
let parsed;
|
|
13688
|
+
try {
|
|
13689
|
+
parsed = raw ? JSON.parse(raw) : {};
|
|
13690
|
+
} catch {
|
|
13691
|
+
sendJson(res, 400, { ok: false, error: 'invalid JSON body' });
|
|
13692
|
+
return;
|
|
13693
|
+
}
|
|
13694
|
+
if (!parsed || typeof parsed !== 'object') parsed = {};
|
|
13695
|
+
sendJson(res, 200, await remoteObj.pause({
|
|
13696
|
+
originClaudeSessionId: parsed.originClaudeSessionId,
|
|
13697
|
+
cwd: parsed.cwd,
|
|
13698
|
+
reason: parsed.reason,
|
|
13699
|
+
}));
|
|
13654
13700
|
});
|
|
13655
13701
|
|
|
13656
13702
|
adminHttp.registerRoute('POST', '/admin/scheduler/resume', async (req, res) => {
|
package/src/main/usage.cjs
CHANGED
|
@@ -302,10 +302,18 @@ async function fetchUsage() {
|
|
|
302
302
|
}
|
|
303
303
|
|
|
304
304
|
if (lastRateLimitedResult && retryNotBeforeMs && Date.now() < retryNotBeforeMs) {
|
|
305
|
-
return { ...lastRateLimitedResult, suppressed: true };
|
|
305
|
+
return withCachedPayload({ ...lastRateLimitedResult, suppressed: true });
|
|
306
306
|
}
|
|
307
307
|
|
|
308
|
-
return singleFlightNetworkFetch();
|
|
308
|
+
return withCachedPayload(await singleFlightNetworkFetch());
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/** Attaches the last good payload (+ its age anchor) to a 429 so callers can keep using it. */
|
|
312
|
+
function withCachedPayload(r) {
|
|
313
|
+
if (r && r.kind === 'meter_rate_limited' && cache && cache.data) {
|
|
314
|
+
return { ...r, cached: cache.data, staleSince: cache.fetchedAt };
|
|
315
|
+
}
|
|
316
|
+
return r;
|
|
309
317
|
}
|
|
310
318
|
|
|
311
319
|
function registerBillingHandlers() {
|