feat: file-read gate allows Edit, add legacy-peer-deps for grammar install

- Change file-read gate from deny to allow with limit:1, injecting the
  observation timeline as additionalContext. Edit now works on gated files
  since the file registers as "read" with near-zero token cost.
- Add updatedInput to HookResult type for PreToolUse hooks.
- Add .npmrc with legacy-peer-deps=true for tree-sitter peer dep conflicts.
- Add --legacy-peer-deps to npm fallback paths in smart-install.js so end
  users without bun can install the 24 grammar packages.
- Rebuild plugin artifacts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Alex Newman
2026-04-07 14:06:07 -07:00
parent 7996dfd5cd
commit d0676aa049
7 changed files with 263 additions and 184 deletions

1
.npmrc Normal file
View File

@@ -0,0 +1 @@
legacy-peer-deps=true

File diff suppressed because one or more lines are too long

View File

@@ -449,7 +449,7 @@ function installDeps() {
console.error('⚠️ Bun install failed, falling back to npm...');
console.error(' (This can happen with npm alias packages like *-cjs)');
try {
execSync('npm install', { cwd: ROOT, stdio: installStdio, shell: IS_WINDOWS });
execSync('npm install --legacy-peer-deps', { cwd: ROOT, stdio: installStdio, shell: IS_WINDOWS });
} catch (npmError) {
throw new Error('Both bun and npm install failed: ' + npmError.message);
}
@@ -546,7 +546,7 @@ try {
if (!verifyCriticalModules()) {
console.error('⚠️ Retrying install with npm...');
try {
execSync('npm install --production', { cwd: ROOT, stdio: ['pipe', 'pipe', 'inherit'], shell: IS_WINDOWS });
execSync('npm install --production --legacy-peer-deps', { cwd: ROOT, stdio: ['pipe', 'pipe', 'inherit'], shell: IS_WINDOWS });
} catch {
// npm also failed
}

View File

@@ -944,8 +944,8 @@ View Observations Live @ http://localhost:${i}`:void 0;return{hookSpecificOutput
`+String.fromCodePoint(128172)+` Community https://discord.gg/J4wttp9vDu
`+String.fromCodePoint(128250)+` Watch live in browser http://localhost:${r}/
`)}catch{}return{exitCode:nt.SUCCESS}}}});function WSe(t){return t.toLowerCase().replace(" am","a").replace(" pm","p")}function VSe(t){return new Date(t).toLocaleString("en-US",{hour:"numeric",minute:"2-digit",hour12:!0})}function KSe(t){return new Date(t).toLocaleString("en-US",{month:"short",day:"numeric",year:"numeric"})}function JSe(t,e,r){let n=new Set,i=[];for(let o of t){let a=o.memory_session_id??`no-session-${o.id}`;n.has(a)||(n.add(a),i.push(o))}let s=i.map(o=>{let a=ka(o.files_read),c=ka(o.files_modified),u=a.length+c.length,l=e.replace(/\\/g,"/"),d=c.some(f=>f.replace(/\\/g,"/")===l),p=0;return d&&(p+=2),u<=3?p+=2:u<=8&&(p+=1),{obs:o,specificityScore:p}});return s.sort((o,a)=>a.specificityScore-o.specificityScore),s.slice(0,r).map(o=>o.obs)}function XSe(t,e){let r=e.replace(/\\/g,"\\\\").replace(/"/g,'\\"').replace(/\n/g,"\\n"),n=new Map;for(let l of t){let d=KSe(l.created_at_epoch);n.has(d)||n.set(d,[]),n.get(d).push(l)}let i=Array.from(n.entries()).sort((l,d)=>{let p=Math.min(...l[1].map(m=>m.created_at_epoch)),f=Math.min(...d[1].map(m=>m.created_at_epoch));return p-f}),s=new Date,o=s.toLocaleDateString("en-CA"),a=s.toLocaleTimeString("en-US",{hour:"numeric",minute:"2-digit",hour12:!0}).toLowerCase().replace(" ",""),c=s.toLocaleTimeString("en-US",{timeZoneName:"short"}).split(" ").pop(),u=[`Current: ${o} ${a} ${c}`,"Read blocked: This file has prior observations. Choose the cheapest path:","- **Already know enough?** The timeline below may be all you need (semantic priming).","- **Need details?** get_observations([IDs]) \u2014 ~300 tokens each.",`- **Need current code?** smart_outline("${r}") for structure (~1-2k tokens), smart_unfold("${r}", "<symbol>") for a specific function (~400-2k tokens).`,"- **Need to edit?** Use smart tools for line numbers, then sed via Bash (Edit requires Read, but you already have the context)."];for(let[l,d]of i){let p=[...d].sort((f,m)=>f.created_at_epoch-m.created_at_epoch);u.push(`### ${l}`);for(let f of p){let m=f.title||"Untitled",h=GSe[f.type]||"\u2753",g=WSe(VSe(f.created_at_epoch));u.push(`${f.id} ${g} ${h} ${m}`)}}return u.join(`
`)}var _5,qa,HSe,ZSe,BSe,GSe,lR,dR=we(()=>{"use strict";Ir();re();js();_5=require("fs"),qa=Pe(require("path"),1);Ky();tr();It();za();HSe=1500,ZSe=40,BSe=15,GSe={decision:"\u2696\uFE0F",bugfix:"\u{1F534}",feature:"\u{1F7E3}",refactor:"\u{1F504}",discovery:"\u{1F535}",change:"\u2705"};lR={async execute(t){let r=t.toolInput?.file_path;if(!r)return{continue:!0,suppressOutput:!0};try{let s=qa.default.isAbsolute(r)?r:qa.default.resolve(t.cwd||process.cwd(),r);if((0,_5.statSync)(s).size<HSe)return{continue:!0,suppressOutput:!0}}catch(s){if(s.code==="ENOENT")return{continue:!0,suppressOutput:!0}}let n=ve.loadFromFile(vt);if(t.cwd&&sl(t.cwd,n.CLAUDE_MEM_EXCLUDED_PROJECTS))return _.debug("HOOK","Project excluded from tracking, skipping file context",{cwd:t.cwd}),{continue:!0,suppressOutput:!0};if(!await sr())return{continue:!0,suppressOutput:!0};try{let s=il(t.cwd),o=t.cwd||process.cwd(),a=qa.default.isAbsolute(r)?r:qa.default.resolve(o,r),c=qa.default.relative(o,a).split(qa.default.sep).join("/"),u=new URLSearchParams({path:c});s.allProjects.length>0&&u.set("projects",s.allProjects.join(",")),u.set("limit",String(ZSe));let l=await at(`/api/observations/by-file?${u.toString()}`,{method:"GET"});if(!l.ok)return _.warn("HOOK","File context query failed, skipping",{status:l.status,filePath:r}),{continue:!0,suppressOutput:!0};let d=await l.json();if(!d.observations||d.observations.length===0)return{continue:!0,suppressOutput:!0};let p=JSe(d.observations,c,BSe);return p.length===0?{continue:!0,suppressOutput:!0}:{hookSpecificOutput:{hookEventName:"PreToolUse",additionalContext:"",permissionDecision:"deny",permissionDecisionReason:XSe(p,r)}}}catch(s){return _.warn("HOOK","File context fetch error, skipping",{error:s instanceof Error?s.message:String(s)}),{continue:!0,suppressOutput:!0}}}}});function b5(t){let e=YSe[t];return e||(_.warn("HOOK",`Unknown event type: ${t}, returning no-op`),{async execute(){return{continue:!0,suppressOutput:!0,exitCode:nt.SUCCESS}}})}var YSe,S5=we(()=>{"use strict";On();re();sR();Jy();Xy();aR();uR();Yy();dR();Qy();sR();Jy();Xy();aR();uR();Yy();dR();Qy();YSe={context:iR,"session-init":qf,observation:Hf,summarize:oR,"session-complete":Bf,"user-message":cR,"file-edit":Zf,"file-context":lR}});var w5={};wn(w5,{hookCommand:()=>QSe,isWorkerUnavailableError:()=>x5});function x5(t){let e=t instanceof Error?t.message:String(t),r=e.toLowerCase();return["econnrefused","econnreset","epipe","etimedout","enotfound","econnaborted","enetunreach","ehostunreach","fetch failed","unable to connect","socket hang up"].some(i=>r.includes(i))||r.includes("timed out")||r.includes("timeout")||/failed:\s*5\d{2}/.test(e)||/status[:\s]+5\d{2}/.test(e)||/failed:\s*429/.test(e)||/status[:\s]+429/.test(e)?!0:(/failed:\s*4\d{2}/.test(e)||/status[:\s]+4\d{2}/.test(e)||t instanceof TypeError||t instanceof ReferenceError||t instanceof SyntaxError,!1)}async function QSe(t,e,r={}){let n=process.stderr.write.bind(process.stderr);process.stderr.write=(()=>!0);try{let i=m5(t),s=b5(e),o=await n5(),a=i.normalizeInput(o);a.platform=t;let c=await s.execute(a),u=i.formatOutput(c);console.log(JSON.stringify(u));let l=c.exitCode??nt.SUCCESS;return r.skipExit||process.exit(l),l}catch(i){return x5(i)?(_.warn("HOOK",`Worker unavailable, skipping hook: ${i instanceof Error?i.message:i}`),r.skipExit||process.exit(nt.SUCCESS),nt.SUCCESS):(_.error("HOOK",`Hook error: ${i instanceof Error?i.message:i}`,{},i instanceof Error?i:void 0),r.skipExit||process.exit(nt.BLOCKING_ERROR),nt.BLOCKING_ERROR)}finally{process.stderr.write=n}}var E5=we(()=>{"use strict";i5();h5();S5();On();re()});var fR={};wn(fR,{cleanClaudeMd:()=>pxe,generateClaudeMd:()=>dxe});function rxe(t){return txe[t]||"\u{1F4DD}"}function nxe(t){let e=(t.title?.length||0)+(t.subtitle?.length||0)+(t.narrative?.length||0)+(t.facts?.length||0);return Math.ceil(e/4)}function ixe(t){let e=new Set;try{let n=(0,$5.execSync)("git ls-files",{cwd:t,encoding:"utf-8",maxBuffer:52428800}).trim().split(`
`)}catch{}return{exitCode:nt.SUCCESS}}}});function WSe(t){return t.toLowerCase().replace(" am","a").replace(" pm","p")}function VSe(t){return new Date(t).toLocaleString("en-US",{hour:"numeric",minute:"2-digit",hour12:!0})}function KSe(t){return new Date(t).toLocaleString("en-US",{month:"short",day:"numeric",year:"numeric"})}function JSe(t,e,r){let n=new Set,i=[];for(let o of t){let a=o.memory_session_id??`no-session-${o.id}`;n.has(a)||(n.add(a),i.push(o))}let s=i.map(o=>{let a=ka(o.files_read),c=ka(o.files_modified),u=a.length+c.length,l=e.replace(/\\/g,"/"),d=c.some(f=>f.replace(/\\/g,"/")===l),p=0;return d&&(p+=2),u<=3?p+=2:u<=8&&(p+=1),{obs:o,specificityScore:p}});return s.sort((o,a)=>a.specificityScore-o.specificityScore),s.slice(0,r).map(o=>o.obs)}function XSe(t,e){let r=e.replace(/\\/g,"\\\\").replace(/"/g,'\\"').replace(/\n/g,"\\n"),n=new Map;for(let l of t){let d=KSe(l.created_at_epoch);n.has(d)||n.set(d,[]),n.get(d).push(l)}let i=Array.from(n.entries()).sort((l,d)=>{let p=Math.min(...l[1].map(m=>m.created_at_epoch)),f=Math.min(...d[1].map(m=>m.created_at_epoch));return p-f}),s=new Date,o=s.toLocaleDateString("en-CA"),a=s.toLocaleTimeString("en-US",{hour:"numeric",minute:"2-digit",hour12:!0}).toLowerCase().replace(" ",""),c=s.toLocaleTimeString("en-US",{timeZoneName:"short"}).split(" ").pop(),u=[`Current: ${o} ${a} ${c}`,"This file has prior observations. Only line 1 was read to save tokens.","- **Already know enough?** The timeline below may be all you need (semantic priming).","- **Need details?** get_observations([IDs]) \u2014 ~300 tokens each.","- **Need full file?** Read again with offset/limit for the section you need.",`- **Need to edit?** Edit works \u2014 the file is registered as read. Use smart_outline("${r}") for line numbers.`];for(let[l,d]of i){let p=[...d].sort((f,m)=>f.created_at_epoch-m.created_at_epoch);u.push(`### ${l}`);for(let f of p){let m=f.title||"Untitled",h=GSe[f.type]||"\u2753",g=WSe(VSe(f.created_at_epoch));u.push(`${f.id} ${g} ${h} ${m}`)}}return u.join(`
`)}var _5,qa,HSe,ZSe,BSe,GSe,lR,dR=we(()=>{"use strict";Ir();re();js();_5=require("fs"),qa=Pe(require("path"),1);Ky();tr();It();za();HSe=1500,ZSe=40,BSe=15,GSe={decision:"\u2696\uFE0F",bugfix:"\u{1F534}",feature:"\u{1F7E3}",refactor:"\u{1F504}",discovery:"\u{1F535}",change:"\u2705"};lR={async execute(t){let r=t.toolInput?.file_path;if(!r)return{continue:!0,suppressOutput:!0};try{let s=qa.default.isAbsolute(r)?r:qa.default.resolve(t.cwd||process.cwd(),r);if((0,_5.statSync)(s).size<HSe)return{continue:!0,suppressOutput:!0}}catch(s){if(s.code==="ENOENT")return{continue:!0,suppressOutput:!0}}let n=ve.loadFromFile(vt);if(t.cwd&&sl(t.cwd,n.CLAUDE_MEM_EXCLUDED_PROJECTS))return _.debug("HOOK","Project excluded from tracking, skipping file context",{cwd:t.cwd}),{continue:!0,suppressOutput:!0};if(!await sr())return{continue:!0,suppressOutput:!0};try{let s=il(t.cwd),o=t.cwd||process.cwd(),a=qa.default.isAbsolute(r)?r:qa.default.resolve(o,r),c=qa.default.relative(o,a).split(qa.default.sep).join("/"),u=new URLSearchParams({path:c});s.allProjects.length>0&&u.set("projects",s.allProjects.join(",")),u.set("limit",String(ZSe));let l=await at(`/api/observations/by-file?${u.toString()}`,{method:"GET"});if(!l.ok)return _.warn("HOOK","File context query failed, skipping",{status:l.status,filePath:r}),{continue:!0,suppressOutput:!0};let d=await l.json();if(!d.observations||d.observations.length===0)return{continue:!0,suppressOutput:!0};let p=JSe(d.observations,c,BSe);return p.length===0?{continue:!0,suppressOutput:!0}:{hookSpecificOutput:{hookEventName:"PreToolUse",additionalContext:XSe(p,r),permissionDecision:"allow",updatedInput:{file_path:r,limit:1}}}}catch(s){return _.warn("HOOK","File context fetch error, skipping",{error:s instanceof Error?s.message:String(s)}),{continue:!0,suppressOutput:!0}}}}});function b5(t){let e=YSe[t];return e||(_.warn("HOOK",`Unknown event type: ${t}, returning no-op`),{async execute(){return{continue:!0,suppressOutput:!0,exitCode:nt.SUCCESS}}})}var YSe,S5=we(()=>{"use strict";On();re();sR();Jy();Xy();aR();uR();Yy();dR();Qy();sR();Jy();Xy();aR();uR();Yy();dR();Qy();YSe={context:iR,"session-init":qf,observation:Hf,summarize:oR,"session-complete":Bf,"user-message":cR,"file-edit":Zf,"file-context":lR}});var w5={};wn(w5,{hookCommand:()=>QSe,isWorkerUnavailableError:()=>x5});function x5(t){let e=t instanceof Error?t.message:String(t),r=e.toLowerCase();return["econnrefused","econnreset","epipe","etimedout","enotfound","econnaborted","enetunreach","ehostunreach","fetch failed","unable to connect","socket hang up"].some(i=>r.includes(i))||r.includes("timed out")||r.includes("timeout")||/failed:\s*5\d{2}/.test(e)||/status[:\s]+5\d{2}/.test(e)||/failed:\s*429/.test(e)||/status[:\s]+429/.test(e)?!0:(/failed:\s*4\d{2}/.test(e)||/status[:\s]+4\d{2}/.test(e)||t instanceof TypeError||t instanceof ReferenceError||t instanceof SyntaxError,!1)}async function QSe(t,e,r={}){let n=process.stderr.write.bind(process.stderr);process.stderr.write=(()=>!0);try{let i=m5(t),s=b5(e),o=await n5(),a=i.normalizeInput(o);a.platform=t;let c=await s.execute(a),u=i.formatOutput(c);console.log(JSON.stringify(u));let l=c.exitCode??nt.SUCCESS;return r.skipExit||process.exit(l),l}catch(i){return x5(i)?(_.warn("HOOK",`Worker unavailable, skipping hook: ${i instanceof Error?i.message:i}`),r.skipExit||process.exit(nt.SUCCESS),nt.SUCCESS):(_.error("HOOK",`Hook error: ${i instanceof Error?i.message:i}`,{},i instanceof Error?i:void 0),r.skipExit||process.exit(nt.BLOCKING_ERROR),nt.BLOCKING_ERROR)}finally{process.stderr.write=n}}var E5=we(()=>{"use strict";i5();h5();S5();On();re()});var fR={};wn(fR,{cleanClaudeMd:()=>pxe,generateClaudeMd:()=>dxe});function rxe(t){return txe[t]||"\u{1F4DD}"}function nxe(t){let e=(t.title?.length||0)+(t.subtitle?.length||0)+(t.narrative?.length||0)+(t.facts?.length||0);return Math.ceil(e/4)}function ixe(t){let e=new Set;try{let n=(0,$5.execSync)("git ls-files",{cwd:t,encoding:"utf-8",maxBuffer:52428800}).trim().split(`
`).filter(i=>i);for(let i of n){let s=lr.default.join(t,i),o=lr.default.dirname(s);for(;o.length>t.length&&o.startsWith(t);)e.add(o),o=lr.default.dirname(o)}}catch(r){_.warn("CLAUDE_MD","git ls-files failed, falling back to directory walk",{error:String(r)}),I5(t,e)}return e}function I5(t,e,r=0){if(r>10)return;let n=["node_modules",".git",".next","dist","build",".cache","__pycache__",".venv","venv",".idea",".vscode","coverage",".claude-mem",".open-next",".turbo"];try{let i=(0,dr.readdirSync)(t,{withFileTypes:!0});for(let s of i){if(!s.isDirectory()||n.includes(s.name)||s.name.startsWith(".")&&s.name!==".claude")continue;let o=lr.default.join(t,s.name);e.add(o),I5(o,e,r+1)}}catch{}}function sxe(t,e){let r=n=>{if(!n)return!1;try{let i=JSON.parse(n);if(Array.isArray(i))return i.some(s=>wa(s,e))}catch{}return!1};return r(t.files_modified)||r(t.files_read)}function oxe(t,e,r,n){let i=n*3,s=`
SELECT o.*, o.discovery_tokens
FROM observations o

File diff suppressed because one or more lines are too long

View File

@@ -138,11 +138,11 @@ function formatFileTimeline(observations: ObservationRow[], filePath: string): s
const lines: string[] = [
`Current: ${currentDate} ${currentTime} ${currentTimezone}`,
`Read blocked: This file has prior observations. Choose the cheapest path:`,
`This file has prior observations. Only line 1 was read to save tokens.`,
`- **Already know enough?** The timeline below may be all you need (semantic priming).`,
`- **Need details?** get_observations([IDs]) — ~300 tokens each.`,
`- **Need current code?** smart_outline("${safePath}") for structure (~1-2k tokens), smart_unfold("${safePath}", "<symbol>") for a specific function (~400-2k tokens).`,
`- **Need to edit?** Use smart tools for line numbers, then sed via Bash (Edit requires Read, but you already have the context).`,
`- **Need full file?** Read again with offset/limit for the section you need.`,
`- **Need to edit?** Edit works — the file is registered as read. Use smart_outline("${safePath}") for line numbers.`,
];
for (const [day, dayObservations] of sortedDays) {
@@ -233,15 +233,19 @@ export const fileContextHandler: EventHandler = {
return { continue: true, suppressOutput: true };
}
// Deny the read with the timeline as the reason — Claude sees the timeline
// and decides: work from semantic priming, use get_observations(), or ask user to allow read
// Allow the read with limit: 1 line — just enough for Edit's "file must be read"
// check to pass, while keeping token cost near zero. The observation timeline
// gives Claude full context about prior work on this file.
const timeline = formatFileTimeline(dedupedObservations, filePath);
return {
hookSpecificOutput: {
hookEventName: 'PreToolUse',
additionalContext: '',
permissionDecision: 'deny',
permissionDecisionReason: timeline,
additionalContext: timeline,
permissionDecision: 'allow',
updatedInput: {
file_path: filePath,
limit: 1,
},
},
};
} catch (error) {

View File

@@ -22,6 +22,7 @@ export interface HookResult {
additionalContext: string;
permissionDecision?: 'allow' | 'deny';
permissionDecisionReason?: string;
updatedInput?: Record<string, unknown>;
};
systemMessage?: string;
exitCode?: number;