@devflow-tools/claude-code-plugin 0.10.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ import {createConnection}from'net';import {join}from'path';import {homedir}from'os';var f=join(homedir(),".devflow","hook-daemon.sock"),s=process.argv[2],u=process.argv[3];function t(){s==="pre-tool-use"&&console.log(JSON.stringify({hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"allow"}})),process.exit(0);}var o=createConnection(f,()=>{if(s==="stop")o.write(JSON.stringify({type:"stop"})+`
2
+ `);else if(s==="post-tool-use")o.write(JSON.stringify({type:"post-tool-use",input:u||""})+`
3
+ `);else if(s==="pre-tool-use"){let e="";process.stdin.setEncoding("utf8"),process.stdin.on("data",n=>{e+=n;}),process.stdin.on("end",()=>{o.write(JSON.stringify({type:"pre-tool-use",input:e})+`
4
+ `);}),process.stdin.on("error",()=>t());}else t();}),i="";o.on("data",e=>{if(i+=e.toString(),i.includes(`
5
+ `)){let n=i.split(`
6
+ `).filter(Boolean);for(let r of n)process.stdout.write(r+`
7
+ `);o.end();}});o.on("error",()=>t());o.on("close",()=>process.exit(0));setTimeout(()=>t(),4e3).unref();
@@ -0,0 +1,4 @@
1
+ import {createServer}from'net';import {join}from'path';import {homedir}from'os';import {existsSync,unlinkSync,readdirSync,readFileSync,mkdirSync,writeFileSync}from'fs';import {createHash}from'crypto';import {request}from'http';var E=1e4,J=3e4,L=300*1e3;function y(a,t,e){return new Promise(n=>{try{let o=new URL(a),s=request({hostname:o.hostname,port:o.port||80,path:o.pathname+o.search,method:"POST",headers:{"Content-Type":"application/json","X-API-Key":e,"Content-Length":Buffer.byteLength(t)},timeout:5e3},r=>{let i=[];r.on("data",c=>i.push(c)),r.on("end",()=>{let c=Buffer.concat(i).toString();n(r.statusCode!=null&&r.statusCode>=200&&r.statusCode<300?c:null);});});s.on("error",()=>n(null)),s.on("timeout",()=>{s.destroy(),n(null);}),s.write(t),s.end();}catch{n(null);}})}var v=class{constructor(t){this.retryScheduled=false;this.retryAttempts=0;this.apiUrl=t?.apiUrl??process.env.DEVFLOW_API_URL??"http://127.0.0.1:13337",this.apiKey=t?.apiKey??process.env.DEVFLOW_API_KEY??"dev-key-000000",this.cacheDir=t?.cacheDir??join(homedir(),".devflow","telemetry-cache");}async sendEvent(t){let e=this.truncateInput(t.input),n={...t,input:e},o=`${this.apiUrl}/api/telemetry/tool-call`,s=await y(o,JSON.stringify(n),this.apiKey);if(s)try{let r=JSON.parse(s),i=r.data?.eventId??r.eventId??null;if(i)return i}catch{}return this.cacheEvent({type:"tool_call",payload:n}),null}async sendExecutionStart(t,e,n,o){let s={executionId:t,sessionId:e,skillName:n,startedAt:o},r=`${this.apiUrl}/api/telemetry/skill-execution/start`;await y(r,JSON.stringify(s),this.apiKey)||this.cacheEvent({type:"execution_start",payload:s});}async sendSessionStart(t,e,n){let o={id:t,projectRoot:e,startedAt:n,label:e.split("/").pop()??"unknown"},s=`${this.apiUrl}/api/telemetry/sessions`;return await y(s,JSON.stringify(o),this.apiKey)!==null}async flushCache(){if(!existsSync(this.cacheDir))return;let t=readdirSync(this.cacheDir).filter(e=>e.endsWith(".json"));for(let e of t)try{let n=join(this.cacheDir,e),o=readFileSync(n,"utf8"),s=JSON.parse(o),r=s.type==="execution_start"?"/api/telemetry/skill-execution/start":"/api/telemetry/tool-call",i=`${this.apiUrl}${r}`;await y(i,JSON.stringify(s.payload),this.apiKey)&&unlinkSync(n);}catch{}}cacheEvent(t){try{existsSync(this.cacheDir)||mkdirSync(this.cacheDir,{recursive:!0});let e=join(this.cacheDir,`${Date.now()}_${Math.random().toString(36).slice(2,11)}.json`);writeFileSync(e,JSON.stringify(t,null,2)),this.scheduleRetry();}catch{}}scheduleRetry(){if(this.retryScheduled)return;this.retryScheduled=true;let t=Math.min(J*Math.pow(2,this.retryAttempts),L);setTimeout(()=>{this.retryScheduled=false,this.flushCache().then(()=>{this.retryAttempts=0;}).catch(()=>{this.retryAttempts++;});},t).unref();}truncateInput(t){let e=JSON.stringify(t);return e.length<=E?t:{_truncated:true,_original_size:e.length,_preview:e.substring(0,E)+"..."}}};var W=1800*1e3;function G(){return process.env.DEVFLOW_STATE_DIR??join(homedir(),".devflow")}var S=class{constructor(t){this.stateDir=G();}detect(){let t=join(this.stateDir,"current-skill.json");if(!existsSync(t))return null;try{let e=readFileSync(t,"utf8"),n=JSON.parse(e);return !n.registeredAt||Date.now()-n.registeredAt>W?(this.endExecution(),null):n}catch{return null}}startExecution(t,e){existsSync(this.stateDir)||mkdirSync(this.stateDir,{recursive:true});let n=this.getCurrentExecutionId();if(n)return n;let o=`exec_${Date.now()}_${Math.random().toString(36).slice(2,11)}`;return writeFileSync(join(this.stateDir,"current-execution-id"),o),writeFileSync(join(this.stateDir,"current-skill.json"),JSON.stringify({name:t,required_mcp_tools:e?.required_mcp_tools??[],registeredAt:Date.now()})),o}getCurrentExecutionId(){let t=join(this.stateDir,"current-execution-id");if(!existsSync(t))return null;try{return readFileSync(t,"utf8").trim()}catch{return null}}endExecution(){let t=join(this.stateDir,"current-skill.json"),e=join(this.stateDir,"current-execution-id");try{existsSync(t)&&unlinkSync(t);}catch{}try{existsSync(e)&&unlinkSync(e);}catch{}}};var m=join(homedir(),".devflow","hook-daemon.sock");join(homedir(),".devflow");var Q=600*1e3,tt=new Set(["Agent","Bash","Glob","Grep","WebSearch","WebFetch"]);function et(a){switch(a){case "Read":case "Glob":return "file_read";case "Write":case "Edit":case "NotebookEdit":return "file_write";case "Bash":return "bash_command";case "Agent":return "subagent";case "Skill":return "skill_invoke";default:return "tool_use"}}function C(a){let t=createHash("sha256").update(a).digest("hex").slice(0,16);return join(process.env.TMPDIR||"/tmp",".devflow-receipts",`${t}.json`)}function nt(a){try{let t=C(a);return existsSync(t)?JSON.parse(readFileSync(t,"utf-8")):{}}catch{return {}}}function R(a,t){try{let e=C(a),n=join(process.env.TMPDIR||"/tmp",".devflow-receipts");existsSync(n)||mkdirSync(n,{recursive:!0}),writeFileSync(e,JSON.stringify(t));}catch{}}var k=class{constructor(t){this.sessionId=null;this.eventMap=[];this.idleTimer=null;this.projectRoot=t,this.projectName=t.split("/").pop()||"unknown",this.telemetry=new v,this.skillManager=new S;}async start(){try{existsSync(m)&&unlinkSync(m);}catch{}let t=createServer(e=>{this.resetIdleTimer();let n="";e.on("data",async o=>{n+=o.toString();let s=n.split(`
2
+ `);n=s.pop()||"";for(let r of s)if(r.trim())try{let i=JSON.parse(r),c=await this.handleRequest(i);c!==null&&e.write(JSON.stringify(c)+`
3
+ `);}catch(i){e.write(JSON.stringify({error:i.message})+`
4
+ `);}}),e.on("error",()=>{});});t.on("error",e=>{console.error("[devflow-daemon] Server error:",e.message);}),t.listen(m,()=>{console.error("[devflow-daemon] Listening on",m);}),this.resetIdleTimer();}resetIdleTimer(){this.idleTimer&&clearTimeout(this.idleTimer),this.idleTimer=setTimeout(()=>{console.error("[devflow-daemon] Idle timeout, exiting"),this.shutdown();},Q).unref();}async handleRequest(t){switch(t.type){case "pre-tool-use":return this.handlePreToolUse(t.input);case "post-tool-use":return this.handlePostToolUse(t.input);case "stop":return await this.handleStop(),{status:"ok"};default:return {error:"unknown request type"}}}async handlePreToolUse(t){try{if(!t||t.trim()==="")return {hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"allow"}};let e=JSON.parse(t);if(this.sessionId||(this.sessionId=`session:${this.projectName}:${Date.now().toString(36)}`,this.telemetry.sendSessionStart(this.sessionId,this.projectRoot,Date.now()).catch(()=>{})),e.tool_name==="Skill"){let l=e.tool_input?.skill;if(l&&l.startsWith("devflow:")){let N=this.skillManager.startExecution(l);this.telemetry.sendExecutionStart(N,this.sessionId,l,Date.now()).catch(()=>{});}}let n=this.skillManager.detect(),o=e.tool_name.startsWith("mcp__devflow__"),s=o?e.tool_name.replace("mcp__devflow__",""):void 0,r=n?.required_mcp_tools??[],i=r.length>0,c=i&&!o,p;c?p={hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:`Skill "${n?.name||"unknown"}" requires MCP tools. Use one of: ${r.join(", ")}`}}:p={hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"allow"}};let d=this.skillManager.getCurrentExecutionId()??this.sessionId,g={eventId:`evt_${Date.now()}_${Math.random().toString(36).slice(2,11)}`,executionId:d,sessionId:this.sessionId,timestamp:Date.now(),toolName:e.tool_name,toolType:o?"mcp":e.tool_name==="Agent"?"subagent":"direct",isMcpTool:o,mcpToolName:s,mcpEnforced:o&&i,mcpFallback:!o&&i,kind:et(e.tool_name),input:e.tool_input,duration:0,blocked:c};return this.telemetry.sendEvent(g).then(l=>{l&&this.eventMap.push({toolName:e.tool_name,eventId:l,timestamp:g.timestamp});}).catch(()=>{}),p}catch{return {hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"allow"}}}}popEventFromMap(t){let e=this.eventMap.filter(o=>o.toolName===t);if(e.length===0)return null;e.sort((o,s)=>Math.abs(o.timestamp-Date.now())-Math.abs(s.timestamp-Date.now()));let n=e[0];return this.eventMap=this.eventMap.filter(o=>o.eventId!==n.eventId),n}enforcePostToolUse(t){if(!tt.has(t))return null;let e=nt(this.projectRoot),n=e.lastMcpCall??0;if(Math.floor(Date.now()/1e3)-n<30)return null;let s=e.bypassCount??0;if(s===0)return R(this.projectRoot,{...e,bypassCount:1}),null;let r=s+1;R(this.projectRoot,{...e,bypassCount:r});let i;switch(t){case "Agent":i=`\u7B2C ${r} \u6B21\u8FDD\u89C4\uFF1A\u4F60\u521A\u624D\u4F7F\u7528\u4E86 Agent \u5B50\u4EE3\u7406\u6267\u884C\u641C\u7D22\u3002\u4E0B\u6B21\u8BF7\u7528 get_project_context MCP \u5DE5\u5177\u66FF\u4EE3\u3002`;break;case "Bash":i=`\u7B2C ${r} \u6B21\u8FDD\u89C4\uFF1A\u4F60\u521A\u624D\u7528\u4E86 Bash \u547D\u4EE4\u641C\u7D22\u4EE3\u7801\u3002\u4E0B\u6B21\u8BF7\u7528 get_project_context MCP \u5DE5\u5177\u66FF\u4EE3\u3002`;break;case "Glob":i=`\u7B2C ${r} \u6B21\u8FDD\u89C4\uFF1A\u4F60\u521A\u624D\u7528\u4E86 Glob \u5339\u914D\u6587\u4EF6\u3002\u4E0B\u6B21\u8BF7\u7528 get_project_context MCP \u5DE5\u5177\u66FF\u4EE3\u3002`;break;case "Grep":i=`\u7B2C ${r} \u6B21\u8FDD\u89C4\uFF1A\u4F60\u521A\u624D\u7528\u4E86 Grep \u641C\u7D22\u4EE3\u7801\u3002\u4E0B\u6B21\u8BF7\u7528 get_project_context MCP \u5DE5\u5177\u66FF\u4EE3\u3002`;break;case "WebSearch":case "WebFetch":i=`\u7B2C ${r} \u6B21\u8FDD\u89C4\uFF1A\u4F60\u521A\u624D\u7528\u4E86\u7F51\u7EDC\u641C\u7D22\u3002\u4E0B\u6B21\u8BF7\u7528 get_knowledge MCP \u5DE5\u5177\u66FF\u4EE3\u3002`;break;default:i=`\u7B2C ${r} \u6B21\u8FDD\u89C4\uFF1A\u8BF7\u4F7F\u7528 DevFlow MCP \u5DE5\u5177\u66FF\u4EE3\u76F4\u63A5\u8C03\u7528\u3002`;}return r>=3?i=`${i} \u5DF2\u8FBE\u5230\u6700\u5927\u8FDD\u89C4\u6B21\u6570\uFF0C\u540E\u7EED\u7ED5\u8FC7\u5C06\u88AB\u786C\u963B\u6B62\u3002`:r>=2?i=`${i} \u4E0B\u6B21\u8FDD\u89C4\u5C06\u88AB\u963B\u6B62\u3002`:i=`${i} \u8BF7\u7ACB\u5373\u7EA0\u6B63\u3002`,i}async handlePostToolUse(t){if(!t)return null;let e;try{e=JSON.parse(t);}catch{return null}let n=e.tool_name||"",o=e.tool_input||{},s=e.tool_response,r=this.popEventFromMap(n);if(r&&this.sessionId){let f=typeof s=="string"?s:JSON.stringify(s),g=f.length>5e3?{_truncated:true,_originalSize:f.length,text:f.slice(0,5e3)}:s,l=Date.now()-r.timestamp;this.sendHttp("/api/telemetry/tool-call/output",{eventId:r.eventId,sessionId:this.sessionId,output:g,duration:l,completedAt:Date.now()});}let i=typeof s=="object"&&s!==null?s:null,c=i?.exitCode,p=i?.stderr;n==="Bash"&&c!==void 0&&c!==0&&this.sendHttp(`/api/memory/events?rootPath=${encodeURIComponent(this.projectRoot)}`,{id:`evt:${Date.now()}:${Math.random().toString(36).slice(2,7)}`,sessionId:this.sessionId??`session:${this.projectName}`,tool:n,command:o.command||n,exitCode:c,stderr:p??null,durationMs:0,createdAt:Date.now()});let d=this.enforcePostToolUse(n);return d?(console.error("[devflow-daemon] Enforcement:",d),{hookSpecificOutput:{hookEventName:"PostToolUse",additionalContext:JSON.stringify(d)}}):null}sendHttp(t,e){let n=JSON.stringify(e),o=new URL(t,"http://127.0.0.1:13337"),s=request({hostname:o.hostname,port:o.port,path:o.pathname+o.search,method:"POST",headers:{"Content-Type":"application/json","X-API-Key":"dev-key-000000","Content-Length":Buffer.byteLength(n)},timeout:5e3},()=>{});s.on("error",()=>{}),s.write(n),s.end();}async handleStop(){this.sessionId&&this.sendHttp(`/api/telemetry/sessions/${this.sessionId}/close`,{finishedAt:Date.now()}),this.sendHttp(`/api/memory/process?rootPath=${encodeURIComponent(this.projectRoot)}`,{}),await this.telemetry.flushCache().catch(()=>{}),this.shutdown();}shutdown(){this.idleTimer&&clearTimeout(this.idleTimer);try{unlinkSync(m);}catch{}process.exit(0);}},ot=process.argv[2]||process.env.CLAUDE_PROJECT_DIR||process.cwd(),st=new k(ot);st.start().catch(a=>{console.error("[devflow-daemon] Failed to start:",a),process.exit(1);});
@@ -1,2 +1,2 @@
1
- import {request}from'http';import {existsSync,readFileSync,writeFileSync}from'fs';import {join}from'path';import {homedir}from'os';var x=process.env.DEVFLOW_SERVER_URL||"http://127.0.0.1:13337",A=process.env.DEVFLOW_API_KEY||"dev-key-000000",w=process.env.CLAUDE_PROJECT_DIR||process.cwd(),v=join(homedir(),".devflow","session-id"),f=join(homedir(),".devflow","event-map.json");function y(d,a){return new Promise(e=>{let n=JSON.stringify(a),t=new URL(d,x),o=request({hostname:t.hostname,port:t.port,path:t.pathname,method:"POST",headers:{"Content-Type":"application/json","X-API-Key":A,"Content-Length":Buffer.byteLength(n)},timeout:5e3},()=>e());o.on("error",()=>e()),o.write(n),o.end();})}function L(){try{if(existsSync(v))return readFileSync(v,"utf-8").trim()}catch{}return null}function C(d,a){try{if(!existsSync(f))return null;let e=JSON.parse(readFileSync(f,"utf-8")),n=e.filter(c=>c.toolName===d);if(n.length===0)return null;let t=a??Date.now();n.sort((c,m)=>Math.abs(c.timestamp-t)-Math.abs(m.timestamp-t));let o=n[0],s=e.findIndex(c=>c.eventId===o.eventId);return s!==-1&&e.splice(s,1),writeFileSync(f,JSON.stringify(e)),{eventId:o.eventId,timestamp:o.timestamp}}catch{return null}}async function M(d){if(!d)return;let a;try{a=JSON.parse(d);}catch{return}let e=a.tool_name||"",n=a.tool_input||{},t=a.tool_response||{},o=L(),s=C(e,Date.now());if(s&&o){let r=typeof t=="string"?t:JSON.stringify(t),g=r.length>5e3?{_truncated:true,_originalSize:r.length,text:r.slice(0,5e3)}:t,i=Date.now()-s.timestamp;y("/api/telemetry/tool-call/output",{eventId:s.eventId,sessionId:o,output:g,duration:i,completedAt:Date.now()}).catch(()=>{});}let c=typeof n=="object"&&n.command?n.command:"",m=typeof t=="object"&&t!==null?t:null,p=m?.exitCode,O=m?.stderr;if(e==="Bash"&&p!==void 0&&p!==0){let r={id:`evt:${Date.now()}:${Math.random().toString(36).slice(2,7)}`,sessionId:o??`session:${w.split("/").pop()||"default"}`,tool:e,command:c||e,exitCode:p,stderr:O??null,durationMs:0,createdAt:Date.now()};y(`/api/memory/events?rootPath=${encodeURIComponent(w)}`,r).catch(()=>{});}let R=["get_project_context","get_knowledge","get_memory","search_symbol","get_dependency_graph","react_diagnose_bug","react_new_component","react_audit_performance","vue_diagnose_bug","nest_diagnose_bug","nextjs_diagnose_bug","electron_diagnose_bug","graphql_diagnose_bug","run_workflow"],l=e.replace("mcp__devflow__","");if(e.startsWith("mcp__devflow__")&&R.includes(l)){let r=typeof t=="object"&&t!==null?t:null,g=["files","results","data","result","memories","nodes","chunks"],i=0,u=r?.data??r;if(Array.isArray(u))i=u.length;else if(typeof u=="object"&&u!==null){let _=u;for(let h of g)Array.isArray(_[h])&&(i+=_[h].length);i===0&&Object.keys(_).length>0&&(i=1);}let k=JSON.stringify(t),j=Buffer.byteLength(k,"utf8"),D={id:`metric:${Date.now()}:${Math.random().toString(36).slice(2,11)}`,toolCallEventId:s?.eventId??`unknown:${Date.now()}`,sessionId:o??"",toolName:l,query:typeof n=="object"&&n.query?n.query:void 0,status:r?.error?"error":i===0?"empty":"success",resultCount:i,resultSizeBytes:j,latencyMs:s?Date.now()-s.timestamp:0,engine:l.startsWith("get_project_context")||l==="search_symbol"||l==="get_dependency_graph"?"codegraph":l==="get_knowledge"?"knowledge":l==="get_memory"?"memory":void 0};y("/api/telemetry/tool-metrics",D).catch(()=>{});}}(process.argv[1]?.endsWith("post-tool-use")||process.argv[1]?.endsWith("post-tool-use.js"))&&M(process.argv[2]||"").catch(()=>process.exit(0));
2
- export{M as handlePostToolUse};
1
+ import {request}from'http';import {existsSync,readFileSync,writeFileSync,mkdirSync}from'fs';import {join}from'path';import {homedir}from'os';import {createHash}from'crypto';var T=process.env.DEVFLOW_SERVER_URL||"http://127.0.0.1:13337",L=process.env.DEVFLOW_API_KEY||"dev-key-000000",v=process.env.CLAUDE_PROJECT_DIR||process.cwd(),C=join(homedir(),".devflow","session-id"),w=join(homedir(),".devflow","event-map.json"),F=new Set(["Agent","Bash","Glob","Grep","WebSearch","WebFetch"]);function b(r,s){return new Promise(t=>{let c=JSON.stringify(s),o=new URL(r,T),n=request({hostname:o.hostname,port:o.port,path:o.pathname,method:"POST",headers:{"Content-Type":"application/json","X-API-Key":L,"Content-Length":Buffer.byteLength(c)},timeout:5e3},()=>t());n.on("error",()=>t()),n.write(c),n.end();})}function J(){try{if(existsSync(C))return readFileSync(C,"utf-8").trim()}catch{}return null}function W(r,s){try{if(!existsSync(w))return null;let t=JSON.parse(readFileSync(w,"utf-8")),c=t.filter(l=>l.toolName===r);if(c.length===0)return null;let o=s??Date.now();c.sort((l,d)=>Math.abs(l.timestamp-o)-Math.abs(d.timestamp-o));let n=c[0],e=t.findIndex(l=>l.eventId===n.eventId);return e!==-1&&t.splice(e,1),writeFileSync(w,JSON.stringify(t)),{eventId:n.eventId,timestamp:n.timestamp}}catch{return null}}function P(){let r=createHash("sha256").update(v).digest("hex").slice(0,16);return join(process.env.TMPDIR||"/tmp",".devflow-receipts",`${r}.json`)}function U(){try{let r=P();return existsSync(r)?JSON.parse(readFileSync(r,"utf-8")):{}}catch{return {}}}function k(r){try{let s=P(),t=join(process.env.TMPDIR||"/tmp",".devflow-receipts");existsSync(t)||mkdirSync(t,{recursive:!0}),writeFileSync(s,JSON.stringify(r));}catch{}}function B(r){if(!F.has(r))return null;let s=U(),t=s.lastMcpCall??0;if(Math.floor(Date.now()/1e3)-t<30)return null;let o=s.bypassCount??0;if(o===0)return k({...s,bypassCount:1}),null;let n=o+1;k({...s,bypassCount:n});let e;switch(r){case "Agent":e=`\u7B2C ${n} \u6B21\u8FDD\u89C4\uFF1A\u4F60\u521A\u624D\u4F7F\u7528\u4E86 Agent \u5B50\u4EE3\u7406\u6267\u884C\u641C\u7D22\u3002\u4E0B\u6B21\u8BF7\u7528 get_project_context MCP \u5DE5\u5177\u66FF\u4EE3\u3002`;break;case "Bash":e=`\u7B2C ${n} \u6B21\u8FDD\u89C4\uFF1A\u4F60\u521A\u624D\u7528\u4E86 Bash \u547D\u4EE4\u641C\u7D22\u4EE3\u7801\u3002\u4E0B\u6B21\u8BF7\u7528 get_project_context MCP \u5DE5\u5177\u66FF\u4EE3\u3002`;break;case "Glob":e=`\u7B2C ${n} \u6B21\u8FDD\u89C4\uFF1A\u4F60\u521A\u624D\u7528\u4E86 Glob \u5339\u914D\u6587\u4EF6\u3002\u4E0B\u6B21\u8BF7\u7528 get_project_context MCP \u5DE5\u5177\u66FF\u4EE3\u3002`;break;case "Grep":e=`\u7B2C ${n} \u6B21\u8FDD\u89C4\uFF1A\u4F60\u521A\u624D\u7528\u4E86 Grep \u641C\u7D22\u4EE3\u7801\u3002\u4E0B\u6B21\u8BF7\u7528 get_project_context MCP \u5DE5\u5177\u66FF\u4EE3\u3002`;break;case "WebSearch":case "WebFetch":e=`\u7B2C ${n} \u6B21\u8FDD\u89C4\uFF1A\u4F60\u521A\u624D\u7528\u4E86\u7F51\u7EDC\u641C\u7D22\u3002\u4E0B\u6B21\u8BF7\u7528 get_knowledge MCP \u5DE5\u5177\u66FF\u4EE3\u3002`;break;default:e=`\u7B2C ${n} \u6B21\u8FDD\u89C4\uFF1A\u8BF7\u4F7F\u7528 DevFlow MCP \u5DE5\u5177\u66FF\u4EE3\u76F4\u63A5\u8C03\u7528\u3002`;}return n>=3?e=`${e} \u5DF2\u8FBE\u5230\u6700\u5927\u8FDD\u89C4\u6B21\u6570\uFF0C\u540E\u7EED\u7ED5\u8FC7\u5C06\u88AB\u786C\u963B\u6B62\u3002`:n>=2?e=`${e} \u4E0B\u6B21\u8FDD\u89C4\u5C06\u88AB\u963B\u6B62\u3002`:e=`${e} \u8BF7\u7ACB\u5373\u7EA0\u6B63\u3002`,e}async function q(r){if(!r)return null;let s;try{s=JSON.parse(r);}catch{return null}let t=s.tool_name||"",c=s.tool_input||{},o=s.tool_response||{},n=J(),e=W(t,Date.now());if(e&&n){let a=typeof o=="string"?o:JSON.stringify(o),h=a.length>5e3?{_truncated:true,_originalSize:a.length,text:a.slice(0,5e3)}:o,i=Date.now()-e.timestamp;b("/api/telemetry/tool-call/output",{eventId:e.eventId,sessionId:n,output:h,duration:i,completedAt:Date.now()}).catch(()=>{});}let l=typeof c=="object"&&c.command?c.command:"",d=typeof o=="object"&&o!==null?o:null,f=d?.exitCode,R=d?.stderr;if(t==="Bash"&&f!==void 0&&f!==0){let a={id:`evt:${Date.now()}:${Math.random().toString(36).slice(2,7)}`,sessionId:n??`session:${v.split("/").pop()||"default"}`,tool:t,command:l||t,exitCode:f,stderr:R??null,durationMs:0,createdAt:Date.now()};b(`/api/memory/events?rootPath=${encodeURIComponent(v)}`,a).catch(()=>{});}let M=["get_project_context","get_knowledge","get_memory","search_symbol","get_dependency_graph","react_diagnose_bug","react_new_component","react_audit_performance","vue_diagnose_bug","nest_diagnose_bug","nextjs_diagnose_bug","electron_diagnose_bug","graphql_diagnose_bug","run_workflow"],u=t.replace("mcp__devflow__","");if(t.startsWith("mcp__devflow__")&&M.includes(u)){let a=typeof o=="object"&&o!==null?o:null,h=["files","results","data","result","memories","nodes","chunks"],i=0,p=a?.data??a;if(Array.isArray(p))i=p.length;else if(typeof p=="object"&&p!==null){let y=p;for(let I of h)Array.isArray(y[I])&&(i+=y[I].length);i===0&&Object.keys(y).length>0&&(i=1);}let j=JSON.stringify(o),D=Buffer.byteLength(j,"utf8"),N={id:`metric:${Date.now()}:${Math.random().toString(36).slice(2,11)}`,toolCallEventId:e?.eventId??`unknown:${Date.now()}`,sessionId:n??"",toolName:u,query:typeof c=="object"&&c.query?c.query:void 0,status:a?.error?"error":i===0?"empty":"success",resultCount:i,resultSizeBytes:D,latencyMs:e?Date.now()-e.timestamp:0,engine:u.startsWith("get_project_context")||u==="search_symbol"||u==="get_dependency_graph"?"codegraph":u==="get_knowledge"?"knowledge":u==="get_memory"?"memory":void 0};b("/api/telemetry/tool-metrics",N).catch(()=>{});}let _=B(t);return _&&console.error("[devflow] Enforcement:",_),_}if(process.argv[1]?.endsWith("post-tool-use")||process.argv[1]?.endsWith("post-tool-use.js")){let r="";process.stdin.setEncoding("utf8"),process.stdin.on("data",s=>{r+=s;}),process.stdin.on("end",async()=>{let s=await q(r.trim()||process.argv[2]||"");if(s){let t=JSON.stringify(s);console.log(JSON.stringify({hookSpecificOutput:{hookEventName:"PostToolUse",additionalContext:t}}));}}),process.stdin.on("error",()=>process.exit(0)),setTimeout(()=>process.exit(0),5e3).unref();}
2
+ export{q as handlePostToolUse};
@@ -1,104 +1,16 @@
1
1
  #!/usr/bin/env bash
2
2
  set -euo pipefail
3
3
 
4
- INPUT_JSON=$(cat)
5
- PROJECT_ROOT="${CLAUDE_PROJECT_DIR:-$(pwd)}"
6
-
7
- # ── Event Recording via Node.js (best-effort, never blocks hook) ──
4
+ # DevFlow PostToolUse Hook — talks to hook-daemon via hook-client
8
5
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
9
- HOOK_JS="${CLAUDE_PLUGIN_ROOT:-$SCRIPT_DIR/..}/dist/hooks/post-tool-use.js"
10
- if [ -f "$HOOK_JS" ]; then
11
- node "$HOOK_JS" "$INPUT_JSON" 2>/dev/null || true
12
- fi
13
-
14
- # ── Enforcement Logic ──
15
- TOOL_NAME=$(echo "$INPUT_JSON" | python3 -c "
16
- import sys, json
17
- try:
18
- data = json.load(sys.stdin)
19
- print(data.get('tool_name', ''))
20
- except:
21
- print('')
22
- " 2>/dev/null || echo "")
23
-
24
- case "$TOOL_NAME" in
25
- Agent|Bash|Glob|Grep|WebSearch|WebFetch)
26
- ;;
27
- *)
28
- exit 0
29
- ;;
30
- esac
31
-
32
- RECEIPT_DIR="${TMPDIR:-/tmp}/.devflow-receipts"
33
- mkdir -p "$RECEIPT_DIR"
34
- SESSION_ID=$(echo -n "$PROJECT_ROOT" | shasum -a 256 2>/dev/null | cut -c1-16 || echo "default")
35
- RECEIPT_FILE="$RECEIPT_DIR/${SESSION_ID}.json"
36
-
37
- HAS_RECENT_MCP=false
38
- if [ -f "$RECEIPT_FILE" ]; then
39
- last_call=$(python3 -c "
40
- import json, sys, time
41
- try:
42
- with open('$RECEIPT_FILE') as f:
43
- d = json.load(f)
44
- ts = d.get('lastMcpCall', 0)
45
- now = int(time.time())
46
- if now - ts < 30:
47
- print('valid')
48
- except:
49
- pass
50
- " 2>/dev/null || echo "")
51
- if [ "$last_call" = "valid" ]; then
52
- exit 0
53
- fi
54
- fi
55
-
56
- BYPASS_COUNT=0
57
- if [ -f "$RECEIPT_FILE" ]; then
58
- BYPASS_COUNT=$(python3 -c "
59
- import json
60
- with open('$RECEIPT_FILE') as f:
61
- d = json.load(f)
62
- print(d.get('bypassCount', 0))
63
- " 2>/dev/null || echo "0")
64
- fi
65
-
66
- if [ "$BYPASS_COUNT" -eq 0 ]; then
67
- exit 0
68
- fi
69
-
70
- case "$TOOL_NAME" in
71
- Agent)
72
- MESSAGE="第 ${BYPASS_COUNT} 次违规:你刚才使用了 Agent 子代理执行搜索。下次请用 get_project_context MCP 工具替代。"
73
- ;;
74
- Bash)
75
- MESSAGE="第 ${BYPASS_COUNT} 次违规:你刚才用了 Bash 命令搜索代码。下次请用 get_project_context MCP 工具替代。"
76
- ;;
77
- Glob)
78
- MESSAGE="第 ${BYPASS_COUNT} 次违规:你刚才用了 Glob 匹配文件。下次请用 get_project_context MCP 工具替代。"
79
- ;;
80
- Grep)
81
- MESSAGE="第 ${BYPASS_COUNT} 次违规:你刚才用了 Grep 搜索代码。下次请用 get_project_context MCP 工具替代。"
82
- ;;
83
- WebSearch|WebFetch)
84
- MESSAGE="第 ${BYPASS_COUNT} 次违规:你刚才用了网络搜索。下次请用 get_knowledge MCP 工具替代。"
85
- ;;
86
- *)
87
- exit 0
88
- ;;
89
- esac
90
-
91
- if [ "$BYPASS_COUNT" -ge 3 ]; then
92
- MESSAGE="${MESSAGE} 已达到最大违规次数,后续绕过将被硬阻止。"
93
- elif [ "$BYPASS_COUNT" -ge 2 ]; then
94
- MESSAGE="${MESSAGE} 下次违规将被阻止。"
95
- else
96
- MESSAGE="${MESSAGE} 请立即纠正。"
97
- fi
98
-
99
- if [ -n "${CLAUDE_PLUGIN_ROOT:-}" ]; then
100
- ESCAPED=$(printf '%s' "$MESSAGE" | python3 -c "import sys,json; print(json.dumps(sys.stdin.read()))")
101
- printf '{"hookSpecificOutput":{"hookEventName":"PostToolUse","additionalContext":"%s"}}\n' "$ESCAPED"
6
+ PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT:-$SCRIPT_DIR/..}"
7
+ CLIENT_JS="$PLUGIN_ROOT/dist/hooks/hook-client.js"
8
+ FALLBACK_JS="$PLUGIN_ROOT/dist/hooks/post-tool-use.js"
9
+
10
+ if [ -f "$CLIENT_JS" ]; then
11
+ node "$CLIENT_JS" post-tool-use "$(cat)" 2>/dev/null || true
12
+ elif [ -f "$FALLBACK_JS" ]; then
13
+ node "$FALLBACK_JS" 2>/dev/null || true
102
14
  fi
103
15
 
104
16
  exit 0
@@ -1,16 +1,23 @@
1
1
  #!/usr/bin/env bash
2
2
  set -euo pipefail
3
3
 
4
- # DevFlow PreToolUse Hook — Node.js handler
5
- # Falls back to allow if Node.js handler is not available
6
-
4
+ # DevFlow PreToolUse Hook — talks to hook-daemon via hook-client
7
5
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
8
- HOOK_HANDLER="${CLAUDE_PLUGIN_ROOT:-$SCRIPT_DIR/..}/dist/hooks/pre-tool-use.js"
6
+ PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT:-$SCRIPT_DIR/..}"
7
+ CLIENT_JS="$PLUGIN_ROOT/dist/hooks/hook-client.js"
8
+ FALLBACK_JS="$PLUGIN_ROOT/dist/hooks/pre-tool-use.js"
9
9
 
10
- if [ -f "$HOOK_HANDLER" ]; then
11
- # Pass stdin through to Node.js handler; fallback to allow on any failure
12
- node "$HOOK_HANDLER" || { echo '[devflow] hook handler failed, allowing tool' >&2; echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow"}}'; }
10
+ if [ -f "$CLIENT_JS" ]; then
11
+ node "$CLIENT_JS" pre-tool-use || {
12
+ echo '[devflow] hook-client failed, trying fallback' >&2
13
+ if [ -f "$FALLBACK_JS" ]; then
14
+ node "$FALLBACK_JS" || echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow"}}'
15
+ else
16
+ echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow"}}'
17
+ fi
18
+ }
19
+ elif [ -f "$FALLBACK_JS" ]; then
20
+ node "$FALLBACK_JS" || echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow"}}'
13
21
  else
14
- # Handler not built — allow all tools (graceful degradation)
15
22
  echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow"}}'
16
23
  fi
@@ -131,6 +131,19 @@ if [ -n "${CLAUDE_PLUGIN_ROOT:-}" ]; then
131
131
  fi
132
132
  fi
133
133
 
134
+ # ── Start hook daemon (background, Unix socket) ──
135
+ DAEMON_JS="${PLUGIN_ROOT}/dist/hooks/hook-daemon.js"
136
+ if [ -f "$DAEMON_JS" ]; then
137
+ # Kill any stale daemon from previous session
138
+ STALE_PID=$(pgrep -f "hook-daemon.js" 2>/dev/null || true)
139
+ if [ -n "$STALE_PID" ]; then
140
+ kill "$STALE_PID" 2>/dev/null || true
141
+ sleep 0.5
142
+ fi
143
+ node "$DAEMON_JS" "${CLAUDE_PROJECT_DIR:-$(pwd)}" &
144
+ echo "[devflow] Hook daemon started (PID $!)" >&2
145
+ fi
146
+
134
147
  # ── Trigger memory extraction for previous session events (best-effort, async) ──
135
148
  (python3 -c "
136
149
  import urllib.request, json, sys
package/hooks/stop CHANGED
@@ -1,11 +1,32 @@
1
1
  #!/usr/bin/env bash
2
2
  set -euo pipefail
3
3
 
4
+ # DevFlow Stop Hook — graceful daemon shutdown + cleanup
4
5
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
- HOOK_JS="${CLAUDE_PLUGIN_ROOT:-$SCRIPT_DIR/..}/dist/hooks/stop.js"
6
+ PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT:-$SCRIPT_DIR/..}"
6
7
 
7
- if [ -f "$HOOK_JS" ]; then
8
- node "$HOOK_JS" 2>/dev/null || true
8
+ # 1. Tell daemon to stop via hook-client
9
+ CLIENT_JS="$PLUGIN_ROOT/dist/hooks/hook-client.js"
10
+ if [ -f "$CLIENT_JS" ]; then
11
+ node "$CLIENT_JS" stop </dev/null 2>/dev/null || true
9
12
  fi
10
13
 
14
+ # 2. Fallback: kill any remaining daemon process
15
+ DAEMON_PID=$(pgrep -f "hook-daemon.js" 2>/dev/null || true)
16
+ if [ -n "$DAEMON_PID" ]; then
17
+ kill "$DAEMON_PID" 2>/dev/null || true
18
+ fi
19
+
20
+ # 3. Run existing stop.js cleanup (session close, memory extraction)
21
+ STOP_JS="$PLUGIN_ROOT/dist/hooks/stop.js"
22
+ if [ -f "$STOP_JS" ]; then
23
+ node "$STOP_JS" 2>/dev/null || true
24
+ fi
25
+
26
+ # 4. Clean up stale receipt files for this project
27
+ PROJECT_ROOT="${CLAUDE_PROJECT_DIR:-$(pwd)}"
28
+ SESSION_ID=$(echo -n "$PROJECT_ROOT" | shasum -a 256 2>/dev/null | cut -c1-16 || echo "")
29
+ RECEIPT_FILE="${TMPDIR:-/tmp}/.devflow-receipts/${SESSION_ID}.json"
30
+ rm -f "$RECEIPT_FILE" 2>/dev/null || true
31
+
11
32
  exit 0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devflow-tools/claude-code-plugin",
3
- "version": "0.10.0",
3
+ "version": "0.12.0",
4
4
  "description": "DevFlow — Developer Intelligence Platform for Claude Code",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -8,7 +8,7 @@
8
8
  "deploy": "npx tsx scripts/inject-gates.ts --deploy"
9
9
  },
10
10
  "devDependencies": {
11
- "@devflow-tools/mcp-server": "^0.10.0"
11
+ "@devflow-tools/mcp-server": "^0.12.0"
12
12
  },
13
13
  "keywords": [
14
14
  "pi-package",
@@ -28,5 +28,5 @@
28
28
  "hooks.json",
29
29
  "skills"
30
30
  ],
31
- "gitHead": "20444d2631e3d77c3b3a4c3afd9398b21d888a4f"
31
+ "gitHead": "b7b5606e419887fe8c1f921a8a5a564ce9afcbac"
32
32
  }