wyrm-mcp 7.3.2 → 7.3.3

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 (184) hide show
  1. package/README.md +8 -14
  2. package/dist/activation.js +59 -1
  3. package/dist/agent-daemon.js +281 -4
  4. package/dist/agent-loop.js +332 -7
  5. package/dist/analytics.js +236 -13
  6. package/dist/attribution.js +49 -1
  7. package/dist/audit.js +457 -2
  8. package/dist/auto-capture.js +138 -3
  9. package/dist/auto-orchestrator.js +325 -1
  10. package/dist/autoconfig.d.ts +50 -0
  11. package/dist/autoconfig.d.ts.map +1 -1
  12. package/dist/autoconfig.js +1115 -39
  13. package/dist/autoconfig.js.map +1 -1
  14. package/dist/buddy-runner.js +109 -1
  15. package/dist/buddy.js +564 -14
  16. package/dist/build-flags.js +15 -1
  17. package/dist/capabilities.js +183 -3
  18. package/dist/capture.js +56 -1
  19. package/dist/causality.js +148 -8
  20. package/dist/cli.js +281 -20
  21. package/dist/cloud/cli.js +541 -5
  22. package/dist/cloud/client.js +221 -1
  23. package/dist/cloud/crypto.js +85 -1
  24. package/dist/cloud/machine-id.js +113 -2
  25. package/dist/cloud/recovery.js +60 -1
  26. package/dist/cloud/sync-engine.js +543 -7
  27. package/dist/cloud-backup.js +579 -5
  28. package/dist/cloud-profile.js +138 -1
  29. package/dist/cloud-sync-entrypoint.js +47 -1
  30. package/dist/cloud-sync.js +309 -2
  31. package/dist/connectors/bridge-source.d.ts +46 -0
  32. package/dist/connectors/bridge-source.d.ts.map +1 -0
  33. package/dist/connectors/bridge-source.js +77 -0
  34. package/dist/connectors/bridge-source.js.map +1 -0
  35. package/dist/connectors/index.d.ts +24 -0
  36. package/dist/connectors/index.d.ts.map +1 -0
  37. package/dist/connectors/index.js +69 -0
  38. package/dist/connectors/index.js.map +1 -0
  39. package/dist/connectors/ingest.d.ts +16 -0
  40. package/dist/connectors/ingest.d.ts.map +1 -0
  41. package/dist/connectors/ingest.js +116 -0
  42. package/dist/connectors/ingest.js.map +1 -0
  43. package/dist/connectors/types.d.ts +99 -0
  44. package/dist/connectors/types.d.ts.map +1 -0
  45. package/dist/connectors/types.js +17 -0
  46. package/dist/connectors/types.js.map +1 -0
  47. package/dist/constellation.js +168 -12
  48. package/dist/content-signature.js +45 -1
  49. package/dist/context-build-budgeted.js +144 -4
  50. package/dist/context-ranking.js +69 -1
  51. package/dist/crypto.js +179 -1
  52. package/dist/daemon-write-endpoint.js +290 -1
  53. package/dist/daemon-writer.js +406 -2
  54. package/dist/database.js +1278 -53
  55. package/dist/deprecations.js +162 -2
  56. package/dist/design.js +141 -13
  57. package/dist/event-replication.js +112 -1
  58. package/dist/events-sse.js +43 -7
  59. package/dist/events.js +238 -6
  60. package/dist/failure-patterns.d.ts +107 -0
  61. package/dist/failure-patterns.d.ts.map +1 -1
  62. package/dist/failure-patterns.js +924 -43
  63. package/dist/failure-patterns.js.map +1 -1
  64. package/dist/federation.js +236 -12
  65. package/dist/goals.js +101 -13
  66. package/dist/golden.js +355 -3
  67. package/dist/handlers/agent.js +165 -4
  68. package/dist/handlers/alias-adapters.js +129 -1
  69. package/dist/handlers/aliases.js +171 -1
  70. package/dist/handlers/audit.js +87 -1
  71. package/dist/handlers/boundary.js +221 -1
  72. package/dist/handlers/capture.js +1114 -73
  73. package/dist/handlers/causality.js +119 -9
  74. package/dist/handlers/cloud.js +382 -85
  75. package/dist/handlers/companion.js +459 -28
  76. package/dist/handlers/datalake.js +187 -7
  77. package/dist/handlers/dispatch-context.js +22 -0
  78. package/dist/handlers/entity.js +256 -25
  79. package/dist/handlers/events.js +335 -16
  80. package/dist/handlers/failure.d.ts.map +1 -1
  81. package/dist/handlers/failure.js +408 -13
  82. package/dist/handlers/failure.js.map +1 -1
  83. package/dist/handlers/goals.js +296 -4
  84. package/dist/handlers/intelligence.js +681 -126
  85. package/dist/handlers/invoicing.js +70 -1
  86. package/dist/handlers/mcpclient.js +137 -6
  87. package/dist/handlers/orchestration.js +125 -40
  88. package/dist/handlers/output-schemas.js +24 -1
  89. package/dist/handlers/presence.js +99 -3
  90. package/dist/handlers/project.js +182 -28
  91. package/dist/handlers/prompts.js +157 -6
  92. package/dist/handlers/quest.js +224 -4
  93. package/dist/handlers/recall.js +237 -13
  94. package/dist/handlers/registry.js +167 -1
  95. package/dist/handlers/resources.js +288 -1
  96. package/dist/handlers/review.js +74 -11
  97. package/dist/handlers/run.js +498 -16
  98. package/dist/handlers/search.js +338 -15
  99. package/dist/handlers/session.js +643 -31
  100. package/dist/handlers/share.js +184 -8
  101. package/dist/handlers/shims.js +464 -1
  102. package/dist/handlers/skill.js +449 -67
  103. package/dist/handlers/survivors.js +120 -1
  104. package/dist/handlers/symbols.js +109 -8
  105. package/dist/handlers/syncops.js +302 -4
  106. package/dist/handlers/types.js +27 -1
  107. package/dist/harvest.js +191 -5
  108. package/dist/hours.js +156 -7
  109. package/dist/http-auth.js +321 -3
  110. package/dist/http-fast.js +1302 -22
  111. package/dist/icons.js +47 -1
  112. package/dist/importers.js +268 -1
  113. package/dist/index.js +840 -2
  114. package/dist/indexer.js +145 -4
  115. package/dist/intelligence.js +261 -31
  116. package/dist/internal-dispatch.js +212 -3
  117. package/dist/keyset.js +110 -1
  118. package/dist/knowledge-graph.js +176 -12
  119. package/dist/license.js +441 -2
  120. package/dist/logger.js +199 -2
  121. package/dist/maintenance.js +148 -2
  122. package/dist/mcp-client.js +262 -6
  123. package/dist/memory-artifacts.js +596 -32
  124. package/dist/migrate-prompt.js +124 -2
  125. package/dist/migrations.d.ts.map +1 -1
  126. package/dist/migrations.js +799 -42
  127. package/dist/migrations.js.map +1 -1
  128. package/dist/performance.js +228 -1
  129. package/dist/presence.js +140 -11
  130. package/dist/priority-embed.js +164 -5
  131. package/dist/providers/embedding-provider.js +196 -1
  132. package/dist/readonly-gate.js +29 -1
  133. package/dist/receipt.js +43 -1
  134. package/dist/rehydration.js +157 -9
  135. package/dist/reindex.js +88 -1
  136. package/dist/render-target.js +544 -21
  137. package/dist/render.js +280 -4
  138. package/dist/repl-guard.js +173 -1
  139. package/dist/replication-daemon-entrypoint.js +31 -1
  140. package/dist/replication-daemon.js +262 -2
  141. package/dist/rerank.js +142 -1
  142. package/dist/resilience.js +591 -1
  143. package/dist/reverse-bridge.js +360 -5
  144. package/dist/security.js +244 -1
  145. package/dist/session-seen.js +51 -3
  146. package/dist/setup.js +260 -1
  147. package/dist/skill-author.js +168 -5
  148. package/dist/spec-kit.js +191 -1
  149. package/dist/sqlite-busy.js +154 -1
  150. package/dist/statusline.js +315 -11
  151. package/dist/sub-agent.js +262 -13
  152. package/dist/summarizer.js +139 -13
  153. package/dist/symbols.js +283 -7
  154. package/dist/sync.js +359 -5
  155. package/dist/tasks-dispatch.js +84 -1
  156. package/dist/tasks.js +282 -1
  157. package/dist/token-budget.js +143 -1
  158. package/dist/tool-analytics.js +129 -7
  159. package/dist/tool-annotations.js +365 -1
  160. package/dist/tool-manifest-v2.json +1 -1
  161. package/dist/tool-manifest.json +1 -1
  162. package/dist/tool-profiles.js +75 -1
  163. package/dist/trace-harvest.js +244 -6
  164. package/dist/types.js +30 -1
  165. package/dist/ui-dashboard.js +50 -41
  166. package/dist/ulid.js +81 -1
  167. package/dist/usage-tracker.js +66 -1
  168. package/dist/validate.js +129 -1
  169. package/dist/vault.js +534 -1
  170. package/dist/vector-init.js +67 -1
  171. package/dist/vectors.js +184 -3
  172. package/dist/version-check.js +136 -4
  173. package/dist/visibility.js +155 -19
  174. package/dist/wyrm-cli.js +2845 -101
  175. package/dist/wyrm-cli.js.map +1 -1
  176. package/dist/wyrm-guard.d.ts.map +1 -1
  177. package/dist/wyrm-guard.js +475 -14
  178. package/dist/wyrm-guard.js.map +1 -1
  179. package/dist/wyrm-loop.js +150 -3
  180. package/dist/wyrm-manifest.json +1 -1
  181. package/dist/wyrm-statusline-daemon.js +11 -1
  182. package/dist/wyrm-statusline.js +56 -4
  183. package/dist/wyrm-ui.js +77 -9
  184. package/package.json +1 -1
@@ -1,50 +1,1126 @@
1
- import{existsSync as u,readFileSync as l,writeFileSync as f,mkdirSync as S,copyFileSync as B,chmodSync as I}from"fs";import{homedir as m,platform as U}from"os";import{join as r,dirname as w,resolve as z}from"path";import{fileURLToPath as F}from"url";import{spawnSync as b}from"child_process";function H(){const e=m(),t=U(),s=t==="darwin"?r(e,"Library","Application Support","Code","User"):t==="win32"?r(e,"AppData","Roaming","Code","User"):r(e,".config","Code","User"),o=t==="darwin"?r(e,"Library","Application Support","Code - Insiders","User"):t==="win32"?r(e,"AppData","Roaming","Code - Insiders","User"):r(e,".config","Code - Insiders","User"),c=t==="darwin"?r(e,"Library","Application Support","Claude"):t==="win32"?r(e,"AppData","Roaming","Claude"):r(e,".config","claude"),n=t==="darwin"?r(e,".cursor"):t==="win32"?r(e,".cursor"):r(e,".cursor"),a=t==="darwin"?r(e,".codeium","windsurf"):t==="win32"?r(e,".codeium","windsurf"):r(e,".codeium","windsurf"),d=t==="darwin"?r(e,".config","zed"):t==="win32"?r(e,"AppData","Roaming","Zed"):r(e,".config","zed"),i=r(e,".continue");return[{id:"vscode-copilot",name:"VS Code (Copilot)",icon:"\u{1F4BB}",configPath:r(s,"settings.json"),configFormat:"vscode",mcpKey:"mcp.servers",detected:!1,configured:!1},{id:"vscode-insiders",name:"VS Code Insiders",icon:"\u{1F7E2}",configPath:r(o,"settings.json"),configFormat:"vscode",mcpKey:"mcp.servers",detected:!1,configured:!1},{id:"claude-desktop",name:"Claude Desktop",icon:"\u{1F916}",configPath:r(c,"claude_desktop_config.json"),configFormat:"mcp-json",mcpKey:"mcpServers",detected:!1,configured:!1},{id:"cursor",name:"Cursor",icon:"\u{1F4D0}",configPath:r(n,"mcp.json"),configFormat:"mcp-json",mcpKey:"mcpServers",detected:!1,configured:!1},{id:"windsurf",name:"Windsurf",icon:"\u{1F3C4}",configPath:r(a,"mcp_config.json"),configFormat:"mcp-json",mcpKey:"mcpServers",detected:!1,configured:!1},{id:"zed",name:"Zed",icon:"\u26A1",configPath:r(d,"settings.json"),configFormat:"zed",mcpKey:"context_servers",detected:!1,configured:!1},{id:"continue",name:"Continue",icon:"\u{1F504}",configPath:r(i,"config.json"),configFormat:"mcp-json",mcpKey:"mcpServers",detected:!1,configured:!1}]}function v(){const e=H();for(const t of e){const s=w(t.configPath);if(t.detected=u(s),t.detected&&u(t.configPath))try{const o=l(t.configPath,"utf-8"),c=O(o);t.configured=q(c,t)}catch{t.configured=!1}t.detected&&(t.version=Y(t))}return e}function Y(e){try{switch(e.id){case"vscode-copilot":case"vscode-insiders":{const t=e.id==="vscode-insiders"?"code-insiders":"code",s=b(t,["--version"],{encoding:"utf-8",timeout:5e3});if(s.stdout)return s.stdout.split(`
2
- `)[0];break}case"cursor":{const t=b("cursor",["--version"],{encoding:"utf-8",timeout:5e3});if(t.stdout)return t.stdout.split(`
3
- `)[0];break}}}catch{}}function q(e,t){switch(t.configFormat){case"vscode":return e.mcp?.servers?.wyrm!==void 0;case"mcp-json":return e[t.mcpKey]?.wyrm!==void 0;case"zed":return e[t.mcpKey]?.wyrm!==void 0;default:return!1}}function R(){try{const o=b("which",["wyrm-mcp"],{encoding:"utf-8",timeout:5e3});if(o.stdout?.trim())return o.stdout.trim()}catch{}try{const c=b("npm",["root","-g"],{encoding:"utf-8",timeout:5e3}).stdout?.trim();if(c){const n=r(c,"wyrm-mcp","dist","index.js");if(u(n))return n}}catch{}const e=z(__dirname,"index.js");if(u(e))return e;const t=r(m(),".wyrm","node_modules","wyrm-mcp","dist","index.js");if(u(t))return t;const s=[r(m(),"Git Projects","Wyrm","packages","mcp-server","dist","index.js"),r(m(),"projects","Wyrm","packages","mcp-server","dist","index.js"),r(m(),"dev","Wyrm","packages","mcp-server","dist","index.js")];for(const o of s)if(u(o))return o;return"wyrm-mcp"}function E(){return r(m(),".wyrm","wyrm.db")}function V(e,t){const s=t.serverPath,o=s==="wyrm-mcp",c=s.endsWith(".js");switch(e.configFormat){case"vscode":return{command:o?"npx":"node",args:o?["wyrm-mcp"]:[s],env:{WYRM_DB_PATH:t.dbPath}};case"mcp-json":return{command:o?"npx":"node",args:o?["wyrm-mcp"]:[s],env:{WYRM_DB_PATH:t.dbPath}};case"zed":return{command:o?"npx":"node",args:o?["wyrm-mcp"]:[s],env:{WYRM_DB_PATH:t.dbPath}};default:return{}}}function L(e,t){if(!e.detected)return{client:e,action:"skipped",message:`${e.name} not detected`};try{const s=w(e.configPath);u(s)||S(s,{recursive:!0});let o={},c=!1;if(u(e.configPath))try{const i=l(e.configPath,"utf-8");o=O(i),c=!0}catch{o={}}let n;c&&(n=`${e.configPath}.wyrm-backup`,f(n,l(e.configPath)));const a=V(e,t);switch(e.configFormat){case"vscode":{o.mcp||(o.mcp={});const i=o.mcp;i.servers||(i.servers={});const p=i.servers;p.wyrm=a;break}case"mcp-json":{o[e.mcpKey]||(o[e.mcpKey]={});const i=o[e.mcpKey];i.wyrm=a;break}case"zed":{o[e.mcpKey]||(o[e.mcpKey]={});const i=o[e.mcpKey];i.wyrm=a;break}}f(e.configPath,JSON.stringify(o,null,2)+`
4
- `,"utf-8");const d=e.configured?"updated":"configured";return e.configured=!0,{client:e,action:d,message:`${e.name} ${d==="configured"?"configured":"updated"} successfully`,backup:n}}catch(s){return{client:e,action:"failed",message:`Failed to configure ${e.name}: ${s}`}}}function G(e){if(!e.detected||!e.configured)return{client:e,action:"skipped",message:`${e.name} not configured`};try{if(!u(e.configPath))return{client:e,action:"skipped",message:`${e.name} config not found`};const t=l(e.configPath,"utf-8"),s=O(t),o=`${e.configPath}.wyrm-backup`;switch(f(o,t),e.configFormat){case"vscode":{const n=s.mcp?.servers;n&&delete n.wyrm;break}case"mcp-json":case"zed":{const c=s[e.mcpKey];c&&delete c.wyrm;break}}return f(e.configPath,JSON.stringify(s,null,2)+`
5
- `,"utf-8"),e.configured=!1,{client:e,action:"configured",message:`Removed Wyrm from ${e.name}`,backup:o}}catch(t){return{client:e,action:"failed",message:`Failed to remove from ${e.name}: ${t}`}}}function _(){return{id:"claude-code-hooks",name:"Claude Code (auto-memory)",icon:"\u{1FA9D}",configPath:r(m(),".claude","settings.json"),configFormat:"mcp-json",mcpKey:"",detected:!0,configured:!1}}const W=[{script:"wyrm-session-capture.mjs",events:["SessionEnd","PreCompact"]},{script:"wyrm-session-rehydrate.mjs",events:["SessionStart"]},{script:"wyrm-session-prune.mjs",events:["SessionEnd"]},{script:"wyrm-tool-call-trace.mjs",events:["PostToolUse"]},{script:"wyrm-run-auto.mjs",events:["SessionStart",{event:"PreToolUse",matcher:"Task"},"SessionEnd"]}];function Q(){const e=r(m(),".claude");if(!u(e))return null;const t=_();try{const s=r(e,"hooks");u(s)||S(s,{recursive:!0});const o=w(F(import.meta.url)),c=r(o,"..","scripts","hooks"),n=t.configPath;let a={};if(u(n))try{a=JSON.parse(l(n,"utf-8"))}catch{a={}}const d=a.hooks??{};let i=!1;const p=[];for(const h of W){const k=r(c,h.script);if(!u(k)){p.push(h.script);continue}const j=r(s,h.script);B(k,j);try{I(j,493)}catch{}const x=`node ${j}`;for(const y of h.events){const C=typeof y=="string"?y:y.event,K=typeof y=="string"?void 0:y.matcher,$=Array.isArray(d[C])?d[C]:[];if(!$.some(P=>Array.isArray(P.hooks)&&P.hooks.some(J=>J.command===x))){const P=K?{matcher:K,hooks:[{type:"command",command:x,timeout:30}]}:{hooks:[{type:"command",command:x,timeout:30}]};$.push(P),i=!0}d[C]=$}}a.hooks=d,f(n,JSON.stringify(a,null,2)+`
6
- `,"utf-8"),t.configured=!0;const g=p.length?` (missing from package: ${p.join(", ")})`:"";return{client:t,action:i?"configured":"skipped",message:(i?"auto-memory hooks installed":"already installed")+g}}catch(s){return{client:t,action:"failed",message:s instanceof Error?s.message:String(s)}}}function ie(){const e=r(m(),".claude");if(!u(e))return null;const t=_();try{const s=w(F(import.meta.url)),o=`node ${r(s,"wyrm-statusline.js")}`,c=t.configPath;let n={};if(u(c))try{n=JSON.parse(l(c,"utf-8"))}catch{n={}}const a=n.statusLine;if(a?.command&&!/wyrm-statusline/.test(a.command))return{client:t,action:"skipped",message:`a non-Wyrm statusLine is already set (${a.command}); not overwriting`};const d=a?.command===o;return n.statusLine={type:"command",command:o,padding:0},f(c,JSON.stringify(n,null,2)+`
7
- `,"utf-8"),{client:t,action:d?"skipped":"configured",message:d?"buddy statusline already installed":"Wyrm buddy statusline installed \u2014 persistent in the Claude Code TUI"}}catch(s){return{client:t,action:"failed",message:s instanceof Error?s.message:String(s)}}}function ae(){const e=r(m(),".claude","settings.json");if(!u(e))return null;const t=_();try{const s=JSON.parse(l(e,"utf-8")),o=s.statusLine;return o?.command&&/wyrm-statusline/.test(o.command)?(delete s.statusLine,f(e,JSON.stringify(s,null,2)+`
8
- `,"utf-8"),{client:t,action:"configured",message:"Wyrm statusline removed"}):{client:t,action:"skipped",message:"no Wyrm statusline set"}}catch(s){return{client:t,action:"failed",message:s instanceof Error?s.message:String(s)}}}function Z(){const e=r(m(),".claude","settings.json");if(!u(e))return null;const t=_();try{const s=JSON.parse(l(e,"utf-8")),o=r(m(),".claude","hooks"),c=new Set(W.map(d=>`node ${r(o,d.script)}`)),n=new Set(W.flatMap(d=>d.events.map(i=>typeof i=="string"?i:i.event))),a=s.hooks;if(a){for(const d of n)Array.isArray(a[d])&&(a[d]=a[d].map(i=>({...i,hooks:Array.isArray(i.hooks)?i.hooks.filter(p=>!c.has(p.command??"")):i.hooks})).filter(i=>!Array.isArray(i.hooks)||i.hooks.length>0),a[d].length===0&&delete a[d]);Object.keys(a).length===0&&delete s.hooks}return f(e,JSON.stringify(s,null,2)+`
9
- `,"utf-8"),{client:t,action:"configured",message:"auto-memory hooks removed"}}catch(s){return{client:t,action:"failed",message:s instanceof Error?s.message:String(s)}}}function X(e){const t={serverPath:e?.serverPath||R(),dbPath:e?.dbPath||E(),httpPort:e?.httpPort},s=r(m(),".wyrm");u(s)||S(s,{recursive:!0}),M(t);const o=v(),c=[];for(const a of o)c.push(L(a,t));const n=Q();return n&&c.push(n),c}function de(){const e=v(),t=[];for(const o of e)t.push(G(o));const s=Z();return s&&t.push(s),t}function ee(e,t){const s={serverPath:t?.serverPath||R(),dbPath:t?.dbPath||E(),httpPort:t?.httpPort};M(s);const o=v(),c=[];for(const n of o)e.includes(n.id)&&c.push(L(n,s));return c}function M(e){const t=r(m(),".wyrm","wyrm-config.json"),s=v().filter(c=>c.configured).map(c=>c.id),o={version:"3.0.0",serverPath:e.serverPath,dbPath:e.dbPath,httpPort:e.httpPort,configuredClients:s,lastSetup:new Date().toISOString(),autoUpdate:!0};f(t,JSON.stringify(o,null,2)+`
10
- `,"utf-8")}function N(){const e=r(m(),".wyrm","wyrm-config.json");if(!u(e))return null;try{return JSON.parse(l(e,"utf-8"))}catch{return null}}function ue(){const e=N();return e?ee(e.configuredClients,{serverPath:e.serverPath,dbPath:e.dbPath,httpPort:e.httpPort}):X()}const T="<!-- wyrm:start -->",A="<!-- wyrm:end -->",D=`${T}
11
- ## Wyrm Memory + Agent (auto-managed \u2014 do not edit this section)
1
+ /**
2
+ * Wyrm Auto-Configure - Universal AI Client Detection & Setup
3
+ *
4
+ * Automatically detects installed AI clients (VS Code Copilot, Claude Desktop,
5
+ * Cursor, Windsurf, Zed, etc.) and configures Wyrm's MCP server in each.
6
+ * Handles provider switching seamlessly change your AI, Wyrm follows.
7
+ *
8
+ * @copyright 2026 Ghost Protocol (Pvt) Ltd.
9
+ * @license Proprietary (c) 2026 Ghost Protocol (Pvt) Ltd. All rights reserved. See LICENSE.
10
+ * @module autoconfig
11
+ * @version 3.0.0
12
+ */
13
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, copyFileSync, chmodSync } from 'fs';
14
+ import { homedir, platform } from 'os';
15
+ import { join, dirname, resolve } from 'path';
16
+ import { fileURLToPath } from 'url';
17
+ import { spawnSync } from 'child_process';
18
+ // ==================== CLIENT DEFINITIONS ====================
19
+ function getConfigPaths() {
20
+ const home = homedir();
21
+ const os = platform();
22
+ // Base config directories per OS
23
+ const vscodeBase = os === 'darwin'
24
+ ? join(home, 'Library', 'Application Support', 'Code', 'User')
25
+ : os === 'win32'
26
+ ? join(home, 'AppData', 'Roaming', 'Code', 'User')
27
+ : join(home, '.config', 'Code', 'User');
28
+ const vscodeInsidersBase = os === 'darwin'
29
+ ? join(home, 'Library', 'Application Support', 'Code - Insiders', 'User')
30
+ : os === 'win32'
31
+ ? join(home, 'AppData', 'Roaming', 'Code - Insiders', 'User')
32
+ : join(home, '.config', 'Code - Insiders', 'User');
33
+ const claudeBase = os === 'darwin'
34
+ ? join(home, 'Library', 'Application Support', 'Claude')
35
+ : os === 'win32'
36
+ ? join(home, 'AppData', 'Roaming', 'Claude')
37
+ : join(home, '.config', 'claude');
38
+ const cursorBase = os === 'darwin'
39
+ ? join(home, '.cursor')
40
+ : os === 'win32'
41
+ ? join(home, '.cursor')
42
+ : join(home, '.cursor');
43
+ const windsurfBase = os === 'darwin'
44
+ ? join(home, '.codeium', 'windsurf')
45
+ : os === 'win32'
46
+ ? join(home, '.codeium', 'windsurf')
47
+ : join(home, '.codeium', 'windsurf');
48
+ const zedBase = os === 'darwin'
49
+ ? join(home, '.config', 'zed')
50
+ : os === 'win32'
51
+ ? join(home, 'AppData', 'Roaming', 'Zed')
52
+ : join(home, '.config', 'zed');
53
+ const continueBase = join(home, '.continue');
54
+ return [
55
+ {
56
+ id: 'vscode-copilot',
57
+ name: 'VS Code (Copilot)',
58
+ icon: '💻',
59
+ configPath: join(vscodeBase, 'settings.json'),
60
+ configFormat: 'vscode',
61
+ mcpKey: 'mcp.servers',
62
+ detected: false,
63
+ configured: false,
64
+ },
65
+ {
66
+ id: 'vscode-insiders',
67
+ name: 'VS Code Insiders',
68
+ icon: '🟢',
69
+ configPath: join(vscodeInsidersBase, 'settings.json'),
70
+ configFormat: 'vscode',
71
+ mcpKey: 'mcp.servers',
72
+ detected: false,
73
+ configured: false,
74
+ },
75
+ {
76
+ id: 'claude-desktop',
77
+ name: 'Claude Desktop',
78
+ icon: '🤖',
79
+ configPath: join(claudeBase, 'claude_desktop_config.json'),
80
+ configFormat: 'mcp-json',
81
+ mcpKey: 'mcpServers',
82
+ detected: false,
83
+ configured: false,
84
+ },
85
+ {
86
+ id: 'cursor',
87
+ name: 'Cursor',
88
+ icon: '📐',
89
+ configPath: join(cursorBase, 'mcp.json'),
90
+ configFormat: 'mcp-json',
91
+ mcpKey: 'mcpServers',
92
+ detected: false,
93
+ configured: false,
94
+ },
95
+ {
96
+ id: 'windsurf',
97
+ name: 'Windsurf',
98
+ icon: '🏄',
99
+ configPath: join(windsurfBase, 'mcp_config.json'),
100
+ configFormat: 'mcp-json',
101
+ mcpKey: 'mcpServers',
102
+ detected: false,
103
+ configured: false,
104
+ },
105
+ {
106
+ id: 'zed',
107
+ name: 'Zed',
108
+ icon: '⚡',
109
+ configPath: join(zedBase, 'settings.json'),
110
+ configFormat: 'zed',
111
+ mcpKey: 'context_servers',
112
+ detected: false,
113
+ configured: false,
114
+ },
115
+ {
116
+ id: 'continue',
117
+ name: 'Continue',
118
+ icon: '🔄',
119
+ configPath: join(continueBase, 'config.json'),
120
+ configFormat: 'mcp-json',
121
+ mcpKey: 'mcpServers',
122
+ detected: false,
123
+ configured: false,
124
+ },
125
+ ];
126
+ }
127
+ // ==================== DETECTION ====================
128
+ /**
129
+ * Detect which AI clients are installed by checking for their config directories
130
+ */
131
+ export function detectClients() {
132
+ const clients = getConfigPaths();
133
+ for (const client of clients) {
134
+ // Check if config directory exists (the client is installed)
135
+ const configDir = dirname(client.configPath);
136
+ client.detected = existsSync(configDir);
137
+ // Check if Wyrm is already configured
138
+ if (client.detected && existsSync(client.configPath)) {
139
+ try {
140
+ const content = readFileSync(client.configPath, 'utf-8');
141
+ const config = parseJsonWithComments(content);
142
+ client.configured = hasWyrmConfig(config, client);
143
+ }
144
+ catch {
145
+ client.configured = false;
146
+ }
147
+ }
148
+ // Try to detect version
149
+ if (client.detected) {
150
+ client.version = detectClientVersion(client);
151
+ }
152
+ }
153
+ return clients;
154
+ }
155
+ /**
156
+ * Detect the version of an AI client
157
+ */
158
+ function detectClientVersion(client) {
159
+ try {
160
+ switch (client.id) {
161
+ case 'vscode-copilot':
162
+ case 'vscode-insiders': {
163
+ const cmd = client.id === 'vscode-insiders' ? 'code-insiders' : 'code';
164
+ const result = spawnSync(cmd, ['--version'], { encoding: 'utf-8', timeout: 5000 });
165
+ if (result.stdout)
166
+ return result.stdout.split('\n')[0];
167
+ break;
168
+ }
169
+ case 'cursor': {
170
+ const result = spawnSync('cursor', ['--version'], { encoding: 'utf-8', timeout: 5000 });
171
+ if (result.stdout)
172
+ return result.stdout.split('\n')[0];
173
+ break;
174
+ }
175
+ }
176
+ }
177
+ catch {
178
+ // Version detection is optional
179
+ }
180
+ return undefined;
181
+ }
182
+ /**
183
+ * Check if Wyrm is already configured in a client's config
184
+ */
185
+ function hasWyrmConfig(config, client) {
186
+ switch (client.configFormat) {
187
+ case 'vscode': {
188
+ const mcp = config['mcp'];
189
+ const servers = mcp?.['servers'];
190
+ return servers?.['wyrm'] !== undefined;
191
+ }
192
+ case 'mcp-json': {
193
+ const servers = config[client.mcpKey];
194
+ return servers?.['wyrm'] !== undefined;
195
+ }
196
+ case 'zed': {
197
+ const servers = config[client.mcpKey];
198
+ return servers?.['wyrm'] !== undefined;
199
+ }
200
+ default:
201
+ return false;
202
+ }
203
+ }
204
+ // ==================== CONFIGURATION ====================
205
+ /**
206
+ * Auto-detect Wyrm server path
207
+ */
208
+ export function findWyrmServerPath() {
209
+ // 1. Check if wyrm-mcp binary is in PATH
210
+ try {
211
+ const result = spawnSync('which', ['wyrm-mcp'], { encoding: 'utf-8', timeout: 5000 });
212
+ if (result.stdout?.trim()) {
213
+ return result.stdout.trim();
214
+ }
215
+ }
216
+ catch { }
217
+ // 2. Check common npm global install locations
218
+ try {
219
+ const result = spawnSync('npm', ['root', '-g'], { encoding: 'utf-8', timeout: 5000 });
220
+ const globalDir = result.stdout?.trim();
221
+ if (globalDir) {
222
+ const globalPath = join(globalDir, 'wyrm-mcp', 'dist', 'index.js');
223
+ if (existsSync(globalPath))
224
+ return globalPath;
225
+ }
226
+ }
227
+ catch { }
228
+ // 3. Check local development path (relative to this file)
229
+ const devPath = resolve(__dirname, 'index.js');
230
+ if (existsSync(devPath))
231
+ return devPath;
232
+ // 4. Check ~/.wyrm/node_modules
233
+ const wyrmModulesPath = join(homedir(), '.wyrm', 'node_modules', 'wyrm-mcp', 'dist', 'index.js');
234
+ if (existsSync(wyrmModulesPath))
235
+ return wyrmModulesPath;
236
+ // 5. Check common project locations
237
+ const projectLocations = [
238
+ join(homedir(), 'Git Projects', 'Wyrm', 'packages', 'mcp-server', 'dist', 'index.js'),
239
+ join(homedir(), 'projects', 'Wyrm', 'packages', 'mcp-server', 'dist', 'index.js'),
240
+ join(homedir(), 'dev', 'Wyrm', 'packages', 'mcp-server', 'dist', 'index.js'),
241
+ ];
242
+ for (const loc of projectLocations) {
243
+ if (existsSync(loc))
244
+ return loc;
245
+ }
246
+ // 6. Fallback to npx
247
+ return 'wyrm-mcp';
248
+ }
249
+ /**
250
+ * Get the default Wyrm database path
251
+ */
252
+ export function getDefaultDbPath() {
253
+ return join(homedir(), '.wyrm', 'wyrm.db');
254
+ }
255
+ /**
256
+ * Build the Wyrm MCP config object for a specific client format
257
+ */
258
+ function buildWyrmMcpConfig(client, wyrmConfig) {
259
+ const serverPath = wyrmConfig.serverPath;
260
+ const isNpx = serverPath === 'wyrm-mcp';
261
+ const isJsFile = serverPath.endsWith('.js');
262
+ switch (client.configFormat) {
263
+ case 'vscode':
264
+ return {
265
+ command: isNpx ? 'npx' : 'node',
266
+ args: isNpx ? ['wyrm-mcp'] : [serverPath],
267
+ env: {
268
+ WYRM_DB_PATH: wyrmConfig.dbPath,
269
+ },
270
+ };
271
+ case 'mcp-json':
272
+ return {
273
+ command: isNpx ? 'npx' : 'node',
274
+ args: isNpx ? ['wyrm-mcp'] : [serverPath],
275
+ env: {
276
+ WYRM_DB_PATH: wyrmConfig.dbPath,
277
+ },
278
+ };
279
+ case 'zed':
280
+ return {
281
+ command: isNpx ? 'npx' : 'node',
282
+ args: isNpx ? ['wyrm-mcp'] : [serverPath],
283
+ env: {
284
+ WYRM_DB_PATH: wyrmConfig.dbPath,
285
+ },
286
+ };
287
+ default:
288
+ return {};
289
+ }
290
+ }
291
+ /**
292
+ * Configure Wyrm in a single AI client
293
+ */
294
+ export function configureClient(client, wyrmConfig) {
295
+ if (!client.detected) {
296
+ return {
297
+ client,
298
+ action: 'skipped',
299
+ message: `${client.name} not detected`,
300
+ };
301
+ }
302
+ try {
303
+ // Ensure config directory exists
304
+ const configDir = dirname(client.configPath);
305
+ if (!existsSync(configDir)) {
306
+ mkdirSync(configDir, { recursive: true });
307
+ }
308
+ // Read existing config or create new
309
+ let config = {};
310
+ let existed = false;
311
+ if (existsSync(client.configPath)) {
312
+ try {
313
+ const content = readFileSync(client.configPath, 'utf-8');
314
+ config = parseJsonWithComments(content);
315
+ existed = true;
316
+ }
317
+ catch {
318
+ // If config exists but is invalid, start fresh
319
+ config = {};
320
+ }
321
+ }
322
+ // Create backup before modifying
323
+ let backupPath;
324
+ if (existed) {
325
+ backupPath = `${client.configPath}.wyrm-backup`;
326
+ writeFileSync(backupPath, readFileSync(client.configPath));
327
+ }
328
+ // Build Wyrm MCP entry
329
+ const wyrmEntry = buildWyrmMcpConfig(client, wyrmConfig);
330
+ // Inject into config based on format
331
+ switch (client.configFormat) {
332
+ case 'vscode': {
333
+ // VS Code: { "mcp": { "servers": { "wyrm": {...} } } }
334
+ if (!config['mcp'])
335
+ config['mcp'] = {};
336
+ const mcp = config['mcp'];
337
+ if (!mcp['servers'])
338
+ mcp['servers'] = {};
339
+ const servers = mcp['servers'];
340
+ servers['wyrm'] = wyrmEntry;
341
+ break;
342
+ }
343
+ case 'mcp-json': {
344
+ // MCP JSON: { "mcpServers": { "wyrm": {...} } }
345
+ if (!config[client.mcpKey])
346
+ config[client.mcpKey] = {};
347
+ const servers = config[client.mcpKey];
348
+ servers['wyrm'] = wyrmEntry;
349
+ break;
350
+ }
351
+ case 'zed': {
352
+ // Zed: { "context_servers": { "wyrm": {...} } }
353
+ if (!config[client.mcpKey])
354
+ config[client.mcpKey] = {};
355
+ const servers = config[client.mcpKey];
356
+ servers['wyrm'] = wyrmEntry;
357
+ break;
358
+ }
359
+ }
360
+ // Write config back
361
+ writeFileSync(client.configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
362
+ const action = client.configured ? 'updated' : 'configured';
363
+ client.configured = true;
364
+ return {
365
+ client,
366
+ action,
367
+ message: `${client.name} ${action === 'configured' ? 'configured' : 'updated'} successfully`,
368
+ backup: backupPath,
369
+ };
370
+ }
371
+ catch (error) {
372
+ return {
373
+ client,
374
+ action: 'failed',
375
+ message: `Failed to configure ${client.name}: ${error}`,
376
+ };
377
+ }
378
+ }
379
+ /**
380
+ * Remove Wyrm from a single AI client
381
+ */
382
+ export function removeFromClient(client) {
383
+ if (!client.detected || !client.configured) {
384
+ return {
385
+ client,
386
+ action: 'skipped',
387
+ message: `${client.name} not configured`,
388
+ };
389
+ }
390
+ try {
391
+ if (!existsSync(client.configPath)) {
392
+ return { client, action: 'skipped', message: `${client.name} config not found` };
393
+ }
394
+ const content = readFileSync(client.configPath, 'utf-8');
395
+ const config = parseJsonWithComments(content);
396
+ // Create backup
397
+ const backupPath = `${client.configPath}.wyrm-backup`;
398
+ writeFileSync(backupPath, content);
399
+ // Remove Wyrm entry
400
+ switch (client.configFormat) {
401
+ case 'vscode': {
402
+ const mcp = config['mcp'];
403
+ const servers = mcp?.['servers'];
404
+ if (servers)
405
+ delete servers['wyrm'];
406
+ break;
407
+ }
408
+ case 'mcp-json':
409
+ case 'zed': {
410
+ const servers = config[client.mcpKey];
411
+ if (servers)
412
+ delete servers['wyrm'];
413
+ break;
414
+ }
415
+ }
416
+ writeFileSync(client.configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
417
+ client.configured = false;
418
+ return {
419
+ client,
420
+ action: 'configured',
421
+ message: `Removed Wyrm from ${client.name}`,
422
+ backup: backupPath,
423
+ };
424
+ }
425
+ catch (error) {
426
+ return {
427
+ client,
428
+ action: 'failed',
429
+ message: `Failed to remove from ${client.name}: ${error}`,
430
+ };
431
+ }
432
+ }
433
+ // ==================== ORCHESTRATION ====================
434
+ // ==================== CLAUDE CODE CLI HOOKS ====================
435
+ /** Synthetic "client" so hook install/removal renders in the setup summary. */
436
+ function claudeHooksClient() {
437
+ return {
438
+ id: 'claude-code-hooks',
439
+ name: 'Claude Code (auto-memory)',
440
+ icon: '🪝',
441
+ configPath: join(homedir(), '.claude', 'settings.json'),
442
+ configFormat: 'mcp-json',
443
+ mcpKey: '',
444
+ detected: true,
445
+ configured: false,
446
+ };
447
+ }
448
+ const CLAUDE_HOOK_SPECS = [
449
+ { script: 'wyrm-session-capture.mjs', events: ['SessionEnd', 'PreCompact'] },
450
+ { script: 'wyrm-session-rehydrate.mjs', events: ['SessionStart'] },
451
+ { script: 'wyrm-session-prune.mjs', events: ['SessionEnd'] },
452
+ { script: 'wyrm-tool-call-trace.mjs', events: ['PostToolUse'] },
453
+ { script: 'wyrm-run-auto.mjs', events: ['SessionStart', { event: 'PreToolUse', matcher: 'Task' }, 'SessionEnd'] },
454
+ ];
455
+ /**
456
+ * Install Wyrm's Claude Code CLI hooks so every session is auto-remembered
457
+ * (capture), auto-restored (rehydrate), and optionally tidied (prune). Ships
458
+ * the hook scripts next to the user's other hooks and merges the hook entries
459
+ * into ~/.claude/settings.json WITHOUT clobbering any hooks already there.
460
+ * Idempotent. No-op (returns null) when the Claude Code CLI isn't present.
461
+ */
462
+ export function installClaudeCodeHooks() {
463
+ const claudeDir = join(homedir(), '.claude');
464
+ if (!existsSync(claudeDir))
465
+ return null; // Claude Code CLI not installed here
466
+ const client = claudeHooksClient();
467
+ try {
468
+ const hooksDir = join(claudeDir, 'hooks');
469
+ if (!existsSync(hooksDir))
470
+ mkdirSync(hooksDir, { recursive: true });
471
+ // Hook scripts live next to dist/ (one level below the package root).
472
+ const here = dirname(fileURLToPath(import.meta.url));
473
+ const scriptsDir = join(here, '..', 'scripts', 'hooks');
474
+ const settingsPath = client.configPath;
475
+ let settings = {};
476
+ if (existsSync(settingsPath)) {
477
+ try {
478
+ settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
479
+ }
480
+ catch {
481
+ settings = {};
482
+ }
483
+ }
484
+ const hooks = (settings.hooks ?? {});
485
+ let added = false;
486
+ const missing = [];
487
+ for (const spec of CLAUDE_HOOK_SPECS) {
488
+ const src = join(scriptsDir, spec.script);
489
+ if (!existsSync(src)) {
490
+ missing.push(spec.script);
491
+ continue;
492
+ } // skip, don't abort the rest
493
+ const dest = join(hooksDir, spec.script);
494
+ copyFileSync(src, dest);
495
+ try {
496
+ chmodSync(dest, 0o755);
497
+ }
498
+ catch { /* non-fatal */ }
499
+ const cmd = `node ${dest}`;
500
+ for (const ev of spec.events) {
501
+ const event = typeof ev === 'string' ? ev : ev.event;
502
+ const matcher = typeof ev === 'string' ? undefined : ev.matcher;
503
+ const groups = Array.isArray(hooks[event]) ? hooks[event] : [];
504
+ // Idempotent on the command anywhere under this event (a re-run never
505
+ // duplicates), and the new group carries the matcher when one is set.
506
+ const present = groups.some((g) => Array.isArray(g.hooks) && g.hooks.some((h) => h.command === cmd));
507
+ if (!present) {
508
+ const group = matcher
509
+ ? { matcher, hooks: [{ type: 'command', command: cmd, timeout: 30 }] }
510
+ : { hooks: [{ type: 'command', command: cmd, timeout: 30 }] };
511
+ groups.push(group);
512
+ added = true;
513
+ }
514
+ hooks[event] = groups;
515
+ }
516
+ }
517
+ settings.hooks = hooks;
518
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
519
+ client.configured = true;
520
+ const note = missing.length ? ` (missing from package: ${missing.join(', ')})` : '';
521
+ return { client, action: added ? 'configured' : 'skipped', message: (added ? 'auto-memory hooks installed' : 'already installed') + note };
522
+ }
523
+ catch (err) {
524
+ return { client, action: 'failed', message: err instanceof Error ? err.message : String(err) };
525
+ }
526
+ }
527
+ /**
528
+ * Install Wyrm's buddy statusline into Claude Code (`statusLine` in settings.json)
529
+ * so the dragon + live memory (project · open quests · blocked failures · truths)
530
+ * is visible in the TUI AT ALL TIMES — not just when a tool is called. Won't
531
+ * clobber a non-Wyrm statusline the user already configured.
532
+ */
533
+ export function installClaudeStatusline() {
534
+ const claudeDir = join(homedir(), '.claude');
535
+ if (!existsSync(claudeDir))
536
+ return null;
537
+ const client = claudeHooksClient();
538
+ try {
539
+ const here = dirname(fileURLToPath(import.meta.url)); // dist/
540
+ const cmd = `node ${join(here, 'wyrm-statusline.js')}`;
541
+ const settingsPath = client.configPath;
542
+ let settings = {};
543
+ if (existsSync(settingsPath)) {
544
+ try {
545
+ settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
546
+ }
547
+ catch {
548
+ settings = {};
549
+ }
550
+ }
551
+ const existing = settings.statusLine;
552
+ if (existing?.command && !/wyrm-statusline/.test(existing.command)) {
553
+ return { client, action: 'skipped', message: `a non-Wyrm statusLine is already set (${existing.command}); not overwriting` };
554
+ }
555
+ const already = existing?.command === cmd;
556
+ settings.statusLine = { type: 'command', command: cmd, padding: 0 };
557
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
558
+ return { client, action: already ? 'skipped' : 'configured', message: already ? 'buddy statusline already installed' : 'Wyrm buddy statusline installed — persistent in the Claude Code TUI' };
559
+ }
560
+ catch (err) {
561
+ return { client, action: 'failed', message: err instanceof Error ? err.message : String(err) };
562
+ }
563
+ }
564
+ /** Remove the Wyrm statusline from settings.json (leaves any non-Wyrm one). */
565
+ export function removeClaudeStatusline() {
566
+ const settingsPath = join(homedir(), '.claude', 'settings.json');
567
+ if (!existsSync(settingsPath))
568
+ return null;
569
+ const client = claudeHooksClient();
570
+ try {
571
+ const settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
572
+ const sl = settings.statusLine;
573
+ if (sl?.command && /wyrm-statusline/.test(sl.command)) {
574
+ delete settings.statusLine;
575
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
576
+ return { client, action: 'configured', message: 'Wyrm statusline removed' };
577
+ }
578
+ return { client, action: 'skipped', message: 'no Wyrm statusline set' };
579
+ }
580
+ catch (err) {
581
+ return { client, action: 'failed', message: err instanceof Error ? err.message : String(err) };
582
+ }
583
+ }
584
+ /** Remove all Wyrm hooks from ~/.claude/settings.json (leaves other hooks). */
585
+ export function removeClaudeCodeHooks() {
586
+ const settingsPath = join(homedir(), '.claude', 'settings.json');
587
+ if (!existsSync(settingsPath))
588
+ return null;
589
+ const client = claudeHooksClient();
590
+ try {
591
+ const settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
592
+ const hooksDir = join(homedir(), '.claude', 'hooks');
593
+ const ourCmds = new Set(CLAUDE_HOOK_SPECS.map((s) => `node ${join(hooksDir, s.script)}`));
594
+ const ourEvents = new Set(CLAUDE_HOOK_SPECS.flatMap((s) => s.events.map((ev) => (typeof ev === 'string' ? ev : ev.event))));
595
+ const hooks = settings.hooks;
596
+ if (hooks) {
597
+ for (const event of ourEvents) {
598
+ if (!Array.isArray(hooks[event]))
599
+ continue;
600
+ hooks[event] = hooks[event]
601
+ .map((g) => ({ ...g, hooks: Array.isArray(g.hooks) ? g.hooks.filter((h) => !ourCmds.has(h.command ?? '')) : g.hooks }))
602
+ .filter((g) => !Array.isArray(g.hooks) || g.hooks.length > 0);
603
+ if (hooks[event].length === 0)
604
+ delete hooks[event];
605
+ }
606
+ if (Object.keys(hooks).length === 0)
607
+ delete settings.hooks;
608
+ }
609
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
610
+ return { client, action: 'configured', message: 'auto-memory hooks removed' };
611
+ }
612
+ catch (err) {
613
+ return { client, action: 'failed', message: err instanceof Error ? err.message : String(err) };
614
+ }
615
+ }
616
+ // ==================== WYRM-GUARD FAILURE-FIREWALL HOOKS ====================
617
+ //
618
+ // Pure-TS port of the scripts/hooks/install.sh strip/merge logic for the
619
+ // wyrm-guard half — the deterministic PreToolUse failure gate shipped as the
620
+ // npm bin `wyrm-guard` (package.json bin → dist/wyrm-guard.js). install.sh
621
+ // remains the repo-checkout tool (it also manages wyrm-push.py, which the npm
622
+ // `files` array does NOT ship); THIS installer is what npm-global users get
623
+ // from `wyrm setup` / `wyrm guard`, with no jq or shell dependency.
624
+ //
625
+ // install.sh semantics preserved:
626
+ // · strip-then-add — every prior entry whose command matches the guard
627
+ // pattern is stripped before the fresh entries are added, so a re-run
628
+ // never duplicates and entries survive a moved install / renamed bin;
629
+ // · the same two PreToolUse matchers ('Bash' and
630
+ // 'Edit|Write|MultiEdit|NotebookEdit'), each { type:'command', timeout:1 };
631
+ // · groups/events OUR strip emptied are dropped (as is a hooks object our
632
+ // removal emptied) — but pre-existing empty user groups/events are NOT
633
+ // ours to prune and survive byte-identical;
634
+ // · unrelated hooks/settings are never touched (and unlike install.sh, a
635
+ // no-op uninstall does not even rewrite the file).
636
+ // Deliberate divergences: the strip sweeps EVERY hook event for our pattern
637
+ // (install.sh listed only two specific event types), wyrm-push.py entries are
638
+ // left alone (they are install.sh's to manage — the guard installer only owns
639
+ // what npm distributes), and an unparseable settings.json — or a hooks /
640
+ // hooks.PreToolUse shape we cannot faithfully merge into — is REFUSED rather
641
+ // than clobbered.
642
+ /** A hook command is OURS iff it matches this (the install.sh `ours` test —
643
+ * pattern match, so entries survive a moved install / changed bin path). */
644
+ const GUARD_COMMAND_PATTERN = /wyrm-guard/;
645
+ /** The PreToolUse matchers install.sh wires for the guard. */
646
+ const GUARD_MATCHERS = ['Bash', 'Edit|Write|MultiEdit|NotebookEdit'];
647
+ function guardSettingsPath(options) {
648
+ return options.settingsPath
649
+ ?? process.env.CLAUDE_SETTINGS
650
+ ?? join(homedir(), '.claude', 'settings.json');
651
+ }
652
+ /** Synthetic "client" so guard install/removal renders in the setup summary. */
653
+ function guardClient(settingsPath) {
654
+ return {
655
+ id: 'claude-code-guard',
656
+ name: 'Claude Code (failure firewall)',
657
+ icon: '🛡️',
658
+ configPath: settingsPath,
659
+ configFormat: 'mcp-json',
660
+ mcpKey: '',
661
+ detected: true,
662
+ configured: false,
663
+ };
664
+ }
665
+ /**
666
+ * Shell-quote a path for the PreToolUse hook command, which Claude Code runs
667
+ * THROUGH A SHELL on every tool call. POSIX single-quoting makes $, backtick,
668
+ * backslash and double-quote inert — double-quoting does NOT, so a path
669
+ * containing a command substitution would EXECUTE on every tool call (crucible
670
+ * HIGH 2026-07-03). cmd.exe (Windows) does not honor single quotes, so there
671
+ * we wrap in double quotes and escape embedded double-quotes.
672
+ */
673
+ export function quoteHookPath(p) {
674
+ if (platform() === 'win32')
675
+ return `"${p.replace(/"/g, '""')}"`;
676
+ return `'${p.replace(/'/g, `'\\''`)}'`;
677
+ }
678
+ /**
679
+ * Resolve the wyrm-guard hook command for THIS running install. Never a
680
+ * repo-checkout guess:
681
+ * 1. dist sibling — 'wyrm-guard.js' next to this module (autoconfig ships in
682
+ * the same dist/ for npm-global installs and built checkouts alike),
683
+ * invoked `node '<abs path>'` (shell-quoted via quoteHookPath: install
684
+ * paths may contain spaces AND shell metacharacters; works without an
685
+ * executable bit or shebang support);
686
+ * 2. else the package.json bin name 'wyrm-guard' resolved on PATH
687
+ * (which/where — the same probe install.sh uses), shell-quoted the same way.
688
+ */
689
+ export function resolveWyrmGuardCommand() {
690
+ const here = dirname(fileURLToPath(import.meta.url));
691
+ const sibling = join(here, 'wyrm-guard.js');
692
+ if (existsSync(sibling))
693
+ return `node ${quoteHookPath(sibling)}`;
694
+ try {
695
+ const probe = platform() === 'win32' ? 'where' : 'which';
696
+ const result = spawnSync(probe, ['wyrm-guard'], { encoding: 'utf-8', timeout: 5000 });
697
+ const found = result.stdout?.trim().split('\n')[0]?.trim();
698
+ if (found)
699
+ return quoteHookPath(found);
700
+ }
701
+ catch { /* PATH probe is best-effort */ }
702
+ return null;
703
+ }
704
+ /**
705
+ * Strip every hook entry whose command matches GUARD_COMMAND_PATTERN from
706
+ * every event group (the install.sh strip_existing filter, in TS). Only OUR
707
+ * entries go; unrelated groups and shapes we don't understand are preserved
708
+ * exactly as parsed. Groups/events are pruned ONLY when OUR removal emptied
709
+ * them — a group or event array that was ALREADY empty is user config and
710
+ * survives byte-identical (dropping it would break the uninstall round-trip).
711
+ * Returns true when anything was removed.
712
+ */
713
+ function stripWyrmGuardEntries(settings) {
714
+ const hooks = settings.hooks;
715
+ if (!hooks || typeof hooks !== 'object' || Array.isArray(hooks))
716
+ return false;
717
+ let removed = false;
718
+ for (const event of Object.keys(hooks)) {
719
+ const groups = hooks[event];
720
+ if (!Array.isArray(groups))
721
+ continue; // unknown shape — never touch it
722
+ let eventTouched = false;
723
+ const pruned = [];
724
+ for (const g of groups) {
725
+ const group = g;
726
+ if (!group || typeof group !== 'object' || !Array.isArray(group.hooks)) {
727
+ pruned.push(g); // unknown shape — never touch it
728
+ continue;
729
+ }
730
+ const kept = group.hooks.filter((h) => !GUARD_COMMAND_PATTERN.test(h?.command ?? ''));
731
+ if (kept.length === group.hooks.length) {
732
+ pruned.push(g); // untouched (incl. pre-existing empty groups) — keep the exact object
733
+ continue;
734
+ }
735
+ removed = true;
736
+ eventTouched = true;
737
+ if (kept.length > 0)
738
+ pruned.push({ ...group, hooks: kept });
739
+ // else: OUR strip emptied this group — drop it (the install.sh prune)
740
+ }
741
+ if (!eventTouched)
742
+ continue; // nothing of ours here — leave the event exactly as parsed
743
+ if (pruned.length === 0)
744
+ delete hooks[event]; // OUR removal emptied the event
745
+ else
746
+ hooks[event] = pruned;
747
+ }
748
+ if (removed && Object.keys(hooks).length === 0)
749
+ delete settings.hooks;
750
+ return removed;
751
+ }
752
+ /**
753
+ * Install the wyrm-guard failure-firewall hooks into Claude Code's
754
+ * settings.json — the zero-config path (`wyrm setup` runs this) and the
755
+ * explicit one (`wyrm guard`). Idempotent strip-then-add: a re-run never
756
+ * duplicates, and a byte-identical result skips the write entirely. No-op
757
+ * (returns null) when the Claude Code CLI isn't present and no explicit
758
+ * target was given.
759
+ */
760
+ export function installWyrmGuardHooks(options = {}) {
761
+ const explicitTarget = Boolean(options.settingsPath ?? process.env.CLAUDE_SETTINGS);
762
+ if (!explicitTarget && !existsSync(join(homedir(), '.claude')))
763
+ return null;
764
+ const settingsPath = guardSettingsPath(options);
765
+ const client = guardClient(settingsPath);
766
+ try {
767
+ const command = options.guardCommand ?? resolveWyrmGuardCommand();
768
+ if (!command) {
769
+ return {
770
+ client,
771
+ action: 'skipped',
772
+ message: 'wyrm-guard bin not found (npm i -g wyrm-mcp, or build dist/) — firewall hooks not installed',
773
+ };
774
+ }
775
+ mkdirSync(dirname(settingsPath), { recursive: true });
776
+ let settings = {};
777
+ if (existsSync(settingsPath)) {
778
+ try {
779
+ const parsed = JSON.parse(readFileSync(settingsPath, 'utf-8'));
780
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed))
781
+ throw new Error('not an object');
782
+ settings = parsed;
783
+ }
784
+ catch {
785
+ // install.sh (jq under set -e) aborted here too — never clobber a
786
+ // file we cannot faithfully merge into.
787
+ return { client, action: 'failed', message: `${settingsPath} is not valid JSON — refusing to overwrite it (fix or remove the file, then re-run)` };
788
+ }
789
+ }
790
+ // Shape guard — the same never-clobber stance as the invalid-JSON refusal.
791
+ // A hooks value that is not a plain object (array/string/null/…) or a
792
+ // PreToolUse that is not an array cannot be faithfully merged into:
793
+ // proceeding would either discard user data or silently install nothing
794
+ // while reporting success. install.sh (jq under set -e) aborted on these
795
+ // shapes too.
796
+ const rawHooks = settings.hooks;
797
+ if (rawHooks !== undefined && (typeof rawHooks !== 'object' || rawHooks === null || Array.isArray(rawHooks))) {
798
+ return { client, action: 'failed', message: `${settingsPath} "hooks" has an unexpected shape (not a plain object) — refusing to modify (fix the file, then re-run)` };
799
+ }
800
+ const rawPreToolUse = rawHooks?.['PreToolUse'];
801
+ if (rawPreToolUse !== undefined && !Array.isArray(rawPreToolUse)) {
802
+ return { client, action: 'failed', message: `${settingsPath} hooks.PreToolUse has an unexpected shape (not an array) — refusing to modify (fix the file, then re-run)` };
803
+ }
804
+ const before = JSON.stringify(settings);
805
+ stripWyrmGuardEntries(settings);
806
+ const hooks = (settings.hooks ?? {});
807
+ const groups = (Array.isArray(hooks['PreToolUse']) ? hooks['PreToolUse'] : []);
808
+ for (const matcher of GUARD_MATCHERS) {
809
+ // The exact install.sh entry shape: one group per matcher, 1s timeout
810
+ // (the guard's wyrm-push ~200ms budget discipline, with headroom).
811
+ groups.push({ matcher, hooks: [{ type: 'command', command, timeout: 1 }] });
812
+ }
813
+ hooks['PreToolUse'] = groups;
814
+ settings.hooks = hooks;
815
+ const changed = JSON.stringify(settings) !== before;
816
+ if (changed)
817
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
818
+ client.configured = true;
819
+ return {
820
+ client,
821
+ action: changed ? 'configured' : 'skipped',
822
+ message: changed
823
+ ? 'failure-firewall hooks installed (PreToolUse: Bash + Edit|Write|MultiEdit|NotebookEdit)'
824
+ : 'failure-firewall hooks already installed',
825
+ };
826
+ }
827
+ catch (err) {
828
+ return { client, action: 'failed', message: err instanceof Error ? err.message : String(err) };
829
+ }
830
+ }
831
+ /**
832
+ * Remove ONLY the wyrm-guard hook entries (the install.sh --uninstall filter
833
+ * for the guard). Unrelated hooks/settings survive; when nothing of ours is
834
+ * present the file is not rewritten at all (bytes untouched).
835
+ */
836
+ export function removeWyrmGuardHooks(options = {}) {
837
+ const settingsPath = guardSettingsPath(options);
838
+ if (!existsSync(settingsPath))
839
+ return null;
840
+ const client = guardClient(settingsPath);
841
+ try {
842
+ let settings;
843
+ try {
844
+ const parsed = JSON.parse(readFileSync(settingsPath, 'utf-8'));
845
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed))
846
+ throw new Error('not an object');
847
+ settings = parsed;
848
+ }
849
+ catch {
850
+ return { client, action: 'failed', message: `${settingsPath} is not valid JSON — not touching it` };
851
+ }
852
+ const removed = stripWyrmGuardEntries(settings);
853
+ if (!removed)
854
+ return { client, action: 'skipped', message: 'no wyrm-guard hooks installed' };
855
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
856
+ return { client, action: 'configured', message: 'failure-firewall hooks removed' };
857
+ }
858
+ catch (err) {
859
+ return { client, action: 'failed', message: err instanceof Error ? err.message : String(err) };
860
+ }
861
+ }
862
+ /** Whether guard hooks are currently present, and the commands they run. */
863
+ export function wyrmGuardHookStatus(options = {}) {
864
+ const settingsPath = guardSettingsPath(options);
865
+ const commands = [];
866
+ try {
867
+ const settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
868
+ const hooks = (settings.hooks ?? {});
869
+ for (const groups of Object.values(hooks)) {
870
+ if (!Array.isArray(groups))
871
+ continue;
872
+ for (const g of groups) {
873
+ for (const h of g?.hooks ?? []) {
874
+ const cmd = h?.command;
875
+ if (cmd && GUARD_COMMAND_PATTERN.test(cmd))
876
+ commands.push(cmd);
877
+ }
878
+ }
879
+ }
880
+ }
881
+ catch { /* missing/invalid settings → not installed */ }
882
+ return { settingsPath, installed: commands.length > 0, commands };
883
+ }
884
+ /**
885
+ * Auto-configure Wyrm in ALL detected AI clients
886
+ */
887
+ export function autoConfigureAll(wyrmConfig) {
888
+ const config = {
889
+ serverPath: wyrmConfig?.serverPath || findWyrmServerPath(),
890
+ dbPath: wyrmConfig?.dbPath || getDefaultDbPath(),
891
+ httpPort: wyrmConfig?.httpPort,
892
+ };
893
+ // Ensure .wyrm directory exists
894
+ const wyrmDir = join(homedir(), '.wyrm');
895
+ if (!existsSync(wyrmDir)) {
896
+ mkdirSync(wyrmDir, { recursive: true });
897
+ }
898
+ // Save Wyrm's own config for future reference
899
+ saveWyrmMeta(config);
900
+ const clients = detectClients();
901
+ const results = [];
902
+ for (const client of clients) {
903
+ results.push(configureClient(client, config));
904
+ }
905
+ // Install Claude Code CLI auto-capture hooks (no-op if the CLI isn't present).
906
+ const hookResult = installClaudeCodeHooks();
907
+ if (hookResult)
908
+ results.push(hookResult);
909
+ // Zero-config failure firewall: the wyrm-guard PreToolUse hooks (no-op if
910
+ // the CLI isn't present) — npm-global users get the gate without install.sh.
911
+ const guardResult = installWyrmGuardHooks();
912
+ if (guardResult)
913
+ results.push(guardResult);
914
+ return results;
915
+ }
916
+ /**
917
+ * Remove Wyrm from ALL AI clients
918
+ */
919
+ export function removeFromAll() {
920
+ const clients = detectClients();
921
+ const results = [];
922
+ for (const client of clients) {
923
+ results.push(removeFromClient(client));
924
+ }
925
+ const hookResult = removeClaudeCodeHooks();
926
+ if (hookResult)
927
+ results.push(hookResult);
928
+ const guardResult = removeWyrmGuardHooks();
929
+ if (guardResult)
930
+ results.push(guardResult);
931
+ return results;
932
+ }
933
+ /**
934
+ * Configure Wyrm in specific clients only
935
+ */
936
+ export function configureSpecific(clientIds, wyrmConfig) {
937
+ const config = {
938
+ serverPath: wyrmConfig?.serverPath || findWyrmServerPath(),
939
+ dbPath: wyrmConfig?.dbPath || getDefaultDbPath(),
940
+ httpPort: wyrmConfig?.httpPort,
941
+ };
942
+ saveWyrmMeta(config);
943
+ const clients = detectClients();
944
+ const results = [];
945
+ for (const client of clients) {
946
+ if (clientIds.includes(client.id)) {
947
+ results.push(configureClient(client, config));
948
+ }
949
+ }
950
+ return results;
951
+ }
952
+ /**
953
+ * Save Wyrm's meta configuration for auto-updates
954
+ */
955
+ function saveWyrmMeta(config) {
956
+ const metaPath = join(homedir(), '.wyrm', 'wyrm-config.json');
957
+ const clients = detectClients().filter(c => c.configured).map(c => c.id);
958
+ const meta = {
959
+ version: '3.0.0',
960
+ serverPath: config.serverPath,
961
+ dbPath: config.dbPath,
962
+ httpPort: config.httpPort,
963
+ configuredClients: clients,
964
+ lastSetup: new Date().toISOString(),
965
+ autoUpdate: true,
966
+ };
967
+ writeFileSync(metaPath, JSON.stringify(meta, null, 2) + '\n', 'utf-8');
968
+ }
969
+ /**
970
+ * Load Wyrm's meta configuration
971
+ */
972
+ export function loadWyrmMeta() {
973
+ const metaPath = join(homedir(), '.wyrm', 'wyrm-config.json');
974
+ if (!existsSync(metaPath))
975
+ return null;
976
+ try {
977
+ return JSON.parse(readFileSync(metaPath, 'utf-8'));
978
+ }
979
+ catch {
980
+ return null;
981
+ }
982
+ }
983
+ /**
984
+ * Re-configure all previously configured clients (for use after updates)
985
+ */
986
+ export function reconfAll() {
987
+ const meta = loadWyrmMeta();
988
+ if (!meta) {
989
+ return autoConfigureAll();
990
+ }
991
+ return configureSpecific(meta.configuredClients, {
992
+ serverPath: meta.serverPath,
993
+ dbPath: meta.dbPath,
994
+ httpPort: meta.httpPort,
995
+ });
996
+ }
997
+ // ==================== SYSTEM PROMPT INJECTION ====================
998
+ const WYRM_MARKER_START = '<!-- wyrm:start -->';
999
+ const WYRM_MARKER_END = '<!-- wyrm:end -->';
1000
+ export const WYRM_INJECT_BLOCK = `${WYRM_MARKER_START}
1001
+ ## Wyrm Memory + Agent (auto-managed — do not edit this section)
12
1002
 
13
- **Session start** \u2014 call \`wyrm_session_prime\` first to load project context (ground truths, open quests, recent sessions, scaffolds) before responding.
1003
+ **Session start** call \`wyrm_session_prime\` first to load project context (ground truths, open quests, recent sessions, scaffolds) before responding.
14
1004
 
15
- **Set goals declaratively** \u2014 when the operator describes a multi-step or recurring objective ("ship v2", "audit auth weekly", "keep dependencies updated"), call \`wyrm_goal_set\` with a clear \`success_criteria\`. Goals persist across sessions and the agent loop will pursue them.
1005
+ **Set goals declaratively** when the operator describes a multi-step or recurring objective ("ship v2", "audit auth weekly", "keep dependencies updated"), call \`wyrm_goal_set\` with a clear \`success_criteria\`. Goals persist across sessions and the agent loop will pursue them.
16
1006
 
17
- **Bootstrap the autonomous agent** \u2014 if the operator wants ongoing autonomous work, call \`wyrm_agent_init\` once. This starts the \`wyrm-loop\` background daemon that runs OODA iterations on active goals every \`interval_seconds\` (default 10 min). The same call can seed the first goal via \`seed_goal\`. Verify with \`wyrm_agent_status\`; stop with \`wyrm_agent_stop\`.
1007
+ **Bootstrap the autonomous agent** if the operator wants ongoing autonomous work, call \`wyrm_agent_init\` once. This starts the \`wyrm-loop\` background daemon that runs OODA iterations on active goals every \`interval_seconds\` (default 10 min). The same call can seed the first goal via \`seed_goal\`. Verify with \`wyrm_agent_status\`; stop with \`wyrm_agent_stop\`.
18
1008
 
19
- **Counter-pattern check** \u2014 before proposing a fix in an area with prior failures, call \`wyrm_failure_check\` first. Wyrm blocks repeated identical mistakes.
1009
+ **Counter-pattern check** before proposing a fix in an area with prior failures, call \`wyrm_failure_check\` first. Wyrm blocks repeated identical mistakes.
20
1010
 
21
- **Record what failed** \u2014 when a fix doesn't work, call \`wyrm_failure_record\` so future sessions don't repeat it.
1011
+ **Record what failed** when a fix doesn't work, call \`wyrm_failure_record\` so future sessions don't repeat it.
22
1012
 
23
- **Cite memory in answers** \u2014 when surfacing memory, name the source (e.g., "per truth tech_stack.database = PostgreSQL 15" or "session #142 noted X"). Tagged citations help the operator audit trust.
1013
+ **Cite memory in answers** when surfacing memory, name the source (e.g., "per truth tech_stack.database = PostgreSQL 15" or "session #142 noted X"). Tagged citations help the operator audit trust.
24
1014
 
25
- **Attribute Wyrm visibly** \u2014 when you used Wyrm to answer something (loaded a truth, blocked a failure, cited a quest, primed from a past session), say so in plain English. Example: _"Wyrm reminded me that you decided on PostgreSQL last week, so I'm building this for Postgres"_. Operators often don't realise Wyrm is contributing \u2014 naming the contribution makes the value visible. Do NOT silently absorb Wyrm data and present it as your own knowledge.
1015
+ **Attribute Wyrm visibly** when you used Wyrm to answer something (loaded a truth, blocked a failure, cited a quest, primed from a past session), say so in plain English. Example: _"Wyrm reminded me that you decided on PostgreSQL last week, so I'm building this for Postgres"_. Operators often don't realise Wyrm is contributing naming the contribution makes the value visible. Do NOT silently absorb Wyrm data and present it as your own knowledge.
26
1016
 
27
- **Explain Wyrm on request** \u2014 if the operator asks "what does Wyrm do?", "what is Wyrm?", "is Wyrm even working?", or seems confused about Wyrm's role, call \`wyrm_intro\` for a plain-English explanation. For "what has Wyrm done for me?" call \`wyrm_digest\`. These tools speak to humans, not to you.
1017
+ **Explain Wyrm on request** if the operator asks "what does Wyrm do?", "what is Wyrm?", "is Wyrm even working?", or seems confused about Wyrm's role, call \`wyrm_intro\` for a plain-English explanation. For "what has Wyrm done for me?" call \`wyrm_digest\`. These tools speak to humans, not to you.
28
1018
 
29
- **Know what Wyrm can do** \u2014 call \`wyrm_capabilities\` once per session to get the full feature inventory (counter-patterns, ground truths, scaffolds, agent loop, outbound MCP, federation, audit chain, \u2026). Reach for the right tool the first time instead of re-discovering the surface mid-conversation.
1019
+ **Know what Wyrm can do** call \`wyrm_capabilities\` once per session to get the full feature inventory (counter-patterns, ground truths, scaffolds, agent loop, outbound MCP, federation, audit chain, ). Reach for the right tool the first time instead of re-discovering the surface mid-conversation.
30
1020
 
31
- **Keep wyrm-mcp current** \u2014 \`wyrm_check_update\` returns whether a newer version is on npm (cached 24h). When one is, surface it to the operator and offer \`wyrm_self_update\` (with \`confirm:true\`) to upgrade.
1021
+ **Keep wyrm-mcp current** \`wyrm_check_update\` returns whether a newer version is on npm (cached 24h). When one is, surface it to the operator and offer \`wyrm_self_update\` (with \`confirm:true\`) to upgrade.
32
1022
 
33
- **Buddy companion** \u2014 call \`wyrm_buddy\` for warm, data-grounded check-ins. At session start (after \`wyrm_session_prime\`) for long-running work, and when the operator completes a quest or hits a milestone. The buddy speaks in a chosen persona but every claim sources a real DB row \u2014 speak it verbatim, never invent additional encouragement. Auto-federates with other registered MCPs that expose \`*_buddy\` tools.
34
- ${A}`;function me(e,t){const s={injected:[],skipped:[],errors:[]},o=(n,a)=>{try{const d=w(n);if(u(d)||S(d,{recursive:!0}),u(n)){const i=l(n,"utf-8"),p=i.indexOf(T),g=i.indexOf(A);if(p!==-1&&g!==-1&&g>p){const h=i.slice(0,p),k=i.slice(g+A.length);f(n,`${h}${D}${k}`,"utf-8")}else{const h=i.endsWith(`
35
- `)?`
36
- `:`
37
-
38
- `;f(n,`${i}${h}${D}
39
- `,"utf-8")}}else f(n,`${D}
40
- `,"utf-8");s.injected.push(a)}catch(d){s.errors.push(`${a}: ${d}`)}},c=t.length===0?["copilot","cursor"]:t;for(const n of c){const a=n.toLowerCase();if(a==="copilot"||a==="vscode-copilot"){const d=r(e,".github","copilot-instructions.md");o(d,".github/copilot-instructions.md")}else if(a==="cursor"){const d=r(e,".cursor","rules");o(d,".cursor/rules")}else s.skipped.push(n)}return s}function O(e){let t=e.replace(/\/\/.*$/gm,"");return t=t.replace(/\/\*[\s\S]*?\*\//g,""),t=t.replace(/,\s*([\]}])/g,"$1"),JSON.parse(t)}function fe(){const e=v(),t=e.filter(n=>n.detected),s=e.filter(n=>n.configured);let o=`\u{F115D} Wyrm Auto-Configure Status
41
-
42
- `;o+=` Detected: ${t.length}/${e.length} AI clients
43
- `,o+=` Configured: ${s.length}/${t.length} clients
44
-
45
- `;for(const n of e){const a=n.detected?n.configured?" \u25CF":" \u25D0":" \u25CB",d=n.version?` (${n.version})`:"",i=n.detected?n.configured?"configured \u2713":"detected \u2014 not configured":"not found";o+=`${a} ${n.icon} ${n.name}${d}: ${i}
46
- `}const c=N();return c&&(o+=`
47
- Server: ${c.serverPath}
48
- `,o+=` DB: ${c.dbPath}
49
- `,o+=` Last: ${new Date(c.lastSetup).toLocaleString()}
50
- `),o}export{D as WYRM_INJECT_BLOCK,X as autoConfigureAll,L as configureClient,ee as configureSpecific,v as detectClients,R as findWyrmServerPath,E as getDefaultDbPath,fe as getStatusSummary,me as injectSystemPrompt,Q as installClaudeCodeHooks,ie as installClaudeStatusline,N as loadWyrmMeta,ue as reconfAll,Z as removeClaudeCodeHooks,ae as removeClaudeStatusline,de as removeFromAll,G as removeFromClient};
1023
+ **Buddy companion** call \`wyrm_buddy\` for warm, data-grounded check-ins. At session start (after \`wyrm_session_prime\`) for long-running work, and when the operator completes a quest or hits a milestone. The buddy speaks in a chosen persona but every claim sources a real DB row speak it verbatim, never invent additional encouragement. Auto-federates with other registered MCPs that expose \`*_buddy\` tools.
1024
+ ${WYRM_MARKER_END}`;
1025
+ /**
1026
+ * Inject Wyrm's session-prime instruction block into AI client instruction files.
1027
+ * Supports copilot (.github/copilot-instructions.md) and cursor (.cursor/rules).
1028
+ * Uses marker comments to manage the block idempotently (safe to call multiple times).
1029
+ */
1030
+ export function injectSystemPrompt(projectPath, clients) {
1031
+ const result = { injected: [], skipped: [], errors: [] };
1032
+ const writeWithMarker = (filePath, label) => {
1033
+ try {
1034
+ const dir = dirname(filePath);
1035
+ if (!existsSync(dir))
1036
+ mkdirSync(dir, { recursive: true });
1037
+ if (existsSync(filePath)) {
1038
+ const existing = readFileSync(filePath, 'utf-8');
1039
+ const startIdx = existing.indexOf(WYRM_MARKER_START);
1040
+ const endIdx = existing.indexOf(WYRM_MARKER_END);
1041
+ if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
1042
+ // Replace existing block
1043
+ const before = existing.slice(0, startIdx);
1044
+ const after = existing.slice(endIdx + WYRM_MARKER_END.length);
1045
+ writeFileSync(filePath, `${before}${WYRM_INJECT_BLOCK}${after}`, 'utf-8');
1046
+ }
1047
+ else {
1048
+ // Append block (ensure newline separation)
1049
+ const sep = existing.endsWith('\n') ? '\n' : '\n\n';
1050
+ writeFileSync(filePath, `${existing}${sep}${WYRM_INJECT_BLOCK}\n`, 'utf-8');
1051
+ }
1052
+ }
1053
+ else {
1054
+ writeFileSync(filePath, `${WYRM_INJECT_BLOCK}\n`, 'utf-8');
1055
+ }
1056
+ result.injected.push(label);
1057
+ }
1058
+ catch (e) {
1059
+ result.errors.push(`${label}: ${e}`);
1060
+ }
1061
+ };
1062
+ const allClients = clients.length === 0
1063
+ ? ['copilot', 'cursor']
1064
+ : clients;
1065
+ for (const client of allClients) {
1066
+ const lc = client.toLowerCase();
1067
+ if (lc === 'copilot' || lc === 'vscode-copilot') {
1068
+ const filePath = join(projectPath, '.github', 'copilot-instructions.md');
1069
+ writeWithMarker(filePath, '.github/copilot-instructions.md');
1070
+ }
1071
+ else if (lc === 'cursor') {
1072
+ const filePath = join(projectPath, '.cursor', 'rules');
1073
+ writeWithMarker(filePath, '.cursor/rules');
1074
+ }
1075
+ else {
1076
+ result.skipped.push(client);
1077
+ }
1078
+ }
1079
+ return result;
1080
+ }
1081
+ // ==================== UTILITIES ====================
1082
+ /**
1083
+ * Parse JSON with comments (JSONC) - handles VS Code settings files
1084
+ */
1085
+ function parseJsonWithComments(text) {
1086
+ // Strip single-line comments
1087
+ let cleaned = text.replace(/\/\/.*$/gm, '');
1088
+ // Strip multi-line comments
1089
+ cleaned = cleaned.replace(/\/\*[\s\S]*?\*\//g, '');
1090
+ // Handle trailing commas (common in VS Code settings)
1091
+ cleaned = cleaned.replace(/,\s*([\]}])/g, '$1');
1092
+ return JSON.parse(cleaned);
1093
+ }
1094
+ /**
1095
+ * Get a friendly status summary of all AI clients
1096
+ */
1097
+ export function getStatusSummary() {
1098
+ const clients = detectClients();
1099
+ const detected = clients.filter(c => c.detected);
1100
+ const configured = clients.filter(c => c.configured);
1101
+ let summary = `󱅝 Wyrm Auto-Configure Status\n\n`;
1102
+ summary += ` Detected: ${detected.length}/${clients.length} AI clients\n`;
1103
+ summary += ` Configured: ${configured.length}/${detected.length} clients\n\n`;
1104
+ for (const client of clients) {
1105
+ const status = !client.detected
1106
+ ? ' ○' // Not installed
1107
+ : client.configured
1108
+ ? ' ●' // Configured
1109
+ : ' ◐'; // Installed but not configured
1110
+ const versionStr = client.version ? ` (${client.version})` : '';
1111
+ const statusLabel = !client.detected
1112
+ ? 'not found'
1113
+ : client.configured
1114
+ ? 'configured ✓'
1115
+ : 'detected — not configured';
1116
+ summary += `${status} ${client.icon} ${client.name}${versionStr}: ${statusLabel}\n`;
1117
+ }
1118
+ const meta = loadWyrmMeta();
1119
+ if (meta) {
1120
+ summary += `\n Server: ${meta.serverPath}\n`;
1121
+ summary += ` DB: ${meta.dbPath}\n`;
1122
+ summary += ` Last: ${new Date(meta.lastSetup).toLocaleString()}\n`;
1123
+ }
1124
+ return summary;
1125
+ }
1126
+ //# sourceMappingURL=autoconfig.js.map