@devflow-tools/claude-code-plugin 0.11.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.
- package/dist/hooks/hook-client.js +7 -0
- package/dist/hooks/hook-daemon.js +4 -0
- package/dist/hooks/post-tool-use.js +2 -2
- package/hooks/post-tool-use +9 -97
- package/hooks/pre-tool-use +15 -8
- package/hooks/session-start +13 -0
- package/hooks/stop +24 -3
- package/package.json +3 -3
|
@@ -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
|
|
2
|
-
export{
|
|
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};
|
package/hooks/post-tool-use
CHANGED
|
@@ -1,104 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
set -euo pipefail
|
|
3
3
|
|
|
4
|
-
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
package/hooks/pre-tool-use
CHANGED
|
@@ -1,16 +1,23 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
set -euo pipefail
|
|
3
3
|
|
|
4
|
-
# DevFlow PreToolUse Hook —
|
|
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
|
-
|
|
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 "$
|
|
11
|
-
|
|
12
|
-
|
|
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
|
package/hooks/session-start
CHANGED
|
@@ -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
|
-
|
|
6
|
+
PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT:-$SCRIPT_DIR/..}"
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
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.
|
|
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.
|
|
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": "
|
|
31
|
+
"gitHead": "b7b5606e419887fe8c1f921a8a5a564ce9afcbac"
|
|
32
32
|
}
|