@shawnstack/quickforge 1.6.10 → 1.6.12

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 (30) hide show
  1. package/README.md +292 -452
  2. package/dist/assets/{AgentProfilesPage-BtZpDV-3.js → AgentProfilesPage-CooGLweI.js} +1 -1
  3. package/dist/assets/ChatPanelHost-TEbv0lnL.js +260 -0
  4. package/dist/assets/{PluginsPage-CCIj8-Mn.js → PluginsPage-qXTjDrpd.js} +1 -1
  5. package/dist/assets/{ScheduledTasksPage-DIMewQ3i.js → ScheduledTasksPage-BBor8Yb2.js} +1 -1
  6. package/dist/assets/{SettingsWorkspacePage-BPD495WT.js → SettingsWorkspacePage-CnFDmGvW.js} +190 -120
  7. package/dist/assets/{SharedConversationPage-B_oMTp7B.js → SharedConversationPage-CkQ-XwYS.js} +1 -1
  8. package/dist/assets/{TerminalDock-wgW7kSC4.js → TerminalDock-CvqBBMPg.js} +1 -1
  9. package/dist/assets/{WorkspaceInspector-CdX45r3u.js → WorkspaceInspector-6JGc6VGb.js} +1 -1
  10. package/dist/assets/index-DxNzlPmc.js +63 -0
  11. package/dist/assets/index-HVxfqXfB.css +3 -0
  12. package/dist/assets/{mcp-servers-dialog-jHlDR9jP.js → mcp-servers-dialog-CsRQxx7A.js} +1 -1
  13. package/dist/assets/{skills-dialog-DvSnPPzu.js → skills-dialog-CmVLi_h6.js} +1 -1
  14. package/dist/index.html +4 -4
  15. package/package.json +1 -1
  16. package/server/agent-manager.mjs +45 -3
  17. package/server/approval-store.mjs +4 -0
  18. package/server/global-memory.mjs +168 -0
  19. package/server/index.mjs +7 -0
  20. package/server/project-config.mjs +3 -0
  21. package/server/routes/memory.mjs +26 -0
  22. package/server/routes/project.mjs +3 -18
  23. package/server/routes/tools.mjs +5 -3
  24. package/server/system-prompt.mjs +22 -0
  25. package/server/tools/definitions.mjs +14 -0
  26. package/server/tools/index.mjs +6 -0
  27. package/server/utils/platform.mjs +20 -3
  28. package/dist/assets/ChatPanelHost-BF1V-xOV.js +0 -244
  29. package/dist/assets/index-Bxg5fT2p.css +0 -3
  30. package/dist/assets/index-DKmdePUP.js +0 -63
@@ -1 +1 @@
1
- import{i as e,n as t}from"./rolldown-runtime-DWdDZTNf.js";import{B as n,It as r,Rt as i,Vt as a,_ as o,n as s}from"./icons-C7j5jdKo.js";import{n as c}from"./react-vendor-VqdnQHHS.js";import{ct as l,dt as u,ht as d}from"./index-DKmdePUP.js";var f=t({SkillsDialog:()=>_,SkillsManagerPanel:()=>g}),p=e(a(),1),m=c();async function h(e){let t=await e.json().catch(()=>null);if(!e.ok)throw Error(t?.error||`HTTP ${e.status}`);return t}function g({active:e=!0,scope:t,project:a,embedded:c=!1,onClose:f,onSaved:g}){let[_,v]=(0,p.useState)([]),[y,b]=(0,p.useState)(()=>new Set),[x,S]=(0,p.useState)(``),[C,w]=(0,p.useState)([]),[T,E]=(0,p.useState)(!1),[D,O]=(0,p.useState)(!1),[k,A]=(0,p.useState)(``),j=t===`project`,[M,N]=(0,p.useState)(null),[P,F]=(0,p.useState)(null),[I,L]=(0,p.useState)(!1),[R,z]=(0,p.useState)(``),B=(0,p.useCallback)(()=>{N(null),F(null),z(``)},[]),V=(0,p.useCallback)(()=>{B(),f?.()},[f,B]);(0,p.useEffect)(()=>{if(!e||j&&!a)return;let t=!1;return(async()=>{E(!0),A(``);try{let e=j?`/api/skills?projectId=${encodeURIComponent(a.id)}`:`/api/skills?scope=global`,n=await h(await fetch(e));if(t)return;v(Array.isArray(n.skills)?n.skills:[]),b(new Set(Array.isArray(n.selectedSkills)?n.selectedSkills:[])),w(Array.isArray(n.searchPaths)?n.searchPaths:[])}catch(e){t||A(e instanceof Error?e.message:d(`failedToLoadSkills`))}finally{t||E(!1)}})(),()=>{t=!0}},[e,a,j]),(0,p.useEffect)(()=>{if(!e||c)return;let t=e=>{e.key===`Escape`&&(M?B():D||V())};return document.addEventListener(`keydown`,t),()=>document.removeEventListener(`keydown`,t)},[e,V,c,D,M,B]);let H=(0,p.useMemo)(()=>{let e=x.trim().toLowerCase();return e?_.filter(t=>[t.name,t.displayName,t.description,t.source,t.compatibility,t.allowedTools,...t.tags??[],...t.triggers??[]].filter(Boolean).join(` `).toLowerCase().includes(e)):_},[x,_]),U=async e=>{N(e),F(null),z(``),L(!0);try{let t=new URLSearchParams({name:e});j&&a?(t.set(`scope`,`project`),t.set(`projectId`,a.id)):t.set(`scope`,`global`),F(await h(await fetch(`/api/skills/content?${t}`)))}catch(e){z(e instanceof Error?e.message:d(`failedToReadSkill`))}finally{L(!1)}};if(!e||j&&!a)return null;let W=e=>{if(D)return;let t=new Set(y);t.has(e)?t.delete(e):t.add(e),b(t),G(t)},G=async e=>{O(!0),A(``);try{let n=await h(await fetch(j?`/api/skills/project`:`/api/skills/global`,{method:`PUT`,headers:{"content-type":`application/json`},body:JSON.stringify(j?{projectId:a.id,selectedSkills:[...e]}:{selectedSkills:[...e]})}));g({scope:t,project:j?n.projects?.find(e=>e.id===a.id)??{...a,skills:n.selectedSkills}:void 0,projects:n.projects,selectedSkills:n.selectedSkills})}catch(e){b(y),A(e instanceof Error?e.message:d(`failedToSaveSkills`))}finally{O(!1)}},K=d(j?`projectSkills`:`globalSkills`),q=j?d(`projectSkillsDescription`,{project:a.name}):d(`globalSkillsDescription`),J=(0,m.jsxs)(m.Fragment,{children:[(0,m.jsx)(`div`,{className:`quickforge-settings-divider p-3`,children:(0,m.jsxs)(`div`,{className:`quickforge-settings-inline-field`,children:[(0,m.jsx)(o,{className:`pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground/60`}),(0,m.jsx)(`input`,{value:x,onChange:e=>S(e.target.value),placeholder:d(`searchSkills`),className:`quickforge-settings-input`,disabled:T||D})]})}),k?(0,m.jsx)(`div`,{className:`quickforge-settings-alert quickforge-settings-warning-attached`,children:k}):null,(0,m.jsxs)(`div`,{className:`quickforge-settings-toolbar`,children:[(0,m.jsx)(`span`,{className:`quickforge-settings-row-title`,children:d(`availableSkills`)}),(0,m.jsx)(`span`,{className:`quickforge-settings-badge quickforge-settings-badge-info`,children:d(`selectedSkillsCount`,{count:y.size})})]}),(0,m.jsx)(`div`,{className:c?``:`max-h-[46vh] overflow-y-auto`,children:T?(0,m.jsxs)(`div`,{className:`quickforge-settings-empty-row inline-flex items-center gap-2`,children:[(0,m.jsx)(n,{className:`size-4 animate-spin`}),d(`loading`)]}):H.length===0?(0,m.jsx)(`div`,{className:`quickforge-settings-empty-row`,children:d(`noMatchingSkills`)}):H.map(e=>{let t=y.has(e.name);return(0,m.jsxs)(`div`,{className:u(`quickforge-settings-list-item`,t&&`bg-muted/12`),children:[(0,m.jsxs)(`div`,{className:`quickforge-settings-list-item-main`,children:[(0,m.jsx)(`div`,{className:`quickforge-settings-row-title`,children:e.displayName||e.name}),e.description?(0,m.jsx)(`div`,{className:`quickforge-settings-row-description`,children:e.description}):null,(0,m.jsxs)(`div`,{className:`quickforge-settings-meta`,children:[e.source?(0,m.jsx)(`span`,{className:`quickforge-settings-badge quickforge-settings-badge-muted`,children:e.source}):null,e.compatibility?(0,m.jsx)(`span`,{className:`quickforge-settings-badge quickforge-settings-badge-muted`,children:e.compatibility}):null,e.allowedTools?(0,m.jsx)(`span`,{className:`quickforge-settings-badge quickforge-settings-badge-muted`,children:e.allowedTools}):null,e.tags?.slice(0,5).map(e=>(0,m.jsx)(`span`,{className:`quickforge-settings-badge quickforge-settings-badge-muted`,children:e},e))]})]}),(0,m.jsxs)(`div`,{className:`quickforge-settings-list-item-actions`,onClick:e=>e.stopPropagation(),children:[(0,m.jsxs)(`label`,{className:`quickforge-settings-switch`,"aria-disabled":D?`true`:`false`,children:[(0,m.jsx)(`input`,{type:`checkbox`,checked:t,disabled:D,onChange:()=>W(e.name),"aria-label":t?`Disable ${e.name}`:`Enable ${e.name}`}),(0,m.jsx)(`span`,{"aria-hidden":`true`})]}),(0,m.jsx)(`button`,{type:`button`,className:`quickforge-settings-icon-action`,onClick:()=>void U(e.name),disabled:D||I,title:d(`readSkill`),"aria-label":`${d(`readSkill`)}: ${e.displayName||e.name}`,children:(0,m.jsx)(r,{className:`size-4`})})]})]},e.name)})})]}),Y=(0,m.jsxs)(m.Fragment,{children:[(0,m.jsxs)(`div`,{className:`quickforge-settings-toolbar`,children:[(0,m.jsxs)(`button`,{type:`button`,className:`quickforge-settings-button quickforge-settings-button-secondary`,onClick:B,disabled:I,children:[(0,m.jsx)(i,{className:`mr-2 size-4`}),d(`backToSkillList`)]}),(0,m.jsx)(`div`,{className:`quickforge-settings-row-main min-w-0`,children:(0,m.jsx)(`div`,{className:`quickforge-settings-row-title truncate`,children:P?.displayName||P?.name||M})})]}),(0,m.jsx)(`div`,{className:`p-4`,children:I?(0,m.jsxs)(`div`,{className:`quickforge-settings-empty-row inline-flex items-center gap-2`,children:[(0,m.jsx)(n,{className:`size-4 animate-spin`}),d(`loading`)]}):R?(0,m.jsx)(`div`,{className:`quickforge-settings-alert quickforge-settings-warning-attached`,children:R}):P?(0,m.jsxs)(m.Fragment,{children:[P.description?(0,m.jsx)(`div`,{className:`quickforge-settings-row-description mb-3`,children:P.description}):null,(0,m.jsxs)(`div`,{className:`quickforge-settings-meta mb-3`,children:[P.version?(0,m.jsxs)(`span`,{className:`quickforge-settings-badge quickforge-settings-badge-muted`,children:[`v`,P.version]}):null,P.source?(0,m.jsx)(`span`,{className:`quickforge-settings-badge quickforge-settings-badge-muted`,children:P.source}):null,P.compatibility?(0,m.jsx)(`span`,{className:`quickforge-settings-badge quickforge-settings-badge-muted`,children:P.compatibility}):null,P.allowedTools?(0,m.jsx)(`span`,{className:`quickforge-settings-badge quickforge-settings-badge-muted`,children:P.allowedTools}):null,P.license?(0,m.jsx)(`span`,{className:`quickforge-settings-badge quickforge-settings-badge-muted`,children:P.license}):null,P.tags?.map(e=>(0,m.jsx)(`span`,{className:`quickforge-settings-badge quickforge-settings-badge-muted`,children:e},e))]}),P.triggers?.length?(0,m.jsxs)(`div`,{className:`quickforge-settings-row-description mb-3`,children:[`Triggers: `,P.triggers.join(`, `)]}):null,P.instructions?(0,m.jsx)(`pre`,{className:`whitespace-pre-wrap break-words rounded-md border border-border bg-muted/30 px-4 py-3 font-mono text-sm leading-6 text-foreground/80`,children:P.instructions}):(0,m.jsx)(`div`,{className:`quickforge-settings-empty-row`,children:d(`noSkillContent`)})]}):null})]});return c?(0,m.jsx)(`div`,{className:`quickforge-settings-stack`,children:(0,m.jsx)(`section`,{className:`quickforge-settings-section`,"aria-label":K,children:M?Y:(0,m.jsxs)(m.Fragment,{children:[C.length?(0,m.jsx)(`div`,{className:`px-5 pt-4`,children:(0,m.jsxs)(`span`,{className:`quickforge-settings-row-description break-all`,children:[d(`skillSearchPaths`),`: `,C.join(` · `)]})}):null,J]})})}):(0,m.jsx)(`div`,{className:`fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4`,onClick:e=>{e.target===e.currentTarget&&!D&&V()},children:(0,m.jsxs)(`div`,{className:`flex max-h-[85vh] w-full max-w-2xl flex-col rounded-lg border border-border bg-background shadow-quickforge`,children:[(0,m.jsxs)(`div`,{className:`quickforge-settings-toolbar`,children:[(0,m.jsxs)(`div`,{className:`quickforge-settings-row-main`,children:[(0,m.jsxs)(`div`,{className:`quickforge-settings-row-title`,children:[K,(0,m.jsx)(l,{label:q})]}),(0,m.jsx)(`div`,{className:`quickforge-settings-row-description`,children:q}),C.length?(0,m.jsxs)(`div`,{className:`quickforge-settings-row-description break-all`,children:[d(`skillSearchPaths`),`: `,C.join(` · `)]}):null]}),(0,m.jsx)(`button`,{type:`button`,className:`quickforge-settings-icon-action`,onClick:V,disabled:D,"aria-label":d(`close`),children:(0,m.jsx)(s,{className:`size-4`})})]}),(0,m.jsx)(`div`,{className:`min-h-0 flex-1 overflow-y-auto`,children:M?Y:J})]})})}function _({open:e,scope:t,project:n,onOpenChange:r,onSaved:i}){return(0,m.jsx)(g,{active:e,scope:t,project:n,onClose:()=>r(!1),onSaved:i})}export{f as n,_ as t};
1
+ import{i as e,n as t}from"./rolldown-runtime-DWdDZTNf.js";import{B as n,It as r,Rt as i,Vt as a,_ as o,n as s}from"./icons-C7j5jdKo.js";import{n as c}from"./react-vendor-VqdnQHHS.js";import{ct as l,dt as u,ht as d}from"./index-DxNzlPmc.js";var f=t({SkillsDialog:()=>_,SkillsManagerPanel:()=>g}),p=e(a(),1),m=c();async function h(e){let t=await e.json().catch(()=>null);if(!e.ok)throw Error(t?.error||`HTTP ${e.status}`);return t}function g({active:e=!0,scope:t,project:a,embedded:c=!1,onClose:f,onSaved:g}){let[_,v]=(0,p.useState)([]),[y,b]=(0,p.useState)(()=>new Set),[x,S]=(0,p.useState)(``),[C,w]=(0,p.useState)([]),[T,E]=(0,p.useState)(!1),[D,O]=(0,p.useState)(!1),[k,A]=(0,p.useState)(``),j=t===`project`,[M,N]=(0,p.useState)(null),[P,F]=(0,p.useState)(null),[I,L]=(0,p.useState)(!1),[R,z]=(0,p.useState)(``),B=(0,p.useCallback)(()=>{N(null),F(null),z(``)},[]),V=(0,p.useCallback)(()=>{B(),f?.()},[f,B]);(0,p.useEffect)(()=>{if(!e||j&&!a)return;let t=!1;return(async()=>{E(!0),A(``);try{let e=j?`/api/skills?projectId=${encodeURIComponent(a.id)}`:`/api/skills?scope=global`,n=await h(await fetch(e));if(t)return;v(Array.isArray(n.skills)?n.skills:[]),b(new Set(Array.isArray(n.selectedSkills)?n.selectedSkills:[])),w(Array.isArray(n.searchPaths)?n.searchPaths:[])}catch(e){t||A(e instanceof Error?e.message:d(`failedToLoadSkills`))}finally{t||E(!1)}})(),()=>{t=!0}},[e,a,j]),(0,p.useEffect)(()=>{if(!e||c)return;let t=e=>{e.key===`Escape`&&(M?B():D||V())};return document.addEventListener(`keydown`,t),()=>document.removeEventListener(`keydown`,t)},[e,V,c,D,M,B]);let H=(0,p.useMemo)(()=>{let e=x.trim().toLowerCase();return e?_.filter(t=>[t.name,t.displayName,t.description,t.source,t.compatibility,t.allowedTools,...t.tags??[],...t.triggers??[]].filter(Boolean).join(` `).toLowerCase().includes(e)):_},[x,_]),U=async e=>{N(e),F(null),z(``),L(!0);try{let t=new URLSearchParams({name:e});j&&a?(t.set(`scope`,`project`),t.set(`projectId`,a.id)):t.set(`scope`,`global`),F(await h(await fetch(`/api/skills/content?${t}`)))}catch(e){z(e instanceof Error?e.message:d(`failedToReadSkill`))}finally{L(!1)}};if(!e||j&&!a)return null;let W=e=>{if(D)return;let t=new Set(y);t.has(e)?t.delete(e):t.add(e),b(t),G(t)},G=async e=>{O(!0),A(``);try{let n=await h(await fetch(j?`/api/skills/project`:`/api/skills/global`,{method:`PUT`,headers:{"content-type":`application/json`},body:JSON.stringify(j?{projectId:a.id,selectedSkills:[...e]}:{selectedSkills:[...e]})}));g({scope:t,project:j?n.projects?.find(e=>e.id===a.id)??{...a,skills:n.selectedSkills}:void 0,projects:n.projects,selectedSkills:n.selectedSkills})}catch(e){b(y),A(e instanceof Error?e.message:d(`failedToSaveSkills`))}finally{O(!1)}},K=d(j?`projectSkills`:`globalSkills`),q=j?d(`projectSkillsDescription`,{project:a.name}):d(`globalSkillsDescription`),J=(0,m.jsxs)(m.Fragment,{children:[(0,m.jsx)(`div`,{className:`quickforge-settings-divider p-3`,children:(0,m.jsxs)(`div`,{className:`quickforge-settings-inline-field`,children:[(0,m.jsx)(o,{className:`pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground/60`}),(0,m.jsx)(`input`,{value:x,onChange:e=>S(e.target.value),placeholder:d(`searchSkills`),className:`quickforge-settings-input`,disabled:T||D})]})}),k?(0,m.jsx)(`div`,{className:`quickforge-settings-alert quickforge-settings-warning-attached`,children:k}):null,(0,m.jsxs)(`div`,{className:`quickforge-settings-toolbar`,children:[(0,m.jsx)(`span`,{className:`quickforge-settings-row-title`,children:d(`availableSkills`)}),(0,m.jsx)(`span`,{className:`quickforge-settings-badge quickforge-settings-badge-info`,children:d(`selectedSkillsCount`,{count:y.size})})]}),(0,m.jsx)(`div`,{className:c?``:`max-h-[46vh] overflow-y-auto`,children:T?(0,m.jsxs)(`div`,{className:`quickforge-settings-empty-row inline-flex items-center gap-2`,children:[(0,m.jsx)(n,{className:`size-4 animate-spin`}),d(`loading`)]}):H.length===0?(0,m.jsx)(`div`,{className:`quickforge-settings-empty-row`,children:d(`noMatchingSkills`)}):H.map(e=>{let t=y.has(e.name);return(0,m.jsxs)(`div`,{className:u(`quickforge-settings-list-item`,t&&`bg-muted/12`),children:[(0,m.jsxs)(`div`,{className:`quickforge-settings-list-item-main`,children:[(0,m.jsx)(`div`,{className:`quickforge-settings-row-title`,children:e.displayName||e.name}),e.description?(0,m.jsx)(`div`,{className:`quickforge-settings-row-description`,children:e.description}):null,(0,m.jsxs)(`div`,{className:`quickforge-settings-meta`,children:[e.source?(0,m.jsx)(`span`,{className:`quickforge-settings-badge quickforge-settings-badge-muted`,children:e.source}):null,e.compatibility?(0,m.jsx)(`span`,{className:`quickforge-settings-badge quickforge-settings-badge-muted`,children:e.compatibility}):null,e.allowedTools?(0,m.jsx)(`span`,{className:`quickforge-settings-badge quickforge-settings-badge-muted`,children:e.allowedTools}):null,e.tags?.slice(0,5).map(e=>(0,m.jsx)(`span`,{className:`quickforge-settings-badge quickforge-settings-badge-muted`,children:e},e))]})]}),(0,m.jsxs)(`div`,{className:`quickforge-settings-list-item-actions`,onClick:e=>e.stopPropagation(),children:[(0,m.jsxs)(`label`,{className:`quickforge-settings-switch`,"aria-disabled":D?`true`:`false`,children:[(0,m.jsx)(`input`,{type:`checkbox`,checked:t,disabled:D,onChange:()=>W(e.name),"aria-label":t?`Disable ${e.name}`:`Enable ${e.name}`}),(0,m.jsx)(`span`,{"aria-hidden":`true`})]}),(0,m.jsx)(`button`,{type:`button`,className:`quickforge-settings-icon-action`,onClick:()=>void U(e.name),disabled:D||I,title:d(`readSkill`),"aria-label":`${d(`readSkill`)}: ${e.displayName||e.name}`,children:(0,m.jsx)(r,{className:`size-4`})})]})]},e.name)})})]}),Y=(0,m.jsxs)(m.Fragment,{children:[(0,m.jsxs)(`div`,{className:`quickforge-settings-toolbar`,children:[(0,m.jsxs)(`button`,{type:`button`,className:`quickforge-settings-button quickforge-settings-button-secondary`,onClick:B,disabled:I,children:[(0,m.jsx)(i,{className:`mr-2 size-4`}),d(`backToSkillList`)]}),(0,m.jsx)(`div`,{className:`quickforge-settings-row-main min-w-0`,children:(0,m.jsx)(`div`,{className:`quickforge-settings-row-title truncate`,children:P?.displayName||P?.name||M})})]}),(0,m.jsx)(`div`,{className:`p-4`,children:I?(0,m.jsxs)(`div`,{className:`quickforge-settings-empty-row inline-flex items-center gap-2`,children:[(0,m.jsx)(n,{className:`size-4 animate-spin`}),d(`loading`)]}):R?(0,m.jsx)(`div`,{className:`quickforge-settings-alert quickforge-settings-warning-attached`,children:R}):P?(0,m.jsxs)(m.Fragment,{children:[P.description?(0,m.jsx)(`div`,{className:`quickforge-settings-row-description mb-3`,children:P.description}):null,(0,m.jsxs)(`div`,{className:`quickforge-settings-meta mb-3`,children:[P.version?(0,m.jsxs)(`span`,{className:`quickforge-settings-badge quickforge-settings-badge-muted`,children:[`v`,P.version]}):null,P.source?(0,m.jsx)(`span`,{className:`quickforge-settings-badge quickforge-settings-badge-muted`,children:P.source}):null,P.compatibility?(0,m.jsx)(`span`,{className:`quickforge-settings-badge quickforge-settings-badge-muted`,children:P.compatibility}):null,P.allowedTools?(0,m.jsx)(`span`,{className:`quickforge-settings-badge quickforge-settings-badge-muted`,children:P.allowedTools}):null,P.license?(0,m.jsx)(`span`,{className:`quickforge-settings-badge quickforge-settings-badge-muted`,children:P.license}):null,P.tags?.map(e=>(0,m.jsx)(`span`,{className:`quickforge-settings-badge quickforge-settings-badge-muted`,children:e},e))]}),P.triggers?.length?(0,m.jsxs)(`div`,{className:`quickforge-settings-row-description mb-3`,children:[`Triggers: `,P.triggers.join(`, `)]}):null,P.instructions?(0,m.jsx)(`pre`,{className:`whitespace-pre-wrap break-words rounded-md border border-border bg-muted/30 px-4 py-3 font-mono text-sm leading-6 text-foreground/80`,children:P.instructions}):(0,m.jsx)(`div`,{className:`quickforge-settings-empty-row`,children:d(`noSkillContent`)})]}):null})]});return c?(0,m.jsx)(`div`,{className:`quickforge-settings-stack`,children:(0,m.jsx)(`section`,{className:`quickforge-settings-section`,"aria-label":K,children:M?Y:(0,m.jsxs)(m.Fragment,{children:[C.length?(0,m.jsx)(`div`,{className:`px-5 pt-4`,children:(0,m.jsxs)(`span`,{className:`quickforge-settings-row-description break-all`,children:[d(`skillSearchPaths`),`: `,C.join(` · `)]})}):null,J]})})}):(0,m.jsx)(`div`,{className:`fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4`,onClick:e=>{e.target===e.currentTarget&&!D&&V()},children:(0,m.jsxs)(`div`,{className:`flex max-h-[85vh] w-full max-w-2xl flex-col rounded-lg border border-border bg-background shadow-quickforge`,children:[(0,m.jsxs)(`div`,{className:`quickforge-settings-toolbar`,children:[(0,m.jsxs)(`div`,{className:`quickforge-settings-row-main`,children:[(0,m.jsxs)(`div`,{className:`quickforge-settings-row-title`,children:[K,(0,m.jsx)(l,{label:q})]}),(0,m.jsx)(`div`,{className:`quickforge-settings-row-description`,children:q}),C.length?(0,m.jsxs)(`div`,{className:`quickforge-settings-row-description break-all`,children:[d(`skillSearchPaths`),`: `,C.join(` · `)]}):null]}),(0,m.jsx)(`button`,{type:`button`,className:`quickforge-settings-icon-action`,onClick:V,disabled:D,"aria-label":d(`close`),children:(0,m.jsx)(s,{className:`size-4`})})]}),(0,m.jsx)(`div`,{className:`min-h-0 flex-1 overflow-y-auto`,children:M?Y:J})]})})}function _({open:e,scope:t,project:n,onOpenChange:r,onSaved:i}){return(0,m.jsx)(g,{active:e,scope:t,project:n,onClose:()=>r(!1),onSaved:i})}export{f as n,_ as t};
package/dist/index.html CHANGED
@@ -11,7 +11,7 @@
11
11
  <meta name="apple-mobile-web-app-title" content="QuickForge" />
12
12
  <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
13
13
  <title>速构 QuickForge</title>
14
- <script type="module" crossorigin src="/assets/index-DKmdePUP.js"></script>
14
+ <script type="module" crossorigin src="/assets/index-DxNzlPmc.js"></script>
15
15
  <link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-DWdDZTNf.js">
16
16
  <link rel="modulepreload" crossorigin href="/assets/pi-ai-Cr3rpCXT.js">
17
17
  <link rel="modulepreload" crossorigin href="/assets/lit-vendor-Dr3cpBGF.js">
@@ -20,9 +20,9 @@
20
20
  <link rel="modulepreload" crossorigin href="/assets/icons-C7j5jdKo.js">
21
21
  <link rel="modulepreload" crossorigin href="/assets/react-vendor-VqdnQHHS.js">
22
22
  <link rel="modulepreload" crossorigin href="/assets/logger-B65Akg8A.js">
23
- <link rel="modulepreload" crossorigin href="/assets/index-DKmdePUP.js">
24
- <link rel="modulepreload" crossorigin href="/assets/skills-dialog-DvSnPPzu.js">
25
- <link rel="stylesheet" crossorigin href="/assets/index-Bxg5fT2p.css">
23
+ <link rel="modulepreload" crossorigin href="/assets/index-DxNzlPmc.js">
24
+ <link rel="modulepreload" crossorigin href="/assets/skills-dialog-CmVLi_h6.js">
25
+ <link rel="stylesheet" crossorigin href="/assets/index-HVxfqXfB.css">
26
26
  </head>
27
27
  <body>
28
28
  <div id="root"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shawnstack/quickforge",
3
- "version": "1.6.10",
3
+ "version": "1.6.12",
4
4
  "description": "AI chat application with Agent access modes for local workspace tools. React + Vite + Tailwind CSS frontend, local Node.js storage server.",
5
5
  "keywords": [
6
6
  "ai",
@@ -5,7 +5,7 @@ import path from 'node:path'
5
5
  import { Agent } from '@earendil-works/pi-agent-core'
6
6
  import { streamSimpleWithAiHttpLogging } from './ai-http-logger.mjs'
7
7
  import { loadSkillToolContext, abortRunningCommand } from './tools/index.mjs'
8
- import { createSkillTools, workspaceTools } from './tools/definitions.mjs'
8
+ import { createSkillTools, globalMemoryTool, workspaceTools } from './tools/definitions.mjs'
9
9
  import { createMcpToolDefinitions, isMcpToolName } from './mcp/registry.mjs'
10
10
  import { createPluginToolDefinitions, isPluginToolName } from './plugins/registry.mjs'
11
11
  import {
@@ -28,6 +28,7 @@ import {
28
28
  import { projectContextFromId, defaultGlobalWorkspaceContext, readProjectConfig } from './project-config.mjs'
29
29
  import { ensureStorage, readStore, atomicUpdate, atomicSessionMetadataUpdate, readSessionValue, writeSessionValue, deleteSessionValue, tempAgentsDir } from './storage.mjs'
30
30
  import { logger } from './utils/logger.mjs'
31
+ import { getGlobalMemoryRevision, isGlobalMemoryEnabled } from './global-memory.mjs'
31
32
  import { buildSystemPrompt, generateAiTitle, generateTitle } from './session-utils.mjs'
32
33
  import { restoreReasoningContentInPayload } from './reasoning-cache.mjs'
33
34
  import {
@@ -111,6 +112,10 @@ async function createServerTools(projectId, projectContext, skillsContext, inclu
111
112
  .filter(isAllowed)
112
113
  .map((definition) => wrapToolDefinition(definition, toolContext, toolPermissions))
113
114
 
115
+ if (!allowedTools && await isGlobalMemoryEnabled()) {
116
+ tools.push(wrapToolDefinition(globalMemoryTool, toolContext, toolPermissions))
117
+ }
118
+
114
119
  if (includeWorkspaceTools && projectContext) {
115
120
  const definitions = workspaceTools.filter((definition) => includeSubagentTool || definition.name !== 'run_subagent')
116
121
  tools.push(...definitions
@@ -132,13 +137,21 @@ async function createServerTools(projectId, projectContext, skillsContext, inclu
132
137
  }
133
138
 
134
139
  async function rebuildSessionTools(session) {
140
+ const profileToolNames = Array.isArray(session.agentProfile?.allowedTools) ? session.agentProfile.allowedTools : null
135
141
  session.agent.state.tools = await createServerTools(
136
142
  session.projectId,
137
143
  session.projectContext,
138
144
  sessionSkillsContext(session),
139
145
  !!session.projectContext,
140
146
  createCommandToolPermissions(session),
141
- { parentSessionId: session.sessionId },
147
+ session.agentProfile
148
+ ? {
149
+ allowedToolNames: profileToolNames,
150
+ includeSubagentTool: false,
151
+ includeMcpTools: false,
152
+ parentSessionId: session.sessionId,
153
+ }
154
+ : { parentSessionId: session.sessionId },
142
155
  )
143
156
  }
144
157
 
@@ -1314,6 +1327,28 @@ export function touchSession(sessionId) {
1314
1327
  return false
1315
1328
  }
1316
1329
 
1330
+ function agentProfileSystemPrompt(agentProfile) {
1331
+ return agentProfile?.systemPrompt
1332
+ ? `\n\n<agent_profile_instructions>\nAgent Profile: ${agentProfile.label || agentProfile.name}\n${agentProfile.systemPrompt}\n</agent_profile_instructions>`
1333
+ : ''
1334
+ }
1335
+
1336
+ async function resolvedSessionSystemPrompt(projectId, agentProfile) {
1337
+ return `${await buildSystemPrompt(projectId)}${agentProfileSystemPrompt(agentProfile)}`
1338
+ }
1339
+
1340
+ async function refreshMemoryState(session) {
1341
+ const memoryEnabled = await isGlobalMemoryEnabled()
1342
+ const memoryRevision = memoryEnabled ? await getGlobalMemoryRevision() : null
1343
+ if (session.memoryEnabled === memoryEnabled && session.memoryRevision === memoryRevision) return
1344
+ session.memoryEnabled = memoryEnabled
1345
+ session.memoryRevision = memoryRevision
1346
+ if (session.managedSystemPrompt !== false) {
1347
+ session.agent.state.systemPrompt = await resolvedSessionSystemPrompt(session.projectId, session.agentProfile)
1348
+ }
1349
+ await rebuildSessionTools(session)
1350
+ }
1351
+
1317
1352
  /**
1318
1353
  * Create or retrieve an Agent for a session.
1319
1354
  * If the session already has a running agent, return it.
@@ -1368,7 +1403,7 @@ export async function createAgent(sessionId, config = {}) {
1368
1403
  globalSkillNames: projectConfig.globalSkills,
1369
1404
  projectSkillNames: configuredProject?.skills,
1370
1405
  }
1371
- const profileSystemPrompt = agentProfile?.systemPrompt ? `\n\n<agent_profile_instructions>\nAgent Profile: ${agentProfile.label || agentProfile.name}\n${agentProfile.systemPrompt}\n</agent_profile_instructions>` : ''
1406
+ const profileSystemPrompt = agentProfileSystemPrompt(agentProfile)
1372
1407
  const resolvedSystemPrompt = systemPrompt ?? `${await buildSystemPrompt(projectId)}${profileSystemPrompt}`
1373
1408
 
1374
1409
  let resolvedAgentProfile = agentProfile
@@ -1429,6 +1464,8 @@ export async function createAgent(sessionId, config = {}) {
1429
1464
  }
1430
1465
  }
1431
1466
 
1467
+ const initialMemoryEnabled = await isGlobalMemoryEnabled()
1468
+ const initialMemoryRevision = initialMemoryEnabled ? await getGlobalMemoryRevision() : null
1432
1469
  let session
1433
1470
  const agent = new Agent({
1434
1471
  initialState: {
@@ -1455,6 +1492,7 @@ export async function createAgent(sessionId, config = {}) {
1455
1492
  const isSkillTool = toolName === 'activate_skill' || toolName === 'read_skill_resource'
1456
1493
  if (isSkillTool) return undefined
1457
1494
  if (profileToolNames && !profileToolNames.includes(toolName)) return { block: true, reason: `Agent profile ${agentProfile.name} is not allowed to use ${toolName}.` }
1495
+ if (toolName === 'manage_global_memory') return undefined
1458
1496
  if (toolName === 'run_subagent') {
1459
1497
  const requested = context.args?.subagent
1460
1498
  const policy = requested && typeof requested === 'object' ? normalizeCapabilityPolicy(requested.capabilityPolicy || 'readonly-research', Array.isArray(requested.tools) ? requested.tools : []) : null
@@ -1517,6 +1555,9 @@ export async function createAgent(sessionId, config = {}) {
1517
1555
  stateVersion: 0,
1518
1556
  lastAutoCompactAt: null,
1519
1557
  lastAutoCompactRejected: null,
1558
+ memoryEnabled: initialMemoryEnabled,
1559
+ memoryRevision: initialMemoryRevision,
1560
+ managedSystemPrompt: systemPrompt == null,
1520
1561
  /** Track active SSE connections. Only one SSE stream allowed per session to prevent
1521
1562
  * connection-pool exhaustion when two browser tabs load the same session. */
1522
1563
  sseConnected: false,
@@ -1864,6 +1905,7 @@ export async function runPrompt(sessionId, message, selectedCapabilities = [], p
1864
1905
  throw Object.assign(new Error('Generation is still running. Stop it or wait until it finishes.'), { statusCode: 409 })
1865
1906
  }
1866
1907
 
1908
+ await refreshMemoryState(session)
1867
1909
  resetIdleTimer(session)
1868
1910
 
1869
1911
  // Build user message
@@ -22,6 +22,7 @@ export const commandRestrictedTools = new Set([
22
22
  'edit_file',
23
23
  'run_command',
24
24
  'run_subagent',
25
+ 'manage_global_memory',
25
26
  ])
26
27
 
27
28
  export const planAllowedTools = new Set([
@@ -99,6 +100,9 @@ export function commandToolPermissionError(session, toolName) {
99
100
  if ((toolName === 'write_file' || toolName === 'edit_file') && permissions.allowEdit === false) {
100
101
  return `Command /${session.activeCommandName} does not allow editing files.`
101
102
  }
103
+ if (toolName === 'manage_global_memory' && permissions.allowEdit === false) {
104
+ return `Command /${session.activeCommandName} does not allow editing global memory.`
105
+ }
102
106
  return null
103
107
  }
104
108
 
@@ -0,0 +1,168 @@
1
+ import { createHash, randomUUID } from 'node:crypto'
2
+ import { promises as fs } from 'node:fs'
3
+ import path from 'node:path'
4
+ import { dataDir, readStore } from './storage.mjs'
5
+
6
+ export const GLOBAL_MEMORY_SETTINGS_KEY = 'memory-settings'
7
+ export const GLOBAL_MEMORY_FILE_NAME = 'MEMORY.md'
8
+ export const DEFAULT_GLOBAL_MEMORY_SETTINGS = { enabled: true }
9
+
10
+ const MAX_MEMORY_FILE_BYTES = 16 * 1024
11
+
12
+ let memoryWriteQueue = Promise.resolve()
13
+
14
+ export function normalizeGlobalMemorySettings(value) {
15
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return { ...DEFAULT_GLOBAL_MEMORY_SETTINGS }
16
+ return { enabled: value.enabled !== false }
17
+ }
18
+
19
+ export async function readGlobalMemorySettings() {
20
+ const settings = await readStore('settings')
21
+ return normalizeGlobalMemorySettings(settings?.[GLOBAL_MEMORY_SETTINGS_KEY])
22
+ }
23
+
24
+ export async function isGlobalMemoryEnabled() {
25
+ return (await readGlobalMemorySettings()).enabled
26
+ }
27
+
28
+ export function globalMemoryFilePath(baseDir = dataDir) {
29
+ return path.join(baseDir, GLOBAL_MEMORY_FILE_NAME)
30
+ }
31
+
32
+ function requestError(message, statusCode = 400) {
33
+ const error = new Error(message)
34
+ error.statusCode = statusCode
35
+ return error
36
+ }
37
+
38
+ export function containsSensitiveMemory(value) {
39
+ const text = String(value || '')
40
+ const patterns = [
41
+ /-----BEGIN [A-Z ]*PRIVATE KEY-----/i,
42
+ /\b(?:sk-[A-Za-z0-9_-]{16,}|gh[pousr]_[A-Za-z0-9]{20,}|AKIA[A-Z0-9]{16})\b/,
43
+ /\bauthorization\s*:\s*bearer\s+\S+/i,
44
+ /\bcookie\s*:\s*\S+/i,
45
+ /\b(?:api[ _-]?key|access[ _-]?token|refresh[ _-]?token|password|passwd|secret)\s*(?:is|是|为|:|=)\s*\S{8,}/i,
46
+ /(?:^|\n)\s*[A-Z][A-Z0-9_]{2,}\s*=\s*[^\s]{8,}(?:\n|$)/,
47
+ ]
48
+ return patterns.some((pattern) => pattern.test(text))
49
+ }
50
+
51
+ function validateGlobalMemoryMarkdown(markdown) {
52
+ if (typeof markdown !== 'string') throw requestError('Memory Markdown is required.')
53
+ if (Buffer.byteLength(markdown, 'utf8') > MAX_MEMORY_FILE_BYTES) throw requestError('Global memory exceeds the size limit.')
54
+ if (containsSensitiveMemory(markdown)) throw requestError('Sensitive credentials or secrets cannot be stored in global memory.')
55
+ return markdown
56
+ }
57
+
58
+ export async function getGlobalMemoryRevision(options = {}) {
59
+ const file = globalMemoryFilePath(options.dataDir)
60
+ try {
61
+ const content = await fs.readFile(file)
62
+ return createHash('sha256').update(content).digest('hex')
63
+ } catch (error) {
64
+ if (error?.code === 'ENOENT') return null
65
+ throw error
66
+ }
67
+ }
68
+
69
+ export async function readGlobalMemory(options = {}) {
70
+ const file = globalMemoryFilePath(options.dataDir)
71
+ try {
72
+ const markdown = await fs.readFile(file, 'utf8')
73
+ return { file, markdown }
74
+ } catch (error) {
75
+ if (error?.code === 'ENOENT') return { file, markdown: '' }
76
+ throw error
77
+ }
78
+ }
79
+
80
+ export async function readGlobalMemoryDocument(options = {}) {
81
+ const memory = await readGlobalMemory(options)
82
+ return {
83
+ enabled: options.enabled === undefined ? await isGlobalMemoryEnabled() : options.enabled,
84
+ markdown: memory.markdown,
85
+ path: '~/.quickforge/MEMORY.md',
86
+ }
87
+ }
88
+
89
+ async function writeGlobalMemoryMarkdown(markdown, options = {}) {
90
+ const file = globalMemoryFilePath(options.dataDir)
91
+ await fs.mkdir(path.dirname(file), { recursive: true })
92
+ const temporary = `${file}.${process.pid}.${randomUUID()}.tmp`
93
+ await fs.writeFile(temporary, markdown, 'utf8')
94
+ await fs.rename(temporary, file)
95
+ return file
96
+ }
97
+
98
+ function withMemoryWriteLock(operation) {
99
+ const result = memoryWriteQueue.then(operation, operation)
100
+ memoryWriteQueue = result.catch(() => undefined)
101
+ return result
102
+ }
103
+
104
+ async function assertMemoryEnabled(options) {
105
+ const enabled = options.enabled === undefined ? await isGlobalMemoryEnabled() : options.enabled
106
+ if (!enabled) throw requestError('Global memory is disabled in Settings.', 403)
107
+ }
108
+
109
+ export async function saveGlobalMemoryDocument(markdown, options = {}) {
110
+ await assertMemoryEnabled(options)
111
+ const validated = validateGlobalMemoryMarkdown(markdown)
112
+ return withMemoryWriteLock(async () => {
113
+ const file = await writeGlobalMemoryMarkdown(validated, options)
114
+ return {
115
+ enabled: true,
116
+ markdown: validated,
117
+ path: '~/.quickforge/MEMORY.md',
118
+ file,
119
+ chars: validated.length,
120
+ }
121
+ })
122
+ }
123
+
124
+ export async function readGlobalMemoryPrompt(options = {}) {
125
+ if (options.enabled === false) return null
126
+ if (options.enabled === undefined && !(await isGlobalMemoryEnabled())) return null
127
+ const memory = await readGlobalMemory(options)
128
+ return {
129
+ source: '~/.quickforge/MEMORY.md',
130
+ content: memory.markdown.trim() || null,
131
+ enabled: true,
132
+ }
133
+ }
134
+
135
+ export async function manageGlobalMemory(params = {}, options = {}) {
136
+ await assertMemoryEnabled(options)
137
+ const action = String(params.action || '').trim().toLowerCase()
138
+
139
+ if (action === 'read') {
140
+ const memory = await readGlobalMemory(options)
141
+ return {
142
+ content: memory.markdown || 'Global user memory is empty.',
143
+ details: {
144
+ action,
145
+ status: memory.markdown ? 'loaded' : 'empty',
146
+ markdown: memory.markdown,
147
+ chars: memory.markdown.length,
148
+ path: '~/.quickforge/MEMORY.md',
149
+ },
150
+ }
151
+ }
152
+
153
+ if (action === 'write') {
154
+ const saved = await saveGlobalMemoryDocument(params.markdown, options)
155
+ return {
156
+ content: 'Global user memory saved.',
157
+ details: {
158
+ action,
159
+ status: 'saved',
160
+ markdown: saved.markdown,
161
+ chars: saved.chars,
162
+ path: saved.path,
163
+ },
164
+ }
165
+ }
166
+
167
+ throw requestError('Memory action must be read or write.')
168
+ }
package/server/index.mjs CHANGED
@@ -14,6 +14,7 @@ import { handleProjectApi } from './routes/project.mjs'
14
14
  import { handleFilesystemApi, setActiveWorkspaceRootForFilesystem } from './routes/filesystem.mjs'
15
15
  import { handleToolApi, handleGetTools } from './routes/tools.mjs'
16
16
  import { handleInstructionsApi } from './routes/instructions.mjs'
17
+ import { handleMemoryApi } from './routes/memory.mjs'
17
18
  import { handleSkillsApi } from './routes/skills.mjs'
18
19
  import { handleAgentApi } from './routes/agent.mjs'
19
20
  import { handleAgentProfilesApi } from './routes/agent-profiles.mjs'
@@ -312,6 +313,12 @@ async function handleApi(req, res, url) {
312
313
  return
313
314
  }
314
315
 
316
+ // Global user memory
317
+ if (pathname === '/api/memory') {
318
+ await handleMemoryApi(req, res)
319
+ return
320
+ }
321
+
315
322
  // Agent profiles
316
323
  if (pathname === '/api/agent-profiles' || pathname.startsWith('/api/agent-profiles/')) {
317
324
  await handleAgentProfilesApi(req, res, url)
@@ -6,6 +6,7 @@ import { existsSync, promises as fs } from 'node:fs'
6
6
  import { ensureProjectCache, readProjectConfigData, atomicProjectConfigUpdate, dataDir, readStore, atomicUpdate } from './storage.mjs'
7
7
  import { setWorkspaceRoot, assertDirectory } from './utils/workspace.mjs'
8
8
  import { loadSelectedGlobalSkills, loadSelectedProjectSkills, mergeSkills } from './skills.mjs'
9
+ import { readGlobalMemoryPrompt } from './global-memory.mjs'
9
10
 
10
11
  let defaultWorkspaceRoot = ''
11
12
 
@@ -403,6 +404,7 @@ export async function buildInstructionsPayload(projectId) {
403
404
  ? await loadSelectedProjectSkills(project.skills, project.path)
404
405
  : []
405
406
  const activeSkills = mergeSkills(globalSkills, projectSkills)
407
+ const globalMemory = await readGlobalMemoryPrompt()
406
408
  const stripRuntimeFields = ({ rootDir: _rootDir, instructions: _instructions, location: _location, ...skill }) => skill
407
409
 
408
410
  return {
@@ -417,6 +419,7 @@ export async function buildInstructionsPayload(projectId) {
417
419
  project: projectInstructions,
418
420
  globalSources: globalInstructionSources,
419
421
  projectSources: projectInstructionSources,
422
+ globalMemory,
420
423
  globalSkills: globalSkills.map(stripRuntimeFields),
421
424
  projectSkills: projectSkills.map(stripRuntimeFields),
422
425
  skills: activeSkills.map(stripRuntimeFields),
@@ -0,0 +1,26 @@
1
+ import { readJsonBody, sendJson } from '../utils/response.mjs'
2
+ import { readGlobalMemoryDocument, saveGlobalMemoryDocument } from '../global-memory.mjs'
3
+
4
+ const MAX_MEMORY_REQUEST_BYTES = 20 * 1024
5
+
6
+ export async function handleMemoryApi(req, res, options = {}) {
7
+ if (req.method === 'GET') {
8
+ sendJson(res, 200, await readGlobalMemoryDocument(options))
9
+ return
10
+ }
11
+
12
+ if (req.method === 'PUT') {
13
+ const body = await readJsonBody(req, MAX_MEMORY_REQUEST_BYTES)
14
+ if (!body || typeof body.markdown !== 'string') {
15
+ const error = new Error('Memory Markdown is required.')
16
+ error.statusCode = 400
17
+ throw error
18
+ }
19
+ sendJson(res, 200, await saveGlobalMemoryDocument(body.markdown, options))
20
+ return
21
+ }
22
+
23
+ const error = new Error('Method not allowed')
24
+ error.statusCode = 405
25
+ throw error
26
+ }
@@ -99,12 +99,7 @@ export async function handleProjectApi(req, res, url) {
99
99
 
100
100
  if (req.method === 'POST' && url.pathname.startsWith('/api/project/') && url.pathname.endsWith('/open-in-explorer')) {
101
101
  const id = decodeSegment(url.pathname.split('/').filter(Boolean)[2])
102
- const selected = config.projects.find((project) => project.id === id)
103
- if (!selected) {
104
- const error = new Error('Unknown project')
105
- error.statusCode = 404
106
- throw error
107
- }
102
+ const selected = resolveRequestedProject(config, id, getDefaultWorkspaceRoot())
108
103
  await openPathInFileManager(selected.path)
109
104
  sendJson(res, 200, { ok: true })
110
105
  return
@@ -112,12 +107,7 @@ export async function handleProjectApi(req, res, url) {
112
107
 
113
108
  if (req.method === 'POST' && url.pathname.startsWith('/api/project/') && url.pathname.endsWith('/open-in-vscode')) {
114
109
  const id = decodeSegment(url.pathname.split('/').filter(Boolean)[2])
115
- const selected = config.projects.find((project) => project.id === id)
116
- if (!selected) {
117
- const error = new Error('Unknown project')
118
- error.statusCode = 404
119
- throw error
120
- }
110
+ const selected = resolveRequestedProject(config, id, getDefaultWorkspaceRoot())
121
111
  await openPathInVSCode(selected.path)
122
112
  sendJson(res, 200, { ok: true })
123
113
  return
@@ -125,12 +115,7 @@ export async function handleProjectApi(req, res, url) {
125
115
 
126
116
  if (req.method === 'POST' && url.pathname.startsWith('/api/project/') && url.pathname.endsWith('/open-in-idea')) {
127
117
  const id = decodeSegment(url.pathname.split('/').filter(Boolean)[2])
128
- const selected = config.projects.find((project) => project.id === id)
129
- if (!selected) {
130
- const error = new Error('Unknown project')
131
- error.statusCode = 404
132
- throw error
133
- }
118
+ const selected = resolveRequestedProject(config, id, getDefaultWorkspaceRoot())
134
119
  await openPathInIDEA(selected.path)
135
120
  sendJson(res, 200, { ok: true })
136
121
  return
@@ -1,13 +1,14 @@
1
1
  import { sendJson, readJsonBody, decodeSegment } from '../utils/response.mjs'
2
2
  import { readStore } from '../storage.mjs'
3
3
  import { toolHandlers, loadSkillToolContext } from '../tools/index.mjs'
4
- import { createSkillTools, workspaceTools } from '../tools/definitions.mjs'
4
+ import { createSkillTools, globalMemoryTool, workspaceTools } from '../tools/definitions.mjs'
5
+ import { isGlobalMemoryEnabled } from '../global-memory.mjs'
5
6
  import { createMcpToolDefinitions } from '../mcp/registry.mjs'
6
7
  import { callPluginTool, createPluginToolDefinitions, isPluginToolName } from '../plugins/registry.mjs'
7
8
  import { safeReadTools } from '../approval-store.mjs'
8
9
  import { projectContextFromId, readProjectConfig } from '../project-config.mjs'
9
10
 
10
- const directRouteDisabledTools = new Set(['run_subagent'])
11
+ const directRouteDisabledTools = new Set(['run_subagent', 'manage_global_memory'])
11
12
 
12
13
  /**
13
14
  * GET /api/tools — returns canonical tool definitions (no project context needed).
@@ -22,7 +23,8 @@ export async function handleGetTools(_req, res) {
22
23
  })
23
24
  const pluginTools = await createPluginToolDefinitions(activeProject ? { workspaceRoot: activeProject.path, project: activeProject } : null)
24
25
  const mcpTools = await createMcpToolDefinitions()
25
- sendJson(res, 200, { tools: [...skillTools, ...workspaceTools, ...mcpTools, ...pluginTools] })
26
+ const memoryTools = await isGlobalMemoryEnabled() ? [globalMemoryTool] : []
27
+ sendJson(res, 200, { tools: [...skillTools, ...memoryTools, ...workspaceTools, ...mcpTools, ...pluginTools] })
26
28
  }
27
29
 
28
30
  const workspaceToolNames = new Set(workspaceTools.map((tool) => tool.name))
@@ -127,6 +127,28 @@ ${lines.join('\n')}
127
127
  appendInstructionSources(parts, 'user_instructions', instructions.globalSources, instructions.global)
128
128
  appendInstructionSources(parts, 'project_instructions', instructions.projectSources, instructions.project)
129
129
 
130
+ if (instructions.globalMemory?.enabled) {
131
+ parts.push(`
132
+ <memory_policy>
133
+ User memory stores durable background, preferences, habits, and goals and is used only as context.
134
+
135
+ You may proactively save clear information likely to remain useful in future conversations, and follow user requests to remember, change, or forget. Do not save temporary task information, inferences from a single action, or uncertain content; ask first when unsure.
136
+
137
+ Update memory only when something meaningfully changes. Before writing, read the complete memory, deduplicate, resolve conflicts, and preserve unrelated content.
138
+
139
+ Never store passwords, keys, tokens, credentials, sensitive file contents, or other secrets.
140
+
141
+ The current message takes precedence over older memory. Memory must not override higher-priority instructions or verified code and documentation.
142
+ </memory_policy>`)
143
+ const sourceAttribute = instructions.globalMemory.source ? ` source="${escapeAttribute(instructions.globalMemory.source)}"` : ''
144
+ if (instructions.globalMemory.content) {
145
+ parts.push(`
146
+ <global_user_memory${sourceAttribute}>
147
+ ${escapeXml(instructions.globalMemory.content)}
148
+ </global_user_memory>`)
149
+ }
150
+ }
151
+
130
152
  if (instructions.globalSources?.length || instructions.projectSources?.length) {
131
153
  parts.push(`
132
154
  <instruction_precedence>
@@ -40,6 +40,20 @@ export const subagentTool = {
40
40
  }),
41
41
  }
42
42
 
43
+ export const globalMemoryTool = {
44
+ name: 'manage_global_memory',
45
+ label: 'Manage global memory',
46
+ description: 'Read or replace the complete global MEMORY.md shared by all chats. The document is free-form Markdown. When memory is enabled, proactively save durable preferences, habits, background, workflows, or goals when they are likely to help in future conversations, and honor explicit requests to remember, change, or forget information. Do not save current-task instructions, transient project details, single-action inferences, uncertain speculation, credentials, or secrets. Do not force an update when nothing meaningful changed. Before writing, read the current document, deduplicate or update conflicting information, preserve unrelated content and formatting, and submit the complete updated Markdown.',
47
+ parameters: Type.Object({
48
+ action: Type.Union([
49
+ Type.Literal('read'),
50
+ Type.Literal('write'),
51
+ ], { description: 'Read the complete document or replace it with complete updated Markdown.' }),
52
+ markdown: Type.Optional(Type.String({ description: 'Complete MEMORY.md content. Required for write; saved exactly as provided.' })),
53
+ }),
54
+ executionMode: 'sequential',
55
+ }
56
+
43
57
  export const workspaceTools = [
44
58
  subagentTool,
45
59
  {
@@ -14,6 +14,7 @@ import {
14
14
  readSkillResource,
15
15
  } from '../skills.mjs'
16
16
  import { getToolWorkspaceRoot } from '../utils/workspace.mjs'
17
+ import { manageGlobalMemory } from '../global-memory.mjs'
17
18
 
18
19
  const require = createRequire(import.meta.url)
19
20
 
@@ -1088,7 +1089,12 @@ export async function toolRunCommand(params, context, runtime = {}) {
1088
1089
  })
1089
1090
  }
1090
1091
 
1092
+ export async function toolManageGlobalMemory(params) {
1093
+ return manageGlobalMemory(params)
1094
+ }
1095
+
1091
1096
  export const toolHandlers = {
1097
+ manage_global_memory: toolManageGlobalMemory,
1092
1098
  read_file: toolReadFile,
1093
1099
  grep_files: toolGrepFiles,
1094
1100
  write_file: toolWriteFile,
@@ -131,6 +131,22 @@ async function findExistingFile(candidates) {
131
131
  return undefined
132
132
  }
133
133
 
134
+ export function createExternalAppEnv(source = process.env) {
135
+ const env = { ...source }
136
+ delete env.ELECTRON_RUN_AS_NODE
137
+ delete env.ELECTRON_NO_ATTACH_CONSOLE
138
+ delete env.ATOM_SHELL_INTERNAL_RUN_AS_NODE
139
+ return env
140
+ }
141
+
142
+ export function createVSCodeOpenArgs(targetPath, platform = process.platform) {
143
+ return platform === 'win32' ? ['--reuse-window', targetPath] : [targetPath]
144
+ }
145
+
146
+ export function shouldHideVSCodeLauncherWindow(command, platform = process.platform) {
147
+ return platform === 'win32' && path.win32.basename(command).toLowerCase() === 'cmd.exe'
148
+ }
149
+
134
150
  async function findIntelliJIdeaExecutable() {
135
151
  const roots = [
136
152
  process.env.LOCALAPPDATA ? path.join(process.env.LOCALAPPDATA, 'Programs') : undefined,
@@ -206,10 +222,10 @@ export async function openPathInVSCode(targetPath) {
206
222
  const codeExecutable = await findExistingFile(candidates)
207
223
  if (codeExecutable) {
208
224
  command = codeExecutable
209
- args = [resolved]
225
+ args = createVSCodeOpenArgs(resolved)
210
226
  } else {
211
227
  command = 'cmd.exe'
212
- args = ['/d', '/s', '/c', 'start', '""', '/b', 'code', resolved]
228
+ args = ['/d', '/s', '/c', 'start', '""', '/b', 'code', '--reuse-window', resolved]
213
229
  }
214
230
  }
215
231
 
@@ -217,8 +233,9 @@ export async function openPathInVSCode(targetPath) {
217
233
  const child = spawn(command, args, {
218
234
  detached: true,
219
235
  stdio: 'ignore',
220
- windowsHide: true,
236
+ windowsHide: shouldHideVSCodeLauncherWindow(command),
221
237
  shell: false,
238
+ env: createExternalAppEnv(),
222
239
  })
223
240
  child.once('error', (error) => {
224
241
  error.statusCode = 500