context-mode 1.0.131 → 1.0.133
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.openclaw-plugin/openclaw.plugin.json +1 -1
- package/.openclaw-plugin/package.json +1 -1
- package/README.md +26 -15
- package/build/cli.js +32 -0
- package/build/lifecycle.d.ts +51 -2
- package/build/lifecycle.js +67 -3
- package/build/server.js +118 -16
- package/build/session/analytics.d.ts +38 -0
- package/build/session/analytics.js +58 -1
- package/build/session/extract.d.ts +7 -0
- package/build/session/extract.js +22 -6
- package/build/store.d.ts +17 -2
- package/build/store.js +17 -13
- package/build/util/sibling-mcp.d.ts +40 -0
- package/build/util/sibling-mcp.js +116 -11
- package/cli.bundle.mjs +174 -165
- package/configs/jetbrains-copilot/mcp.json +1 -2
- package/configs/vscode-copilot/mcp.json +1 -2
- package/hooks/session-extract.bundle.mjs +2 -2
- package/hooks/session-loaders.mjs +15 -2
- package/openclaw.plugin.json +1 -1
- package/package.json +5 -2
- package/scripts/heal-better-sqlite3.mjs +99 -2
- package/scripts/postinstall.mjs +58 -0
- package/server.bundle.mjs +106 -104
- package/skills/context-mode/SKILL.md +1 -0
- package/skills/context-mode/references/anti-patterns.md +26 -0
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
function s(t){return t==null?"":String(t)}function
|
|
2
|
-
response: ${s(
|
|
1
|
+
function s(t){return t==null?"":String(t)}function y(t){return t==null?"":typeof t=="string"?t:JSON.stringify(t)}function d(t){let e=String(t.tool_response??""),n=t.tool_output?.isError===!0||t.tool_output?.is_error===!0;return t.tool_name==="Bash"&&/exit code [1-9]|error:|Error:|FAIL|failed/i.test(e)||n}function b(t){if(!t)return[];let e=[];for(let o of t.split(/\r?\n/)){if(o.startsWith("*** Add File: ")){e.push({path:o.slice(14).trim(),type:"file_write"});continue}if(o.startsWith("*** Update File: ")){e.push({path:o.slice(17).trim(),type:"file_edit"});continue}if(o.startsWith("*** Delete File: ")){e.push({path:o.slice(17).trim(),type:"file_edit"});continue}o.startsWith("*** Move to: ")&&e.push({path:o.slice(13).trim(),type:"file_edit"})}let n=new Set;return e.filter(o=>{if(!o.path)return!1;let r=`${o.type}:${o.path}`;return n.has(r)?!1:(n.add(r),!0)})}function h(t){return/(?:^|[/\\])\.claude[/\\]plans[/\\]/.test(t)}function E(t){let{tool_name:e,tool_input:n,tool_response:o}=t,r=[];if(e==="Read"){let i=String(n.file_path??"");return(/(?:CLAUDE|AGENTS(?:\.override)?|GEMINI|QWEN|KIRO)\.md$/i.test(i)||/\/copilot-instructions\.md$/i.test(i)||/\/context-mode\.mdc$/i.test(i)||/\.claude[\\/]/i.test(i)||/[\\/]memor(?:y|ies)[\\/][^\\/]+\.md$/i.test(i))&&(r.push({type:"rule",category:"rule",data:s(i),priority:1}),o&&o.length>0&&r.push({type:"rule_content",category:"rule",data:s(o),priority:1})),r.push({type:"file_read",category:"file",data:s(i),priority:1}),r}if(e==="Edit"){let i=String(n.file_path??"");return r.push({type:"file_edit",category:"file",data:s(i),priority:1}),r}if(e==="NotebookEdit"){let i=String(n.notebook_path??"");return r.push({type:"file_edit",category:"file",data:s(i),priority:1}),r}if(e==="Write"){let i=String(n.file_path??"");return r.push({type:"file_write",category:"file",data:s(i),priority:1}),r}if(e==="apply_patch"){if(d(t))return[];let i=b(String(n.command??n.patch??""));for(let a of i)r.push({type:a.type,category:"file",data:s(a.path),priority:1});return r}if(e==="Glob"){let i=String(n.pattern??"");return r.push({type:"file_glob",category:"file",data:s(i),priority:3}),r}if(e==="Grep"){let i=String(n.pattern??""),a=String(n.path??"");return r.push({type:"file_search",category:"file",data:s(`${i} in ${a}`),priority:3}),r}return r}function k(t){if(t.tool_name!=="Bash")return[];let n=String(t.tool_input.command??"").match(/\bcd\s+("([^"]+)"|'([^']+)'|(\S+))/);if(!n)return[];let o=n[2]??n[3]??n[4]??"";return[{type:"cwd",category:"cwd",data:s(o),priority:2}]}function v(t){let{tool_response:e}=t,n=String(e??"");return d(t)?[{type:"error_tool",category:"error",data:s(n),priority:2}]:[]}var A=[{pattern:/\bgit\s+checkout\b/,operation:"branch"},{pattern:/\bgit\s+commit\b/,operation:"commit"},{pattern:/\bgit\s+merge\s+\S+/,operation:"merge"},{pattern:/\bgit\s+rebase\b/,operation:"rebase"},{pattern:/\bgit\s+stash\b/,operation:"stash"},{pattern:/\bgit\s+push\b/,operation:"push"},{pattern:/\bgit\s+pull\b/,operation:"pull"},{pattern:/\bgit\s+log\b/,operation:"log"},{pattern:/\bgit\s+diff\b/,operation:"diff"},{pattern:/\bgit\s+status\b/,operation:"status"},{pattern:/\bgit\s+branch\b/,operation:"branch"},{pattern:/\bgit\s+reset\b/,operation:"reset"},{pattern:/\bgit\s+add\b/,operation:"add"},{pattern:/\bgit\s+cherry-pick\b/,operation:"cherry-pick"},{pattern:/\bgit\s+tag\b/,operation:"tag"},{pattern:/\bgit\s+fetch\b/,operation:"fetch"},{pattern:/\bgit\s+clone\b/,operation:"clone"},{pattern:/\bgit\s+worktree\b/,operation:"worktree"}];function R(t){if(t.tool_name!=="Bash")return[];let e=String(t.tool_input.command??""),n=A.find(o=>o.pattern.test(e));return n?[{type:"git",category:"git",data:s(n.operation),priority:2}]:[]}function T(t){return new Set(["TodoWrite","TaskCreate","TaskUpdate"]).has(t.tool_name)?[{type:t.tool_name==="TaskUpdate"?"task_update":t.tool_name==="TaskCreate"?"task_create":"task",category:"task",data:s(JSON.stringify(t.tool_input)),priority:1}]:[]}function x(t){if(t.tool_name==="EnterPlanMode")return[{type:"plan_enter",category:"plan",data:"entered plan mode",priority:2}];if(t.tool_name==="ExitPlanMode"){let e=[],n=t.tool_input.allowedPrompts,o=Array.isArray(n)&&n.length>0?`exited plan mode (allowed: ${y(n.map(i=>typeof i=="object"&&i!==null&&"prompt"in i?String(i.prompt):String(i)).join(", "))})`:"exited plan mode";e.push({type:"plan_exit",category:"plan",data:s(o),priority:2});let r=String(t.tool_response??"").toLowerCase();return r.includes("approved")||r.includes("approve")?e.push({type:"plan_approved",category:"plan",data:"plan approved by user",priority:1}):(r.includes("rejected")||r.includes("decline")||r.includes("denied"))&&e.push({type:"plan_rejected",category:"plan",data:s(`plan rejected: ${t.tool_response??""}`),priority:2}),e}if(t.tool_name==="Write"||t.tool_name==="Edit"){let e=String(t.tool_input.file_path??"");if(h(e))return[{type:"plan_file_write",category:"plan",data:s(`plan file: ${e.split(/[/\\]/).pop()??e}`),priority:2}]}return t.tool_name==="apply_patch"?d(t)?[]:b(String(t.tool_input.command??t.tool_input.patch??"")).filter(n=>h(n.path)).map(n=>({type:"plan_file_write",category:"plan",data:s(`plan file: ${n.path.split(/[/\\]/).pop()??n.path}`),priority:2})):[]}var I=[/\bsource\s+\S*activate\b/,/\bexport\s+\w+=/,/\bnvm\s+use\b/,/\bpyenv\s+(shell|local|global)\b/,/\bconda\s+activate\b/,/\brbenv\s+(shell|local|global)\b/,/\bnpm\s+install\b/,/\bnpm\s+ci\b/,/\bpip\s+install\b/,/\bbun\s+install\b/,/\byarn\s+(add|install)\b/,/\bpnpm\s+(add|install)\b/,/\bcargo\s+(install|add)\b/,/\bgo\s+(install|get)\b/,/\brustup\b/,/\basdf\b/,/\bvolta\b/,/\bdeno\s+install\b/];function w(t){if(t.tool_name!=="Bash")return[];let e=String(t.tool_input.command??"");if(!I.some(r=>r.test(e)))return[];let o=e.replace(/\bexport\s+(\w+)=\S*/g,"export $1=***");return[{type:"env",category:"env",data:s(o),priority:2}]}function N(t){if(t.tool_name!=="Skill")return[];let e=String(t.tool_input.skill??"");return[{type:"skill",category:"skill",data:s(e),priority:2}]}function C(t){if(!t.tool_response?.includes("Error")&&!t.tool_output?.isError)return[];let e=String(t.tool_response||""),n=[/not supported/i,/cannot/i,/does not support/i,/FAIL/i,/refused/i,/permission denied/i,/incompatible/i];for(let o of n){let r=e.match(o);if(r){let i=e.toLowerCase().indexOf(r[0].toLowerCase()),a=e.slice(Math.max(0,i-50),Math.min(e.length,i+200)).trim();return[{type:"constraint_discovered",category:"constraint",data:s(a),priority:2}]}}return[]}function H(t){if(t.tool_name!=="Agent")return[];let e=s(String(t.tool_input.prompt??t.tool_input.description??"")),n=t.tool_response?s(String(t.tool_response)):"",o=n.length>0;return[{type:o?"subagent_completed":"subagent_launched",category:"subagent",data:s(o?`[completed] ${e} \u2192 ${n}`:`[launched] ${e}`),priority:o?2:3}]}function L(t){let{tool_name:e,tool_input:n,tool_response:o}=t;if(!e.startsWith("mcp__"))return[];let r=e.split("__"),i=r[r.length-1]||e,a=Object.values(n).find(S=>typeof S=="string"),u=a?`: ${s(String(a))}`:"",c=o&&o.length>0?`
|
|
2
|
+
response: ${s(o)}`:"";return[{type:"mcp",category:"mcp",data:s(`${i}${u}${c}`),priority:3}]}var $=2048;function P(t,e){if(Buffer.byteLength(t,"utf8")<=e)return{value:t,truncated:!1};let n=Buffer.from(t,"utf8"),o=e;for(;o>0&&(n[o]&192)===128;)o--;return{value:n.subarray(0,o).toString("utf8"),truncated:!0}}var O=/(authorization|auth_token|access_token|refresh_token|bearer|token|secret|password|passwd|pwd|api[-_]?key|apikey|cookie|set-cookie|signature|private[-_]?key|client[-_]?secret|x[-_]?api[-_]?key)/i,M="[REDACTED]";function g(t,e=new WeakSet){if(t==null||typeof t!="object")return t;if(e.has(t))return"[CIRCULAR]";e.add(t);let n;if(Array.isArray(t))n=t.map(o=>g(o,e));else{let o={};for(let[r,i]of Object.entries(t))O.test(r)?o[r]=M:o[r]=g(i,e);n=o}return e.delete(t),n}function B(t){let{tool_name:e,tool_input:n}=t;if(!e.startsWith("mcp__"))return[];let o=g(n??{}),r;try{r=JSON.stringify(o)}catch{r="{}"}let{value:i,truncated:a}=P(r,$),u=a?`{"tool_name":${JSON.stringify(e)},"params_raw":${JSON.stringify(i)},"truncated":true}`:`{"tool_name":${JSON.stringify(e)},"params":${i}}`;return[{type:"mcp_tool_call",category:"mcp_tool_call",data:s(u),priority:4}]}function W(t){if(t.tool_name!=="AskUserQuestion")return[];let e=t.tool_input.questions,n=Array.isArray(e)&&e.length>0?String(e[0].question??""):"",o=s(String(t.tool_response??"")),r=n?`Q: ${s(n)} \u2192 A: ${o}`:`answer: ${o}`;return[{type:"decision_question",category:"decision",data:s(r),priority:2}]}function F(t){if(t.tool_name!=="Agent")return[];if(!t.tool_response||t.tool_response.length===0)return[];let e=t.tool_response.length>500?t.tool_response.slice(0,500):t.tool_response;return[{type:"agent_finding",category:"agent-finding",data:s(e),priority:2}]}function j(t){let e=[y(t.tool_input),s(t.tool_response)].join(" ");if(e.length===0)return[];let n=new Set,o=e.match(/https?:\/\/[^\s)]+/g);if(o)for(let c of o)c=c.replace(/["'})\],;.]+$/,""),/localhost|127\.0\.0\.1/i.test(c)||n.add(c);let r=e.match(/(?<!\w)#(\d+)/g);if(r)for(let c of r)n.add(c);if(n.size===0)return[];let i,a=s(t.tool_response).match(/Fetched and indexed[^\(]*\(([\d.]+)\s*KB\)/i);if(a){let c=Number(a[1]);Number.isFinite(c)&&c>0&&(i=Math.round(c*1024))}let u={type:"external_ref",category:"external-ref",data:s(Array.from(n).join(", ")),priority:3};return i!==void 0&&(u.bytes_avoided=i),[u]}function D(t){if(t.tool_name!=="EnterWorktree")return[];let e=String(t.tool_input.name??"unnamed");return[{type:"worktree",category:"env",data:s(`entered worktree: ${e}`),priority:2}]}var m=/[,;,;、،]/u,U=15,K=500;function G(t){if(f.test(t)||!_.test(t)||!m.test(t))return!1;let e=[...t].length;return e>=U&&e<=K}function J(t){let e=t.trim();return G(e)?[{type:"decision",category:"decision",data:s(t),priority:2}]:[]}var q=8,z=120,Q=new RegExp("\\p{L}+\\s+\\p{L}+","u"),V=new RegExp("\\p{L}{6,}","u");function X(t){let e=t.split(/[.!\n。!]/u)[0].trim();if(f.test(e)||m.test(e)||!_.test(e))return!1;let n=[...e].length;return n<q||n>z?!1:Q.test(e)||V.test(e)}function Y(t){let e=t.trim();return X(e)?[{type:"role",category:"role",data:s(t),priority:3}]:[]}var f=/[??؟¿]/u,_=new RegExp("\\p{L}","u"),Z=60;function tt(t){if(f.test(t)||!_.test(t))return!1;let e=[...t].length;return e>0&&e<Z}function et(t){let e=t.trim();if(!e)return[];let n;return f.test(e)?n="investigate":tt(e)&&(n="implement"),n?[{type:"intent",category:"intent",data:s(n),priority:4}]:[]}var nt=/(?:\bError\s*:|\bException\s*:|\bTraceback\b|\bat\s+\S+\s*\([^)]*:\d+:\d+\))/u,ot=/[✓✔✅☑🎉]/u,rt=/^\s*(?:fixed|resolved)\s*:/iu;function it(t){let e=[];return ot.test(t)||rt.test(t)?(e.push({type:"blocker_resolved",category:"blocked-on",data:s(t),priority:2}),e):(nt.test(t)&&e.push({type:"blocker",category:"blocked-on",data:s(t),priority:2}),e)}function st(t){return t.length<=1024?[]:[{type:"data",category:"data",data:s(t),priority:4}]}var l=null;function at(t){let{tool_name:e,tool_response:n}=t,o=String(n??"");if(d(t))return l={tool:e,error:o.slice(0,200),callsSince:0},[];if(!l)return[];if(l.callsSince++,l.callsSince>10)return l=null,[];if(!!d(t))return[];let i=e===l.tool,a=l.tool==="Read"&&(e==="Edit"||e==="Write"||e==="apply_patch");if(i||a){let u={type:"error_resolved",category:"error-resolution",data:s(`Error in ${l.tool}: ${l.error} \u2192 Fixed`),priority:2};return l=null,[u]}return[]}function dt(){l=null}var p=[];function ct(t){return`${t.length}:${t.slice(0,20)}`}function lt(t){let{tool_name:e,tool_input:n}=t,o=ct(JSON.stringify(n).slice(0,200));if(p.push({tool:e,inputHash:o}),p.length>50&&p.splice(0,p.length-50),p.length<3)return[];let r=0;for(let i=p.length-1;i>=0&&(p[i].tool===e&&p[i].inputHash===o);i--)r++;return r>=3?(p.splice(p.length-r),[{type:"retry_detected",category:"iteration-loop",data:s(`${e} called ${r} times with similar input`),priority:2}]):[]}function ft(){p.length=0}var pt={run_shell_command:"Bash",read_file:"Read",read_many_files:"Read",grep_search:"Grep",search_file_content:"Grep",web_fetch:"WebFetch",write_file:"Write",edit:"Edit",glob:"Glob",todo_write:"TodoWrite",ask_user_question:"AskUserQuestion",list_directory:"LS",save_memory:"Memory",skill:"Skill",exit_plan_mode:"ExitPlanMode",agent:"Agent",bash:"Bash",view:"Read",grep:"Grep",fetch:"WebFetch",shell:"Bash",shell_command:"Bash",exec_command:"Bash","container.exec":"Bash",local_shell:"Bash",grep_files:"Grep"};function ut(t){let e=pt[t.tool_name];return!e||e===t.tool_name?t:{...t,tool_name:e}}function gt(t){try{let e=ut(t),n=[];return n.push(...E(e)),n.push(...k(e)),n.push(...v(e)),n.push(...R(e)),n.push(...w(e)),n.push(...T(e)),n.push(...x(e)),n.push(...N(e)),n.push(...H(e)),n.push(...L(e)),n.push(...B(e)),n.push(...W(e)),n.push(...C(e)),n.push(...D(e)),n.push(...F(e)),n.push(...j(e)),n.push(...at(e)),n.push(...lt(e)),n}catch{return[]}}function _t(t){try{let e=[];return e.push(...J(t)),e.push(...Y(t)),e.push(...et(t)),e.push(...it(t)),e.push(...st(t)),e}catch{return[]}}export{gt as extractEvents,_t as extractUserEvents,dt as resetErrorResolutionState,ft as resetIterationLoopState};
|
|
@@ -74,13 +74,26 @@ export function attributeAndInsertEvents(db, sessionId, events, input, projectDi
|
|
|
74
74
|
workspaceRoots: Array.isArray(input.workspace_roots) ? input.workspace_roots : [],
|
|
75
75
|
lastKnownProjectDir,
|
|
76
76
|
});
|
|
77
|
+
// Build a parallel bytesList from event-level bytes_avoided (currently
|
|
78
|
+
// populated by external_ref's ctx_fetch_and_index preamble parser). When
|
|
79
|
+
// no event carries a positive value we leave bytesList undefined so
|
|
80
|
+
// SessionDB falls back to its 0-default for bytes_avoided/bytes_returned
|
|
81
|
+
// — preserves backward compat with older callers / tests.
|
|
82
|
+
let bytesList;
|
|
83
|
+
if (events.some((e) => typeof e?.bytes_avoided === "number" && e.bytes_avoided > 0)) {
|
|
84
|
+
bytesList = events.map((e) =>
|
|
85
|
+
typeof e?.bytes_avoided === "number" && e.bytes_avoided > 0
|
|
86
|
+
? { bytesAvoided: e.bytes_avoided }
|
|
87
|
+
: undefined,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
77
90
|
// Prefer bulk path (single transaction = single WAL commit). Falls back
|
|
78
91
|
// to per-event insert for older SessionDB instances that lack bulkInsertEvents.
|
|
79
92
|
if (typeof db.bulkInsertEvents === "function") {
|
|
80
|
-
db.bulkInsertEvents(sessionId, events, hookName, attributions);
|
|
93
|
+
db.bulkInsertEvents(sessionId, events, hookName, attributions, bytesList);
|
|
81
94
|
} else {
|
|
82
95
|
for (let i = 0; i < events.length; i++) {
|
|
83
|
-
db.insertEvent(sessionId, events[i], hookName, attributions[i]);
|
|
96
|
+
db.insertEvent(sessionId, events[i], hookName, attributions[i], bytesList?.[i]);
|
|
84
97
|
}
|
|
85
98
|
}
|
|
86
99
|
return attributions;
|
package/openclaw.plugin.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "Context Mode",
|
|
4
4
|
"kind": "tool",
|
|
5
5
|
"description": "OpenClaw plugin that saves 98% of your context window. Sandboxed code execution in 11 languages, FTS5 knowledge base with BM25 ranking, and intent-driven search.",
|
|
6
|
-
"version": "1.0.
|
|
6
|
+
"version": "1.0.133",
|
|
7
7
|
"sandbox": {
|
|
8
8
|
"mode": "permissive",
|
|
9
9
|
"filesystem_access": "full",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "context-mode",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.133",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "MCP plugin that saves 98% of your context window. Works with Claude Code, Gemini CLI, VS Code Copilot, OpenCode, and Codex CLI. Sandboxed code execution, FTS5 knowledge base, and intent-driven search.",
|
|
6
6
|
"author": "Mert Koseoğlu",
|
|
@@ -124,5 +124,8 @@
|
|
|
124
124
|
"typescript": "^5.7.0",
|
|
125
125
|
"vitest": "^4.0.18"
|
|
126
126
|
},
|
|
127
|
-
"packageManager": "pnpm@10.23.0+sha512.21c4e5698002ade97e4efe8b8b4a89a8de3c85a37919f957e7a0f30f38fbc5bbdd05980ffe29179b2fb6e6e691242e098d945d1601772cad0fef5fb6411e2a4b"
|
|
127
|
+
"packageManager": "pnpm@10.23.0+sha512.21c4e5698002ade97e4efe8b8b4a89a8de3c85a37919f957e7a0f30f38fbc5bbdd05980ffe29179b2fb6e6e691242e098d945d1601772cad0fef5fb6411e2a4b",
|
|
128
|
+
"engines": {
|
|
129
|
+
"node": ">=22.5.0"
|
|
130
|
+
}
|
|
128
131
|
}
|
|
@@ -30,6 +30,15 @@
|
|
|
30
30
|
*
|
|
31
31
|
* @see https://github.com/mksglu/context-mode/issues/408
|
|
32
32
|
* @see https://github.com/mksglu/context-mode/issues/533
|
|
33
|
+
*
|
|
34
|
+
* Windows VS 2026+ detection:
|
|
35
|
+
* node-gyp has a hardcoded internal-version→year map. VS 2026 (internal
|
|
36
|
+
* major 18) was absent from older node-gyp builds, causing "unknown version"
|
|
37
|
+
* failures on machines that only have VS 2026 installed. Rather than
|
|
38
|
+
* extending the map (which would break again with VS 2029, etc.), we query
|
|
39
|
+
* vswhere's `displayName` property ("Visual Studio Community 2026") and
|
|
40
|
+
* extract the 4-digit year with a regex. `catalog_productLineVersion` is NOT
|
|
41
|
+
* used — it returns the internal major ("18") on VS 2026, not the year.
|
|
33
42
|
*/
|
|
34
43
|
|
|
35
44
|
import { existsSync as fsExistsSync } from "node:fs";
|
|
@@ -137,11 +146,90 @@ function isCondaActive(env = process.env) {
|
|
|
137
146
|
return pathEntries.some((dir) => !isSafePythonPath(dir + "/python3"));
|
|
138
147
|
}
|
|
139
148
|
|
|
149
|
+
/**
|
|
150
|
+
* Detect the installed Visual Studio year string via vswhere.exe.
|
|
151
|
+
*
|
|
152
|
+
* Uses the `displayName` property (e.g. "Visual Studio Community 2026")
|
|
153
|
+
* and extracts the 4-digit year with a regex. This is more reliable than
|
|
154
|
+
* `catalog_productLineVersion`, which returns the internal major version
|
|
155
|
+
* number ("18") on VS 2026 instead of the year — making it useless as a
|
|
156
|
+
* direct msvs_version value without a mapping table.
|
|
157
|
+
*
|
|
158
|
+
* `displayName` has consistently included the branded year across every
|
|
159
|
+
* VS release (2017, 2019, 2022, 2026) and will continue to do so because
|
|
160
|
+
* it is the user-visible product name Microsoft ships.
|
|
161
|
+
*
|
|
162
|
+
* Dependency-injected so unit tests can exercise all branches without
|
|
163
|
+
* spawning a real process or requiring vswhere to be present on the host.
|
|
164
|
+
*
|
|
165
|
+
* Returns null on non-Windows, when vswhere is absent, or on any error.
|
|
166
|
+
*
|
|
167
|
+
* Timeout: 15s. Cold-disk vswhere queries on HDD-backed Windows CI runners
|
|
168
|
+
* with multiple VS installs have been observed to exceed the previous 5s
|
|
169
|
+
* budget (see ARCH-REVIEW #571 Part B). 15s comfortably covers slow-disk
|
|
170
|
+
* scenarios without freezing /ctx-upgrade.
|
|
171
|
+
*
|
|
172
|
+
* Year sanity cap: the regex matches any 21st-century 4-digit year, but
|
|
173
|
+
* we additionally reject anything > currentYear+5. Corrupted vswhere
|
|
174
|
+
* output or a future MS rebrand could surface a bogus "2099"; passing
|
|
175
|
+
* that through to `npm_config_msvs_version` would fail node-gyp
|
|
176
|
+
* silently. Cap-and-null lets the caller fall back to node-gyp's own
|
|
177
|
+
* detection and we log a single stderr breadcrumb for support triage.
|
|
178
|
+
*
|
|
179
|
+
* @param {object} [deps]
|
|
180
|
+
* @param {string} [deps.platform] - process.platform override
|
|
181
|
+
* @param {(p: string) => boolean} [deps.existsSync] - fs probe override
|
|
182
|
+
* @param {(cmd: string, opts: object) => string} [deps.exec] - execSync override
|
|
183
|
+
* @param {() => number} [deps.now] - clock override for sanity cap (test seam)
|
|
184
|
+
* @returns {string | null}
|
|
185
|
+
*/
|
|
186
|
+
export function detectWindowsVsYear({
|
|
187
|
+
platform = process.platform,
|
|
188
|
+
existsSync = fsExistsSync,
|
|
189
|
+
exec = execSync,
|
|
190
|
+
now = () => new Date().getFullYear(),
|
|
191
|
+
} = {}) {
|
|
192
|
+
if (platform !== "win32") return null;
|
|
193
|
+
try {
|
|
194
|
+
const vswhere =
|
|
195
|
+
"C:\\Program Files (x86)\\Microsoft Visual Studio\\Installer\\vswhere.exe";
|
|
196
|
+
if (!existsSync(vswhere)) return null;
|
|
197
|
+
const displayName = exec(
|
|
198
|
+
`"${vswhere}" -latest -property displayName`,
|
|
199
|
+
{ encoding: "utf-8", stdio: "pipe", timeout: 15000 },
|
|
200
|
+
).trim();
|
|
201
|
+
// "Visual Studio Community 2026" → "2026"
|
|
202
|
+
const match = displayName.match(/\b(20\d{2})\b/);
|
|
203
|
+
if (!match) return null;
|
|
204
|
+
const year = Number(match[1]);
|
|
205
|
+
const ceiling = now() + 5;
|
|
206
|
+
if (year > ceiling) {
|
|
207
|
+
// Fail LOUD, not silent: poisoning npm_config_msvs_version with
|
|
208
|
+
// a bogus year would manifest as opaque node-gyp errors deep in
|
|
209
|
+
// the rebuild. Surface a breadcrumb and return null so the caller
|
|
210
|
+
// falls back to node-gyp's own version detection.
|
|
211
|
+
try {
|
|
212
|
+
process.stderr.write(
|
|
213
|
+
`[context-mode] vswhere displayName reports VS year ${year} ` +
|
|
214
|
+
`(> ${ceiling}); ignoring as likely corrupted output. ` +
|
|
215
|
+
`Falling back to node-gyp default detection.\n`,
|
|
216
|
+
);
|
|
217
|
+
} catch { /* stderr unavailable — proceed silently */ }
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
return match[1];
|
|
221
|
+
} catch {
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
140
226
|
/**
|
|
141
227
|
* Build the child-process env for an npm/node-gyp invocation. Pins
|
|
142
228
|
* PYTHON + npm_config_python to the resolved safe interpreter, strips
|
|
143
|
-
* CONDA_* keys,
|
|
144
|
-
*
|
|
229
|
+
* CONDA_* keys, prepends /usr/bin to PATH on darwin so any downstream
|
|
230
|
+
* PATH-based python3 lookup resolves to system python, and on Windows
|
|
231
|
+
* injects npm_config_msvs_version from vswhere so node-gyp finds VS
|
|
232
|
+
* regardless of which major version is installed.
|
|
145
233
|
*
|
|
146
234
|
* @param {string | null} safePython - output of resolveSafePython()
|
|
147
235
|
* @param {NodeJS.ProcessEnv} [base] - starting env (defaults to process.env)
|
|
@@ -171,6 +259,15 @@ function buildSafeEnv(safePython, base = process.env) {
|
|
|
171
259
|
env.PATH = "/usr/bin:" + parts.filter((p) => p !== "/usr/bin").join(":");
|
|
172
260
|
}
|
|
173
261
|
}
|
|
262
|
+
// ── Windows: pin npm_config_msvs_version via vswhere (#VS2026) ───────
|
|
263
|
+
// node-gyp defaults to VS 2022. On machines that only have VS 2026 (or
|
|
264
|
+
// a later release) installed the build fails with "unknown version" or
|
|
265
|
+
// "msvs_version does not match". Querying vswhere directly gives us the
|
|
266
|
+
// correct year string without a hardcoded mapping table.
|
|
267
|
+
if (process.platform === "win32" && !env.npm_config_msvs_version) {
|
|
268
|
+
const year = detectWindowsVsYear();
|
|
269
|
+
if (year) env.npm_config_msvs_version = year;
|
|
270
|
+
}
|
|
174
271
|
return env;
|
|
175
272
|
}
|
|
176
273
|
|
package/scripts/postinstall.mjs
CHANGED
|
@@ -19,6 +19,64 @@ import { healInstalledPlugins, healSettingsEnabledPlugins, healPluginJsonMcpServ
|
|
|
19
19
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
20
20
|
const pkgRoot = resolve(__dirname, "..");
|
|
21
21
|
|
|
22
|
+
// ── -2. Issue #564 — Linux SIGSEGV class hard-fail (v1.0.132) ────────
|
|
23
|
+
// On Linux + Node < 22.5 + no Bun, better-sqlite3's native addon is
|
|
24
|
+
// vulnerable to V8 calling `madvise(MADV_DONTNEED)` on memory ranges
|
|
25
|
+
// that overlap the addon's `.got.plt` section, corrupting resolved
|
|
26
|
+
// symbol addresses and causing sporadic SIGSEGV (1-4/hour) — see
|
|
27
|
+
// https://github.com/nodejs/node/issues/62515 and our internal #564.
|
|
28
|
+
//
|
|
29
|
+
// node:sqlite (built-in, no native addon, no .got.plt to corrupt) ships
|
|
30
|
+
// from Node 22.5 onward — that is the contract `hasModernSqlite()` in
|
|
31
|
+
// src/db-base.ts encodes. Six prior fixes (#228, #331, #461, #540,
|
|
32
|
+
// #551, #556) silently assumed users had Node >= 22.5 on Linux; #564
|
|
33
|
+
// is the second confirmed report (after #556) of the same SIGSEGV
|
|
34
|
+
// class on Node 20.
|
|
35
|
+
//
|
|
36
|
+
// The architect mandate for v1.0.132 is HARD-FAIL, not warn-then-
|
|
37
|
+
// degrade. `engines.node >= 22.5.0` in package.json is cosmetic under
|
|
38
|
+
// the default npm `engine-strict=false`, so the contract has to be
|
|
39
|
+
// enforced HERE — preinstall/postinstall is the only place that can
|
|
40
|
+
// `process.exit(1)` across npm/pnpm/yarn.
|
|
41
|
+
//
|
|
42
|
+
// Linux + Bun is allowed through (bun:sqlite sidesteps better-sqlite3
|
|
43
|
+
// entirely). Non-Linux platforms are unaffected by the madvise bug
|
|
44
|
+
// and pass through unchanged.
|
|
45
|
+
{
|
|
46
|
+
const isLinux = process.platform === "linux";
|
|
47
|
+
const hasBun =
|
|
48
|
+
typeof globalThis.Bun !== "undefined" ||
|
|
49
|
+
typeof process.versions.bun === "string";
|
|
50
|
+
const [majStr, minStr] = (process.versions.node ?? "0.0.0").split(".");
|
|
51
|
+
const major = Number(majStr);
|
|
52
|
+
const minor = Number(minStr);
|
|
53
|
+
const hasModernNode =
|
|
54
|
+
Number.isFinite(major) &&
|
|
55
|
+
Number.isFinite(minor) &&
|
|
56
|
+
(major > 22 || (major === 22 && minor >= 5));
|
|
57
|
+
if (isLinux && !hasBun && !hasModernNode) {
|
|
58
|
+
process.stderr.write(
|
|
59
|
+
"\n" +
|
|
60
|
+
"context-mode: install aborted\n" +
|
|
61
|
+
" Linux + Node " + (process.versions.node ?? "?") + " is unsupported.\n" +
|
|
62
|
+
" context-mode requires Node.js >= 22.5 (or Bun) on Linux to avoid the\n" +
|
|
63
|
+
" V8 madvise(MADV_DONTNEED) SIGSEGV affecting better-sqlite3 (1-4/hour).\n" +
|
|
64
|
+
" Tracking: https://github.com/nodejs/node/issues/62515\n" +
|
|
65
|
+
" https://github.com/mksglu/context-mode/issues/564\n" +
|
|
66
|
+
"\n" +
|
|
67
|
+
" Fix: upgrade Node (recommended)\n" +
|
|
68
|
+
" nvm install 22.5 && nvm use 22.5\n" +
|
|
69
|
+
" npm install -g context-mode\n" +
|
|
70
|
+
"\n" +
|
|
71
|
+
" Or: run under Bun\n" +
|
|
72
|
+
" curl -fsSL https://bun.sh/install | bash\n" +
|
|
73
|
+
" bun add -g context-mode\n" +
|
|
74
|
+
"\n",
|
|
75
|
+
);
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
22
80
|
/**
|
|
23
81
|
* True when running as a real `npm install -g context-mode`. We use this
|
|
24
82
|
* to keep contributors' local `npm install` runs from rewriting their HOME's
|