@devflow-tools/claude-code-plugin 0.11.0 → 0.12.1
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
- package/skills/devflow:start/SKILL.md +14 -18
|
@@ -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.1",
|
|
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": "221830920b02ecb0fffb7652ac7ceec5a3f884d3"
|
|
32
32
|
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: devflow:start
|
|
3
|
-
description: 启动 DevFlow
|
|
3
|
+
description: 启动 DevFlow 服务端(API + Dashboard),显示访问信息
|
|
4
4
|
required_mcp_tools: []
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# DevFlow Start
|
|
8
8
|
|
|
9
|
-
启动 DevFlow
|
|
9
|
+
启动 DevFlow 开发环境。使用 `npx devflow start` 命令,无论在哪个目录下调用都能正确启动 DevFlow 服务。
|
|
10
10
|
|
|
11
11
|
## 执行步骤
|
|
12
12
|
|
|
@@ -15,40 +15,36 @@ required_mcp_tools: []
|
|
|
15
15
|
npx devflow start
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
18
|
+
Server 启动后同时提供:
|
|
19
|
+
- REST API(默认端口 13337)
|
|
20
|
+
- Dashboard(由 Server 在同一端口提供静态文件服务)
|
|
21
21
|
|
|
22
22
|
2. **显示启动信息**
|
|
23
23
|
服务启动后,显示以下信息:
|
|
24
24
|
|
|
25
25
|
```
|
|
26
26
|
╔═══════════════════════════════════════════════════════════╗
|
|
27
|
-
║
|
|
27
|
+
║ DevFlow Started! ║
|
|
28
28
|
╠═══════════════════════════════════════════════════════════╣
|
|
29
|
-
║
|
|
30
|
-
║
|
|
31
|
-
║
|
|
32
|
-
║ ║
|
|
33
|
-
║ Press Ctrl+C to stop all services ║
|
|
29
|
+
║ Server + Dashboard: http://localhost:13337 ║
|
|
30
|
+
║ ║
|
|
31
|
+
║ Press Ctrl+C to stop ║
|
|
34
32
|
╚═══════════════════════════════════════════════════════════╝
|
|
35
33
|
```
|
|
36
34
|
|
|
37
35
|
3. **保持运行**
|
|
38
36
|
- `devflow start` 会持续运行直到 Ctrl+C
|
|
39
|
-
-
|
|
37
|
+
- API 和 Dashboard 由 CLI 统一管理生命周期
|
|
40
38
|
|
|
41
39
|
## 注意事项
|
|
42
40
|
|
|
43
|
-
-
|
|
44
|
-
- 端口 17788:前端(Next.js Dashboard)
|
|
41
|
+
- Server 和 Dashboard 共用端口 13337(Dashboard 作为静态文件由 server 提供)
|
|
45
42
|
- 使用 `npx devflow start` 从任意目录启动,不依赖当前工作目录
|
|
46
|
-
- 如果端口被占用,提示用户 `lsof -i :13337`
|
|
43
|
+
- 如果端口被占用,提示用户 `lsof -i :13337` 检查
|
|
47
44
|
|
|
48
45
|
## 故障排除
|
|
49
46
|
|
|
50
47
|
如果启动失败:
|
|
51
|
-
1. 检查端口是否被占用:`lsof -i :13337`
|
|
48
|
+
1. 检查端口是否被占用:`lsof -i :13337`
|
|
52
49
|
2. 确认 devflow 已安装:`npx devflow --version`
|
|
53
|
-
3. 如需后台运行:`npx devflow start
|
|
54
|
-
4. 仅启动服务端(不要 Dashboard):`npx devflow start --no-dashboard`
|
|
50
|
+
3. 如需后台运行:`npx devflow start --background`
|