@scrylog/cli 0.4.1 → 0.5.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/README.md +148 -93
- package/dist/cli/assets/providers/SOURCES.md +9 -9
- package/dist/cli/index.js +395 -224
- package/dist/cli/index.js.map +43 -32
- package/dist/daemon/index.js +580 -488
- package/dist/daemon/index.js.map +144 -213
- package/dist/hooks/claude.js +3 -3
- package/dist/hooks/claude.js.map +4 -4
- package/dist/hooks/codex.js +6 -6
- package/dist/hooks/codex.js.map +4 -4
- package/dist/mcp/index.js +41 -41
- package/dist/mcp/index.js.map +23 -22
- package/dist/oc/index.js +3 -3
- package/dist/oc/index.js.map +3 -3
- package/package.json +44 -36
- package/src/postinstall.cjs +8 -8
package/dist/hooks/claude.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
// @bun
|
|
3
|
-
var
|
|
4
|
-
`)[0]??"";return G.length>0?G.slice(0,120):void 0}function BG(J){if(!J)return!1;let G=J.trim();return/\?/.test(G)||/^(would you|do you|can you|should i|which|what|how|please confirm|shall i|want me to)/i.test(G)}function EG(J,G){let K=`${G??""} ${J??""}`.trim();if(!K)return"turn";if(/(approval|approve|permission|confirm|proceed|allow)/i.test(K))return"permission";if(/[?]/.test(K)||/^(would you|do you|can you|should i|which|what|how|shall i|want me to)/i.test(K))return"question";return"turn"}function UG(J){if(BG(J.last_assistant_message))return"waiting-input";return"idle"}function v(J,G,K){let V=l(G.tool_name,G.tool_input);switch(J){case"SessionStart":return{
|
|
3
|
+
var r=Object.create;var{getPrototypeOf:n,defineProperty:E,getOwnPropertyNames:g}=Object;var N=Object.prototype.hasOwnProperty;function z(J){return this[J]}var jG=(J,G,K)=>{var V=g(G);for(let Y of V)if(!N.call(J,Y)&&Y!=="default")E(J,Y,{get:z.bind(G,Y),enumerable:!0});if(K){for(let Y of V)if(!N.call(K,Y)&&Y!=="default")E(K,Y,{get:z.bind(G,Y),enumerable:!0});return K}},p,t,qG=(J,G,K)=>{var V=J!=null&&typeof J==="object";if(V){var Y=G?p??=new WeakMap:t??=new WeakMap,$=Y.get(J);if($)return $}K=J!=null?r(n(J)):{};let L=G||!J||!J.__esModule?E(K,"default",{value:J,enumerable:!0}):K;for(let Z of g(J))if(!N.call(L,Z))E(L,Z,{get:z.bind(J,Z),enumerable:!0});if(V)Y.set(J,L);return L};var CG=(J,G)=>()=>(G||J((G={exports:{}}).exports,G),G.exports);var d=(J)=>J;function o(J,G){this[J]=d.bind(null,G)}var WG=(J,G)=>{for(var K in G)E(J,K,{get:G[K],enumerable:!0,configurable:!0,set:o.bind(G,K)})};var e=(J,G)=>()=>(J&&(G=J(J=0)),G);var NG=(J)=>Promise.all(J),zG=import.meta.require;import{chmodSync as a,lstatSync as GG,mkdirSync as w,readFileSync as JG,rmSync as KG}from"fs";import{tmpdir as VG,userInfo as YG}from"os";import{join as D}from"path";function PG(J){let G=Bun.which?.(J);if(G)return G;if(O)return k("where",[J]);return k("which",[J])}function k(J,G){try{let K=Bun.spawnSync([J,...G],{stderr:"ignore",stdout:"pipe"}),V=new TextDecoder().decode(K.stdout).trim();return V?V.split(/\r?\n/)[0]??V:null}catch{return null}}function $G(){try{return YG().uid}catch{return process.env.USER??process.env.USERNAME??"default"}}function I(J){if(O)return D(VG(),`scrylog-${J}`);return D("/tmp",`scrylog-${$G()}-${J}`)}function x(J){let G=GG(J);return G.isDirectory()&&G.uid===process.getuid?.()}function h(J){if(w(J,{mode:448,recursive:!0}),O)return;if(!x(J)){if(KG(J,{force:!0,recursive:!0}),w(J,{mode:448,recursive:!0}),!x(J))throw Error(`Refusing to use untrusted hooks directory: ${J}`)}a(J,448)}var O,HG,ZG,fG;var S=e(()=>{O=process.platform==="win32",HG=process.platform==="darwin",ZG=process.platform==="linux",fG=(()=>{if(!ZG)return!1;try{let J=JG("/proc/version","utf8");return/microsoft|wsl/i.test(J)}catch{return!1}})()});S();import{basename as LG,join as q}from"path";import{existsSync as b,readFileSync as H,renameSync as QG,rmSync as XG,writeFileSync as c}from"fs";import{spawnSync as m}from"child_process";function RG(){let J=process.env.SCRYLOG_CONFIG;if(J&&b(J))try{let K=JSON.parse(H(J,"utf8")).claude;if(typeof K?.hooksDir==="string")return K.hooksDir}catch{}return I("claude-hooks")}function MG(){let J=process.env.SCRYLOG_CONFIG;if(J&&b(J))try{let K=JSON.parse(H(J,"utf8")).api;if(typeof K?.port==="number")return K.port}catch{}return 7788}async function j(J){try{let G=MG();return(await fetch(`http://127.0.0.1:${G}/api/webhook/claude`,{body:JSON.stringify(J),headers:{"Content-Type":"application/json"},method:"POST",signal:AbortSignal.timeout(1500)})).ok}catch{return!1}}function l(J,G){if(!J)return;let K=typeof G?.command==="string"?G.command.slice(0,120):void 0;if(K)return K;let V=typeof G?.file_path==="string"?G.file_path:void 0;if(V)return V;let Y=typeof G?.description==="string"?G.description:void 0;if(Y)return Y.slice(0,120);return}function R(J){if(!J)return;let G=J.trim().split(`
|
|
4
|
+
`)[0]??"";return G.length>0?G.slice(0,120):void 0}function BG(J){if(!J)return!1;let G=J.trim();return/\?/.test(G)||/^(would you|do you|can you|should i|which|what|how|please confirm|shall i|want me to)/i.test(G)}function EG(J,G){let K=`${G??""} ${J??""}`.trim();if(!K)return"turn";if(/(approval|approve|permission|confirm|proceed|allow)/i.test(K))return"permission";if(/[?]/.test(K)||/^(would you|do you|can you|should i|which|what|how|shall i|want me to)/i.test(K))return"question";return"turn"}function UG(J){if(BG(J.last_assistant_message))return"waiting-input";return"idle"}function v(J,G,K){let V=l(G.tool_name,G.tool_input);switch(J){case"SessionStart":return{attentionKind:"none",hasAttention:!1,state:"idle",summary:G.source?`Claude session ${G.source}`:"Claude session started"};case"UserPromptSubmit":return{attentionKind:"none",hasAttention:!1,state:"working",summary:R(G.prompt)??"Claude is working"};case"PreToolUse":return{activeTool:G.tool_name,attentionKind:"none",hasAttention:!1,state:"working",summary:G.tool_name?`Using ${G.tool_name}${V?`: ${V}`:""}`:void 0,toolInputSummary:V,toolName:G.tool_name,toolResultStatus:"started"};case"PermissionRequest":return{activeTool:G.tool_name,attentionKind:"permission",hasAttention:!0,state:"waiting-input",summary:G.tool_name?`Permission required for ${G.tool_name}${V?`: ${V}`:""}`:"Permission required",toolInputSummary:V,toolName:G.tool_name,toolResultStatus:"blocked"};case"PermissionDenied":return{activeTool:G.tool_name,attentionKind:"none",hasAttention:!1,state:"working",summary:G.reason?.slice(0,120)??(G.tool_name?`Permission denied for ${G.tool_name}`:"Permission denied"),toolInputSummary:V,toolName:G.tool_name,toolResultStatus:"denied"};case"PostToolUse":return{activeTool:G.tool_name,attentionKind:"none",hasAttention:!1,state:"working",summary:G.tool_name?`Using ${G.tool_name}${V?`: ${V}`:""}`:void 0,toolInputSummary:V,toolName:G.tool_name,toolResultStatus:typeof G.tool_response==="object"&&G.tool_response!==null&&"is_error"in G.tool_response?G.tool_response.is_error===!0?"error":"completed":"completed"};case"PostToolUseFailure":return{activeTool:G.tool_name,attentionKind:"none",hasAttention:!1,state:G.is_interrupt?"idle":"working",summary:G.error?.slice(0,120)??(G.tool_name?`Failed ${G.tool_name}`:"Tool failed"),toolInputSummary:V,toolName:G.tool_name,toolResultStatus:"error"};case"PostToolBatch":return{attentionKind:"none",hasAttention:!1,state:"working",summary:"Processing tool results"};case"Stop":{let Y=UG(G);return{attentionKind:Y==="waiting-input"?"question":"none",hasAttention:Y==="waiting-input",state:Y,summary:R(G.last_assistant_message)}}case"StopFailure":return{attentionKind:"turn",hasAttention:!0,state:"error",summary:R(G.last_assistant_message)??G.error_details?.slice(0,120)??G.error?.slice(0,120)};case"CwdChanged":return{attentionKind:K?.attentionKind??"none",hasAttention:K?.hasAttention??!1,state:K?.state??"idle"};case"SubagentStart":return{attentionKind:"none",hasAttention:!1,state:"working",summary:G.agent_type?`Subagent ${G.agent_type}`:"Subagent started"};case"SubagentStop":return{attentionKind:"none",hasAttention:!1,state:"idle",summary:R(G.last_assistant_message)};case"SessionEnd":return{attentionKind:"none",hasAttention:!1,state:"idle",summary:G.reason?`Claude session ended: ${G.reason}`:"Claude session ended"};case"Notification":return{attentionKind:EG(G.message,G.title),hasAttention:!0,state:K?.state??"idle",summary:R(G.message??G.title)};default:return{attentionKind:K?.attentionKind??"none",hasAttention:K?.hasAttention??!1,state:K?.state??"idle"}}}function U(J,G){if(J==="UserPromptSubmit"&&G.prompt)return R(G.prompt);if(J==="SessionStart"&&G.source)return`Claude session ${G.source}`;if(J==="PermissionRequest"&&G.tool_name){let K=l(G.tool_name,G.tool_input);return`Permission required for ${G.tool_name}${K?`: ${K}`:""}`}if(J==="PermissionDenied")return G.reason?.slice(0,120);if((J==="PreToolUse"||J==="PostToolUse")&&G.tool_name){let K=G.tool_input?.command?.slice(0,60);return K?`Using ${G.tool_name}: ${K}`:`Using ${G.tool_name}`}if(J==="PostToolUseFailure")return G.error?.slice(0,120);if(J==="Stop"||J==="StopFailure"||J==="SubagentStop")return R(G.last_assistant_message);if(J==="SessionEnd"&&G.reason)return`Claude session ended: ${G.reason}`;return}function u(J){let G=process.env.TMUX_PANE;if(!G)return;let K=m("tmux",["set-option","-p","-t",G,"@claude-session",J],{stdio:"ignore",timeout:2000});if(K.error||K.status!==0);}function AG(){let J=process.env.TMUX_PANE;if(!J)return;m("tmux",["set-option","-up","-t",J,"@claude-session"],{stdio:"ignore",timeout:2000})}function F(J,G){let K=q(J,`${G}.json`);if(!b(K))return null;try{return JSON.parse(H(K,"utf8"))}catch{return null}}function T(J,G,K){h(J);let V=q(J,`${G}.json`),Y=q(J,`${G}.json.tmp`);c(Y,JSON.stringify(K),"utf8");try{QG(Y,V)}catch{c(V,JSON.stringify(K),"utf8")}}async function _G(){let J="";for await(let M of Bun.stdin.stream())J+=new TextDecoder().decode(M);let G;try{G=JSON.parse(J)}catch{process.exit(0)}let K=G.hook_event_name??"",V=G.session_id;if(!V)process.exit(0);let Y=RG(),$=Date.now();if(K==="SessionEnd"){await j({attentionKind:"none",detectionSource:"claude-hook",eventTime:$,eventType:K,hasAttention:!1,hookSeq:0,sessionId:V,state:"idle",summary:U(K,G),...G.reason?{endReason:G.reason}:{},...G.transcript_path?{transcriptPath:G.transcript_path}:{},currentPath:G.cwd,lastActivity:$});try{XG(q(Y,`${V}.json`),{force:!0})}catch{}AG(),process.exit(0)}if(K==="SubagentStart"||K==="SubagentStop"){let M=G.agent_transcript_path??G.subagent_transcript_path,A=G.agent_id??G.subagent_session_id??(M?LG(M,".jsonl"):void 0);if(A){let Q=F(Y,A),X=v(K,G,Q),C=Q?.hookSeq??0,W=Math.floor(Date.now()/100),_=Math.max(C+1,W),B={attentionKind:X.attentionKind,detectionSource:"claude-hook",eventTime:$,eventType:K,hasAttention:X.hasAttention,hookSeq:_,state:X.state,...X.summary??U(K,G)?{summary:X.summary??U(K,G)}:{},...G.permission_mode?{permissionMode:G.permission_mode}:{},...G.agent_id?{agentId:G.agent_id}:{},...G.agent_type?{agentType:G.agent_type}:{},...M?{transcriptPath:M}:{},currentPath:G.cwd,lastActivity:$,parentSessionId:`claude:${V}`};T(Y,A,B),await j({sessionId:A,...B})}if(K==="SubagentStop"){let Q=F(Y,V),X=Q?.hookSeq??0,C=Math.floor(Date.now()/100),W=Math.max(X+1,C),_=U("SubagentStop",G),B={attentionKind:Q?.attentionKind??"none",detectionSource:"claude-hook",eventTime:$,eventType:"SubagentStop",hasAttention:Q?.hasAttention??!1,hookSeq:W,state:Q?.state??"working",..._?{summary:_}:{},currentPath:G.new_cwd??G.cwd,lastActivity:$};T(Y,V,B),await j({sessionId:V,...B}),u(V),process.exit(0)}}let L=F(Y,V),Z=v(K,G,L),f=Z.summary??U(K,G)??L?.summary,s=L?.hookSeq??0,y=Math.floor(Date.now()/100),i=Math.max(s+1,y),P={attentionKind:Z.attentionKind,detectionSource:"claude-hook",eventTime:$,eventType:K,hasAttention:Z.hasAttention,hookSeq:i,state:Z.state,...f?{summary:f}:{},...Z.activeTool?{activeTool:Z.activeTool}:{},...Z.toolName?{toolName:Z.toolName}:{},...Z.toolInputSummary?{toolInputSummary:Z.toolInputSummary}:{},...Z.toolResultStatus?{toolResultStatus:Z.toolResultStatus}:{},...G.permission_mode?{permissionMode:G.permission_mode}:{},...G.source?{startSource:G.source}:{},...G.reason?{endReason:G.reason}:{},...G.error?{errorType:G.error}:{},...G.agent_id?{agentId:G.agent_id}:{},...G.agent_type?{agentType:G.agent_type}:{},...G.transcript_path?{transcriptPath:G.transcript_path}:{},currentPath:G.new_cwd??G.cwd,lastActivity:$};if(T(Y,V,P),await j({sessionId:V,...P}),K==="SessionStart"||K==="UserPromptSubmit"||K==="PermissionRequest"||K==="PreToolUse")u(V);process.exit(0)}_G().catch(()=>process.exit(0));
|
|
5
5
|
|
|
6
|
-
//# debugId=
|
|
6
|
+
//# debugId=301E94BAA2B7FF9164756E2164756E21
|
package/dist/hooks/claude.js.map
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/shared/platform.ts", "../src/hooks/claude.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"import {
|
|
6
|
-
"#!/usr/bin/env bun\n/**\n * scrylog-claude-hook — Claude Code lifecycle hook handler.\n *\n * Claude Code calls this binary at each lifecycle event (Stop, PreToolUse,\n * PostToolUse, Notification, SubagentStop) with a JSON payload on stdin.\n * This script writes a state file for the ClaudeCollector to pick up and,\n * when running inside tmux, sets the @claude-session pane option so the\n * TmuxCollector can link the pane to the Claude session.\n *\n * Configure in ~/.claude/settings.json via:\n * scrylog claude hooks install\n */\n\nimport { writeFileSync, renameSync, readFileSync, existsSync, rmSync } from \"node:fs\";\nimport { join, basename } from \"node:path\";\nimport { spawnSync } from \"node:child_process\";\nimport { getHooksDir, ensureHooksDirSafe } from \"../shared/platform\";\nimport type { SessionState } from \"../shared/types\";\n\ntype AttentionKind = \"permission\" | \"question\" | \"turn\" | \"none\";\ntype ToolResultStatus = \"started\" | \"completed\" | \"error\" | \"blocked\" | \"denied\" | \"unknown\";\n\n// ── Hook payload ──────────────────────────────────────────────────────────────\n\ninterface ClaudeHookPayload {\n session_id: string;\n transcript_path?: string;\n cwd?: string;\n permission_mode?: string;\n hook_event_name?: string;\n source?: string;\n model?: string;\n prompt?: string;\n stop_hook_active?: boolean;\n last_assistant_message?: string;\n // PreToolUse / PostToolUse\n tool_name?: string;\n tool_input?: Record<string, unknown>;\n tool_response?: unknown;\n tool_use_id?: string;\n duration_ms?: number;\n error?: string;\n error_details?: string;\n is_interrupt?: boolean;\n reason?: string;\n old_cwd?: string;\n new_cwd?: string;\n permission_suggestions?: unknown[];\n tool_calls?: Array<{\n tool_name?: string;\n tool_input?: Record<string, unknown>;\n tool_use_id?: string;\n tool_response?: unknown;\n }>;\n // Notification / async events\n message?: string;\n title?: string;\n // Subagents\n agent_id?: string;\n agent_type?: string;\n agent_transcript_path?: string;\n subagent_session_id?: string;\n subagent_transcript_path?: string;\n}\n\ninterface DerivedHookState {\n state: SessionState;\n hasAttention: boolean;\n attentionKind: AttentionKind;\n activeTool?: string;\n toolName?: string;\n toolInputSummary?: string;\n toolResultStatus?: ToolResultStatus;\n summary?: string;\n}\n\n// ── Config resolution ─────────────────────────────────────────────────────────\n\nfunction resolveHooksDir(): string {\n const envConfig = process.env[\"SCRYLOG_CONFIG\"];\n if (envConfig && existsSync(envConfig)) {\n try {\n const raw = JSON.parse(readFileSync(envConfig, \"utf8\")) as Record<string, unknown>;\n const claude = raw[\"claude\"] as Record<string, unknown> | undefined;\n if (typeof claude?.[\"hooksDir\"] === \"string\") return claude[\"hooksDir\"];\n } catch {\n // fall through to default\n }\n }\n return getHooksDir(\"claude-hooks\");\n}\n\nfunction resolveDaemonPort(): number {\n const envConfig = process.env[\"SCRYLOG_CONFIG\"];\n if (envConfig && existsSync(envConfig)) {\n try {\n const raw = JSON.parse(readFileSync(envConfig, \"utf8\")) as Record<string, unknown>;\n const api = raw[\"api\"] as Record<string, unknown> | undefined;\n if (typeof api?.[\"port\"] === \"number\") return api[\"port\"] as number;\n } catch {\n // fall through to default\n }\n }\n return 7788;\n}\n\nasync function tryHttpDelivery(payload: HookStateFile & { sessionId: string }): Promise<boolean> {\n try {\n const port = resolveDaemonPort();\n const res = await fetch(`http://127.0.0.1:${port}/api/webhook/claude`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(payload),\n signal: AbortSignal.timeout(1500),\n });\n return res.ok;\n } catch {\n return false;\n }\n}\n\n// ── Transcript inspection ─────────────────────────────────────────────────────\n\nfunction inferPermissionModeFromTranscript(transcriptPath?: string): string | undefined {\n if (!transcriptPath) return undefined;\n try {\n const content = readFileSync(transcriptPath, \"utf8\");\n const lines = content.split(/\\r?\\n/).filter(Boolean);\n for (let i = lines.length - 1; i >= 0; i--) {\n let entry: Record<string, unknown>;\n try {\n entry = JSON.parse(lines[i]!) as Record<string, unknown>;\n } catch {\n continue;\n }\n if (entry[\"type\"] !== \"permission-mode\") continue;\n const mode = entry[\"permissionMode\"];\n if (typeof mode === \"string\" && mode.length > 0) return mode;\n }\n } catch {\n // Ignore transcript read issues\n }\n return undefined;\n}\n\nfunction summarizeToolInput(toolName?: string, input?: Record<string, unknown>): string | undefined {\n if (!toolName) return undefined;\n const command = typeof input?.[\"command\"] === \"string\" ? input[\"command\"].slice(0, 120) : undefined;\n if (command) return command;\n const filePath = typeof input?.[\"file_path\"] === \"string\" ? input[\"file_path\"] : undefined;\n if (filePath) return filePath;\n const description = typeof input?.[\"description\"] === \"string\" ? input[\"description\"] : undefined;\n if (description) return description.slice(0, 120);\n return undefined;\n}\n\nfunction summarizeText(text?: string): string | undefined {\n if (!text) return undefined;\n const first = text.trim().split(\"\\n\")[0] ?? \"\";\n return first.length > 0 ? first.slice(0, 120) : undefined;\n}\n\nfunction isQuestion(text?: string): boolean {\n if (!text) return false;\n const trimmed = text.trim();\n return /\\?/.test(trimmed) || /^(would you|do you|can you|should i|which|what|how|please confirm|shall i|want me to)/i.test(trimmed);\n}\n\nfunction isInterruptedRequestText(text: string): boolean {\n return /^\\[Request interrupted by user(?: for tool use)?\\]$/i.test(text.trim());\n}\n\nfunction classifyAttention(message?: string, title?: string): AttentionKind {\n const text = `${title ?? \"\"} ${message ?? \"\"}`.trim();\n if (!text) return \"turn\";\n if (/(approval|approve|permission|confirm|proceed|allow)/i.test(text)) return \"permission\";\n if (/[?]/.test(text) || /^(would you|do you|can you|should i|which|what|how|shall i|want me to)/i.test(text)) {\n return \"question\";\n }\n return \"turn\";\n}\n\nfunction inferStopState(payload: ClaudeHookPayload): SessionState {\n if (isQuestion(payload.last_assistant_message)) return \"waiting-input\";\n return \"idle\";\n}\n\n// ── State derivation ──────────────────────────────────────────────────────────\n\ninterface PreviousState {\n state?: SessionState;\n hasAttention?: boolean;\n attentionKind?: AttentionKind;\n}\n\nfunction deriveHookState(event: string, payload: ClaudeHookPayload, previous?: PreviousState | null): DerivedHookState {\n const toolInputSummary = summarizeToolInput(payload.tool_name, payload.tool_input);\n switch (event) {\n case \"SessionStart\":\n return {\n state: \"idle\",\n hasAttention: false,\n attentionKind: \"none\",\n summary: payload.source ? `Claude session ${payload.source}` : \"Claude session started\",\n };\n case \"UserPromptSubmit\":\n return {\n state: \"working\",\n hasAttention: false,\n attentionKind: \"none\",\n summary: summarizeText(payload.prompt) ?? \"Claude is working\",\n };\n case \"PreToolUse\":\n return {\n state: \"working\",\n hasAttention: false,\n attentionKind: \"none\",\n activeTool: payload.tool_name,\n toolName: payload.tool_name,\n toolInputSummary,\n toolResultStatus: \"started\",\n summary: payload.tool_name ? `Using ${payload.tool_name}${toolInputSummary ? `: ${toolInputSummary}` : \"\"}` : undefined,\n };\n case \"PermissionRequest\":\n return {\n state: \"waiting-input\",\n hasAttention: true,\n attentionKind: \"permission\",\n activeTool: payload.tool_name,\n toolName: payload.tool_name,\n toolInputSummary,\n toolResultStatus: \"blocked\",\n summary: payload.tool_name ? `Permission required for ${payload.tool_name}${toolInputSummary ? `: ${toolInputSummary}` : \"\"}` : \"Permission required\",\n };\n case \"PermissionDenied\":\n return {\n state: \"working\",\n hasAttention: false,\n attentionKind: \"none\",\n activeTool: payload.tool_name,\n toolName: payload.tool_name,\n toolInputSummary,\n toolResultStatus: \"denied\",\n summary: payload.reason?.slice(0, 120) ?? (payload.tool_name ? `Permission denied for ${payload.tool_name}` : \"Permission denied\"),\n };\n case \"PostToolUse\":\n return {\n state: \"working\",\n hasAttention: false,\n attentionKind: \"none\",\n activeTool: payload.tool_name,\n toolName: payload.tool_name,\n toolInputSummary,\n toolResultStatus: typeof payload.tool_response === \"object\" && payload.tool_response !== null && \"is_error\" in (payload.tool_response as Record<string, unknown>)\n ? ((payload.tool_response as Record<string, unknown>)[\"is_error\"] === true ? \"error\" : \"completed\")\n : \"completed\",\n summary: payload.tool_name ? `Using ${payload.tool_name}${toolInputSummary ? `: ${toolInputSummary}` : \"\"}` : undefined,\n };\n case \"PostToolUseFailure\":\n return {\n state: payload.is_interrupt ? \"idle\" : \"working\",\n hasAttention: false,\n attentionKind: \"none\",\n activeTool: payload.tool_name,\n toolName: payload.tool_name,\n toolInputSummary,\n toolResultStatus: \"error\",\n summary: payload.error?.slice(0, 120) ?? (payload.tool_name ? `Failed ${payload.tool_name}` : \"Tool failed\"),\n };\n case \"PostToolBatch\":\n return {\n state: \"working\",\n hasAttention: false,\n attentionKind: \"none\",\n summary: \"Processing tool results\",\n };\n case \"Stop\": {\n const state = inferStopState(payload);\n return {\n state,\n hasAttention: state === \"waiting-input\",\n attentionKind: state === \"waiting-input\" ? \"question\" : \"none\",\n summary: summarizeText(payload.last_assistant_message),\n };\n }\n case \"StopFailure\":\n return {\n state: \"error\",\n hasAttention: true,\n attentionKind: \"turn\",\n summary: summarizeText(payload.last_assistant_message) ?? payload.error_details?.slice(0, 120) ?? payload.error?.slice(0, 120),\n };\n case \"CwdChanged\":\n return {\n state: previous?.state ?? \"idle\",\n hasAttention: previous?.hasAttention ?? false,\n attentionKind: previous?.attentionKind ?? \"none\",\n };\n case \"SubagentStart\":\n return {\n state: \"working\",\n hasAttention: false,\n attentionKind: \"none\",\n summary: payload.agent_type ? `Subagent ${payload.agent_type}` : \"Subagent started\",\n };\n case \"SubagentStop\":\n return {\n state: \"idle\",\n hasAttention: false,\n attentionKind: \"none\",\n summary: summarizeText(payload.last_assistant_message),\n };\n case \"SessionEnd\":\n return {\n state: \"idle\",\n hasAttention: false,\n attentionKind: \"none\",\n summary: payload.reason ? `Claude session ended: ${payload.reason}` : \"Claude session ended\",\n };\n case \"Notification\":\n return {\n state: previous?.state ?? \"idle\",\n hasAttention: true,\n attentionKind: classifyAttention(payload.message, payload.title),\n summary: summarizeText(payload.message ?? payload.title),\n };\n default:\n return {\n state: previous?.state ?? \"idle\",\n hasAttention: previous?.hasAttention ?? false,\n attentionKind: previous?.attentionKind ?? \"none\",\n };\n }\n}\n\nfunction deriveSummary(event: string, payload: ClaudeHookPayload): string | undefined {\n if (event === \"UserPromptSubmit\" && payload.prompt) {\n return summarizeText(payload.prompt);\n }\n if (event === \"SessionStart\" && payload.source) {\n return `Claude session ${payload.source}`;\n }\n if (event === \"PermissionRequest\" && payload.tool_name) {\n const target = summarizeToolInput(payload.tool_name, payload.tool_input);\n return `Permission required for ${payload.tool_name}${target ? `: ${target}` : \"\"}`;\n }\n if (event === \"PermissionDenied\") {\n return payload.reason?.slice(0, 120);\n }\n if ((event === \"PreToolUse\" || event === \"PostToolUse\") && payload.tool_name) {\n const cmd = (payload.tool_input?.[\"command\"] as string | undefined)?.slice(0, 60);\n return cmd ? `Using ${payload.tool_name}: ${cmd}` : `Using ${payload.tool_name}`;\n }\n if (event === \"PostToolUseFailure\") {\n return payload.error?.slice(0, 120);\n }\n if (event === \"Stop\" || event === \"StopFailure\" || event === \"SubagentStop\") {\n return summarizeText(payload.last_assistant_message);\n }\n if (event === \"SessionEnd\" && payload.reason) {\n return `Claude session ended: ${payload.reason}`;\n }\n return undefined;\n}\n\n// ── Tmux integration ──────────────────────────────────────────────────────────\n\nfunction setTmuxPaneOption(sessionId: string): void {\n const tmuxPane = process.env[\"TMUX_PANE\"];\n if (!tmuxPane) return;\n\n const result = spawnSync(\"tmux\", [\"set-option\", \"-p\", \"-t\", tmuxPane, \"@claude-session\", sessionId], {\n stdio: \"ignore\",\n timeout: 2000,\n });\n\n if (result.error || result.status !== 0) {\n // Non-fatal — tmux may not be available or the pane might have closed\n }\n}\n\nfunction clearTmuxPaneOption(): void {\n const tmuxPane = process.env[\"TMUX_PANE\"];\n if (!tmuxPane) return;\n\n spawnSync(\"tmux\", [\"set-option\", \"-up\", \"-t\", tmuxPane, \"@claude-session\"], {\n stdio: \"ignore\",\n timeout: 2000,\n });\n}\n\n// ── File writing ──────────────────────────────────────────────────────────────\n\ninterface HookStateFile {\n eventType: string;\n eventTime: number;\n hookSeq: number;\n state: SessionState;\n hasAttention: boolean;\n attentionKind: AttentionKind;\n summary?: string;\n activeTool?: string;\n toolName?: string;\n toolInputSummary?: string;\n toolResultStatus?: ToolResultStatus;\n permissionMode?: string;\n startSource?: string;\n endReason?: string;\n errorType?: string;\n agentId?: string;\n agentType?: string;\n transcriptPath?: string;\n currentPath?: string;\n lastActivity: number;\n parentSessionId?: string;\n detectionSource: \"claude-hook\";\n}\n\nfunction readExistingHookState(hooksDir: string, sessionId: string): HookStateFile | null {\n const path = join(hooksDir, `${sessionId}.json`);\n if (!existsSync(path)) return null;\n try {\n return JSON.parse(readFileSync(path, \"utf8\")) as HookStateFile;\n } catch {\n return null;\n }\n}\n\nfunction writeHookFile(hooksDir: string, sessionId: string, data: HookStateFile): void {\n ensureHooksDirSafe(hooksDir);\n const targetPath = join(hooksDir, `${sessionId}.json`);\n const tmpPath = join(hooksDir, `${sessionId}.json.tmp`);\n writeFileSync(tmpPath, JSON.stringify(data), \"utf8\");\n try {\n renameSync(tmpPath, targetPath);\n } catch {\n writeFileSync(targetPath, JSON.stringify(data), \"utf8\");\n }\n}\n\n// ── Main ──────────────────────────────────────────────────────────────────────\n\nasync function main(): Promise<void> {\n let raw = \"\";\n for await (const chunk of Bun.stdin.stream()) {\n raw += new TextDecoder().decode(chunk);\n }\n\n let payload: ClaudeHookPayload;\n try {\n payload = JSON.parse(raw) as ClaudeHookPayload;\n } catch {\n process.exit(0); // Malformed input — exit cleanly so Claude Code is not disrupted\n }\n\n const event = payload.hook_event_name ?? \"\";\n const sessionId = payload.session_id;\n if (!sessionId) process.exit(0);\n\n const hooksDir = resolveHooksDir();\n const now = Date.now();\n\n if (event === \"SessionEnd\") {\n await tryHttpDelivery({\n sessionId,\n eventType: event,\n eventTime: now,\n hookSeq: 0,\n state: \"idle\",\n hasAttention: false,\n attentionKind: \"none\",\n summary: deriveSummary(event, payload),\n ...(payload.reason ? { endReason: payload.reason } : {}),\n ...(payload.transcript_path ? { transcriptPath: payload.transcript_path } : {}),\n currentPath: payload.cwd,\n lastActivity: now,\n detectionSource: \"claude-hook\",\n });\n try {\n rmSync(join(hooksDir, `${sessionId}.json`), { force: true });\n } catch {\n // Ignore cleanup issues\n }\n clearTmuxPaneOption();\n process.exit(0);\n }\n\n if (event === \"SubagentStart\" || event === \"SubagentStop\") {\n const subTranscriptPath = payload.agent_transcript_path ?? payload.subagent_transcript_path;\n const subId = payload.agent_id ?? payload.subagent_session_id ?? (subTranscriptPath ? basename(subTranscriptPath, \".jsonl\") : undefined);\n\n if (subId) {\n const previous = readExistingHookState(hooksDir, subId);\n const derived = deriveHookState(event, payload, previous);\n const subBaseSeq = previous?.hookSeq ?? 0;\n const subNowMs = Math.floor(Date.now() / 100);\n const subHookSeq = Math.max(subBaseSeq + 1, subNowMs);\n const subData: HookStateFile = {\n eventType: event,\n eventTime: now,\n hookSeq: subHookSeq,\n state: derived.state,\n hasAttention: derived.hasAttention,\n attentionKind: derived.attentionKind,\n ...(derived.summary ?? deriveSummary(event, payload) ? { summary: (derived.summary ?? deriveSummary(event, payload)) } : {}),\n ...(payload.permission_mode ? { permissionMode: payload.permission_mode } : {}),\n ...(payload.agent_id ? { agentId: payload.agent_id } : {}),\n ...(payload.agent_type ? { agentType: payload.agent_type } : {}),\n ...(subTranscriptPath ? { transcriptPath: subTranscriptPath } : {}),\n currentPath: payload.cwd,\n lastActivity: now,\n parentSessionId: `claude:${sessionId}`,\n detectionSource: \"claude-hook\",\n };\n writeHookFile(hooksDir, subId, subData);\n await tryHttpDelivery({ sessionId: subId, ...subData });\n }\n\n if (event === \"SubagentStop\") {\n // Update the parent session metadata (summary, lastActivity) but preserve\n // its state — SubagentStop is a sub-agent event, not a parent state change.\n // deriveHookState(\"SubagentStop\") returns \"idle\" which would incorrectly\n // set the parent to idle while it may still be working.\n const parentPrevious = readExistingHookState(hooksDir, sessionId);\n const parentBaseSeq = parentPrevious?.hookSeq ?? 0;\n const parentNowMs = Math.floor(Date.now() / 100);\n const parentHookSeq = Math.max(parentBaseSeq + 1, parentNowMs);\n const parentSummary = deriveSummary(\"SubagentStop\", payload);\n const parentData: HookStateFile = {\n eventType: \"SubagentStop\",\n eventTime: now,\n hookSeq: parentHookSeq,\n state: parentPrevious?.state ?? \"working\",\n hasAttention: parentPrevious?.hasAttention ?? false,\n attentionKind: parentPrevious?.attentionKind ?? \"none\",\n ...(parentSummary ? { summary: parentSummary } : {}),\n currentPath: payload.new_cwd ?? payload.cwd,\n lastActivity: now,\n detectionSource: \"claude-hook\",\n };\n writeHookFile(hooksDir, sessionId, parentData);\n await tryHttpDelivery({ sessionId, ...parentData });\n // Ensure tmux pane option is refreshed so the parent session remains linked\n setTmuxPaneOption(sessionId);\n process.exit(0);\n }\n }\n\n // For all other events: write state for the current session\n const previous = readExistingHookState(hooksDir, sessionId);\n const derived = deriveHookState(event, payload, previous);\n const summary = derived.summary ?? deriveSummary(event, payload) ?? previous?.summary;\n\n // Use a timestamp-based hookSeq to avoid collisions when multiple hooks\n // fire concurrently. The previous seq is used as a floor to ensure monotonicity.\n const baseSeq = previous?.hookSeq ?? 0;\n const nowMs = Math.floor(Date.now() / 100);\n const hookSeq = Math.max(baseSeq + 1, nowMs);\n\n const hookData: HookStateFile = {\n eventType: event,\n eventTime: now,\n hookSeq,\n state: derived.state,\n hasAttention: derived.hasAttention,\n attentionKind: derived.attentionKind,\n ...(summary ? { summary } : {}),\n ...(derived.activeTool ? { activeTool: derived.activeTool } : {}),\n ...(derived.toolName ? { toolName: derived.toolName } : {}),\n ...(derived.toolInputSummary ? { toolInputSummary: derived.toolInputSummary } : {}),\n ...(derived.toolResultStatus ? { toolResultStatus: derived.toolResultStatus } : {}),\n ...(payload.permission_mode ? { permissionMode: payload.permission_mode } : {}),\n ...(payload.source ? { startSource: payload.source } : {}),\n ...(payload.reason ? { endReason: payload.reason } : {}),\n ...(payload.error ? { errorType: payload.error } : {}),\n ...(payload.agent_id ? { agentId: payload.agent_id } : {}),\n ...(payload.agent_type ? { agentType: payload.agent_type } : {}),\n ...(payload.transcript_path ? { transcriptPath: payload.transcript_path } : {}),\n currentPath: payload.new_cwd ?? payload.cwd,\n lastActivity: now,\n detectionSource: \"claude-hook\",\n };\n\n writeHookFile(hooksDir, sessionId, hookData);\n await tryHttpDelivery({ sessionId, ...hookData });\n\n // Update tmux binding for the main session, especially on SessionStart or resume.\n if (event === \"SessionStart\" || event === \"UserPromptSubmit\" || event === \"PermissionRequest\" || event === \"PreToolUse\") {\n setTmuxPaneOption(sessionId);\n }\n\n process.exit(0);\n}\n\nmain().catch(() => process.exit(0));\n"
|
|
5
|
+
"import { chmodSync, lstatSync, mkdirSync, readFileSync, rmSync } from 'node:fs';\nimport { tmpdir, userInfo } from 'node:os';\nimport { join } from 'node:path';\n\n/** True on Windows where Unix sockets are not available */\nexport const IS_WINDOWS = process.platform === 'win32';\n\n/** True on macOS */\nexport const IS_MAC = process.platform === 'darwin';\n\n/** True on Linux (including WSL) */\nexport const IS_LINUX = process.platform === 'linux';\n\n/** True when running inside Windows Subsystem for Linux */\nexport const IS_WSL = ((): boolean => {\n if (!IS_LINUX) return false;\n try {\n const version = readFileSync('/proc/version', 'utf8');\n return /microsoft|wsl/i.test(version);\n } catch {\n return false;\n }\n})();\n\n/**\n * Resolve the path to an executable using the platform-appropriate command.\n * Prefers Bun.which when available, falling back to `which` on Unix and `where` on Windows.\n * @param name - The name of the executable to resolve.\n */\nexport function whichCommand(name: string): string | null {\n const viaBun = (\n Bun as typeof Bun & { which?: (name: string) => string | null }\n ).which?.(name);\n if (viaBun) return viaBun;\n\n if (IS_WINDOWS) {\n return spawnAndRead('where', [name]);\n }\n return spawnAndRead('which', [name]);\n}\n\nfunction spawnAndRead(command: string, args: string[]): string | null {\n try {\n const proc = Bun.spawnSync([command, ...args], {\n stderr: 'ignore',\n stdout: 'pipe',\n });\n const text = new TextDecoder().decode(proc.stdout).trim();\n return text ? (text.split(/\\r?\\n/)[0] ?? text) : null;\n } catch {\n return null;\n }\n}\n\n/**\n * Return the current user's id, used to scope shared /tmp paths so multiple\n * users on the same host don't conflict or collide on the same path.\n */\nexport function getUserId(): number | string {\n try {\n return userInfo().uid;\n } catch {\n // Fallback: use a hash of the username\n return process.env.USER ?? process.env.USERNAME ?? 'default';\n }\n}\n\n/**\n * Return a stable hooks directory path for the given hook name.\n * On Unix we intentionally avoid os.tmpdir() because macOS per-session temp paths\n * would break rendezvous between independently-launched daemon and hook binaries.\n * The path is scoped by uid so multiple users on the same host don't conflict.\n * On Windows we use os.tmpdir() because %TEMP% is stable per user session and\n * already per-user.\n *\n * This function is pure (no I/O). Callers that need to actually create the\n * directory should use `ensureHooksDirSafe`.\n * @param name - The name of the hook, used to create a unique directory for it.\n */\nexport function getHooksDir(name: string): string {\n if (IS_WINDOWS) {\n return join(tmpdir(), `scrylog-${name}`);\n }\n return join('/tmp', `scrylog-${getUserId()}-${name}`);\n}\n\nfunction isOwnedDirectory(dir: string): boolean {\n const stat = lstatSync(dir);\n return stat.isDirectory() && stat.uid === process.getuid?.();\n}\n\n/**\n * Ensure `dir` exists and, on Unix, is a real directory (not a symlink)\n * owned by the current user, tightening its mode to 0700. A pre-existing\n * path that isn't a directory we own — e.g. a symlink or a directory\n * planted by another local user on a shared host — is untrusted: it is\n * removed and recreated rather than written into. A directory we already\n * own is tightened in place (not recreated) so existing session state isn't\n * discarded. Ownership/mode checks are skipped on Windows, where\n * `getHooksDir` already resolves to a per-user temp directory.\n * @param dir - The path to the hooks directory to ensure is safe.\n * @returns\n */\nexport function ensureHooksDirSafe(dir: string): void {\n mkdirSync(dir, { mode: 0o700, recursive: true });\n\n if (IS_WINDOWS) return;\n\n if (!isOwnedDirectory(dir)) {\n rmSync(dir, { force: true, recursive: true });\n mkdirSync(dir, { mode: 0o700, recursive: true });\n\n if (!isOwnedDirectory(dir)) {\n throw new Error(`Refusing to use untrusted hooks directory: ${dir}`);\n }\n }\n\n chmodSync(dir, 0o700);\n}\n",
|
|
6
|
+
"#!/usr/bin/env bun\n/**\n * scrylog-claude-hook — Claude Code lifecycle hook handler.\n *\n * Claude Code calls this binary at each lifecycle event (Stop, PreToolUse,\n * PostToolUse, Notification, SubagentStop) with a JSON payload on stdin.\n * This script writes a state file for the ClaudeCollector to pick up and,\n * when running inside tmux, sets the \\@claude-session pane option so the\n * TmuxCollector can link the pane to the Claude session.\n *\n * Configure in ~/.claude/settings.json via:\n * scrylog claude hooks install\n */\n\nimport { basename, join } from 'node:path';\nimport {\n existsSync,\n readFileSync,\n renameSync,\n rmSync,\n writeFileSync,\n} from 'node:fs';\nimport { spawnSync } from 'node:child_process';\n\nimport { ensureHooksDirSafe, getHooksDir } from '../shared/platform';\nimport type { SessionState } from '../shared/types';\n\ntype AttentionKind = 'permission' | 'question' | 'turn' | 'none';\ntype ToolResultStatus =\n | 'started'\n | 'completed'\n | 'error'\n | 'blocked'\n | 'denied'\n | 'unknown';\n\n// ── Hook payload ──────────────────────────────────────────────────────────────\n\ninterface ClaudeHookPayload {\n session_id: string;\n transcript_path?: string;\n cwd?: string;\n permission_mode?: string;\n hook_event_name?: string;\n source?: string;\n model?: string;\n prompt?: string;\n stop_hook_active?: boolean;\n last_assistant_message?: string;\n // PreToolUse / PostToolUse\n tool_name?: string;\n tool_input?: Record<string, unknown>;\n tool_response?: unknown;\n tool_use_id?: string;\n duration_ms?: number;\n error?: string;\n error_details?: string;\n is_interrupt?: boolean;\n reason?: string;\n old_cwd?: string;\n new_cwd?: string;\n permission_suggestions?: unknown[];\n tool_calls?: Array<{\n tool_name?: string;\n tool_input?: Record<string, unknown>;\n tool_use_id?: string;\n tool_response?: unknown;\n }>;\n // Notification / async events\n message?: string;\n title?: string;\n // Subagents\n agent_id?: string;\n agent_type?: string;\n agent_transcript_path?: string;\n subagent_session_id?: string;\n subagent_transcript_path?: string;\n}\n\ninterface DerivedHookState {\n state: SessionState;\n hasAttention: boolean;\n attentionKind: AttentionKind;\n activeTool?: string;\n toolName?: string;\n toolInputSummary?: string;\n toolResultStatus?: ToolResultStatus;\n summary?: string;\n}\n\n// ── Config resolution ─────────────────────────────────────────────────────────\n\nfunction resolveHooksDir(): string {\n const envConfig = process.env['SCRYLOG_CONFIG'];\n if (envConfig && existsSync(envConfig)) {\n try {\n const raw = JSON.parse(readFileSync(envConfig, 'utf8')) as Record<\n string,\n unknown\n >;\n const claude = raw['claude'] as Record<string, unknown> | undefined;\n if (typeof claude?.['hooksDir'] === 'string') return claude['hooksDir'];\n } catch {\n // fall through to default\n }\n }\n return getHooksDir('claude-hooks');\n}\n\nfunction resolveDaemonPort(): number {\n const envConfig = process.env['SCRYLOG_CONFIG'];\n if (envConfig && existsSync(envConfig)) {\n try {\n const raw = JSON.parse(readFileSync(envConfig, 'utf8')) as Record<\n string,\n unknown\n >;\n const api = raw['api'] as Record<string, unknown> | undefined;\n if (typeof api?.['port'] === 'number') return api['port'] as number;\n } catch {\n // fall through to default\n }\n }\n return 7788;\n}\n\nasync function tryHttpDelivery(\n payload: HookStateFile & { sessionId: string }\n): Promise<boolean> {\n try {\n const port = resolveDaemonPort();\n const res = await fetch(`http://127.0.0.1:${port}/api/webhook/claude`, {\n body: JSON.stringify(payload),\n headers: { 'Content-Type': 'application/json' },\n method: 'POST',\n signal: AbortSignal.timeout(1500),\n });\n return res.ok;\n } catch {\n return false;\n }\n}\n\n// ── Transcript inspection ─────────────────────────────────────────────────────\n\n// oxlint-disable-next-line no-unused-vars\nfunction inferPermissionModeFromTranscript(\n transcriptPath?: string\n): string | undefined {\n if (!transcriptPath) return undefined;\n try {\n const content = readFileSync(transcriptPath, 'utf8');\n const lines = content.split(/\\r?\\n/).filter(Boolean);\n for (let i = lines.length - 1; i >= 0; i--) {\n let entry: Record<string, unknown>;\n try {\n entry = JSON.parse(lines[i]!) as Record<string, unknown>;\n } catch {\n continue;\n }\n if (entry['type'] !== 'permission-mode') continue;\n const mode = entry['permissionMode'];\n if (typeof mode === 'string' && mode.length > 0) return mode;\n }\n } catch {\n // Ignore transcript read issues\n }\n return undefined;\n}\n\nfunction summarizeToolInput(\n toolName?: string,\n input?: Record<string, unknown>\n): string | undefined {\n if (!toolName) return undefined;\n const command =\n typeof input?.['command'] === 'string'\n ? input['command'].slice(0, 120)\n : undefined;\n if (command) return command;\n const filePath =\n typeof input?.['file_path'] === 'string' ? input['file_path'] : undefined;\n if (filePath) return filePath;\n const description =\n typeof input?.['description'] === 'string'\n ? input['description']\n : undefined;\n if (description) return description.slice(0, 120);\n return undefined;\n}\n\nfunction summarizeText(text?: string): string | undefined {\n if (!text) return undefined;\n const first = text.trim().split('\\n')[0] ?? '';\n return first.length > 0 ? first.slice(0, 120) : undefined;\n}\n\nfunction isQuestion(text?: string): boolean {\n if (!text) return false;\n const trimmed = text.trim();\n return (\n /\\?/.test(trimmed) ||\n /^(would you|do you|can you|should i|which|what|how|please confirm|shall i|want me to)/i.test(\n trimmed\n )\n );\n}\n\n// oxlint-disable-next-line no-unused-vars\nfunction isInterruptedRequestText(text: string): boolean {\n return /^\\[Request interrupted by user(?: for tool use)?\\]$/i.test(\n text.trim()\n );\n}\n\nfunction classifyAttention(message?: string, title?: string): AttentionKind {\n const text = `${title ?? ''} ${message ?? ''}`.trim();\n if (!text) return 'turn';\n if (/(approval|approve|permission|confirm|proceed|allow)/i.test(text))\n return 'permission';\n if (\n /[?]/.test(text) ||\n /^(would you|do you|can you|should i|which|what|how|shall i|want me to)/i.test(\n text\n )\n ) {\n return 'question';\n }\n return 'turn';\n}\n\nfunction inferStopState(payload: ClaudeHookPayload): SessionState {\n if (isQuestion(payload.last_assistant_message)) return 'waiting-input';\n return 'idle';\n}\n\n// ── State derivation ──────────────────────────────────────────────────────────\n\ninterface PreviousState {\n state?: SessionState;\n hasAttention?: boolean;\n attentionKind?: AttentionKind;\n}\n\nfunction deriveHookState(\n event: string,\n payload: ClaudeHookPayload,\n previous?: PreviousState | null\n): DerivedHookState {\n const toolInputSummary = summarizeToolInput(\n payload.tool_name,\n payload.tool_input\n );\n switch (event) {\n case 'SessionStart':\n return {\n attentionKind: 'none',\n hasAttention: false,\n state: 'idle',\n summary: payload.source\n ? `Claude session ${payload.source}`\n : 'Claude session started',\n };\n case 'UserPromptSubmit':\n return {\n attentionKind: 'none',\n hasAttention: false,\n state: 'working',\n summary: summarizeText(payload.prompt) ?? 'Claude is working',\n };\n case 'PreToolUse':\n return {\n activeTool: payload.tool_name,\n attentionKind: 'none',\n hasAttention: false,\n state: 'working',\n summary: payload.tool_name\n ? `Using ${payload.tool_name}${toolInputSummary ? `: ${toolInputSummary}` : ''}`\n : undefined,\n toolInputSummary,\n toolName: payload.tool_name,\n toolResultStatus: 'started',\n };\n case 'PermissionRequest':\n return {\n activeTool: payload.tool_name,\n attentionKind: 'permission',\n hasAttention: true,\n state: 'waiting-input',\n summary: payload.tool_name\n ? `Permission required for ${payload.tool_name}${toolInputSummary ? `: ${toolInputSummary}` : ''}`\n : 'Permission required',\n toolInputSummary,\n toolName: payload.tool_name,\n toolResultStatus: 'blocked',\n };\n case 'PermissionDenied':\n return {\n activeTool: payload.tool_name,\n attentionKind: 'none',\n hasAttention: false,\n state: 'working',\n summary:\n payload.reason?.slice(0, 120) ??\n (payload.tool_name\n ? `Permission denied for ${payload.tool_name}`\n : 'Permission denied'),\n toolInputSummary,\n toolName: payload.tool_name,\n toolResultStatus: 'denied',\n };\n case 'PostToolUse':\n return {\n activeTool: payload.tool_name,\n attentionKind: 'none',\n hasAttention: false,\n state: 'working',\n summary: payload.tool_name\n ? `Using ${payload.tool_name}${toolInputSummary ? `: ${toolInputSummary}` : ''}`\n : undefined,\n toolInputSummary,\n toolName: payload.tool_name,\n toolResultStatus:\n typeof payload.tool_response === 'object' &&\n payload.tool_response !== null &&\n 'is_error' in (payload.tool_response as Record<string, unknown>)\n ? (payload.tool_response as Record<string, unknown>)['is_error'] ===\n true\n ? 'error'\n : 'completed'\n : 'completed',\n };\n case 'PostToolUseFailure':\n return {\n activeTool: payload.tool_name,\n attentionKind: 'none',\n hasAttention: false,\n state: payload.is_interrupt ? 'idle' : 'working',\n summary:\n payload.error?.slice(0, 120) ??\n (payload.tool_name ? `Failed ${payload.tool_name}` : 'Tool failed'),\n toolInputSummary,\n toolName: payload.tool_name,\n toolResultStatus: 'error',\n };\n case 'PostToolBatch':\n return {\n attentionKind: 'none',\n hasAttention: false,\n state: 'working',\n summary: 'Processing tool results',\n };\n case 'Stop': {\n const state = inferStopState(payload);\n return {\n attentionKind: state === 'waiting-input' ? 'question' : 'none',\n hasAttention: state === 'waiting-input',\n state,\n summary: summarizeText(payload.last_assistant_message),\n };\n }\n case 'StopFailure':\n return {\n attentionKind: 'turn',\n hasAttention: true,\n state: 'error',\n summary:\n summarizeText(payload.last_assistant_message) ??\n payload.error_details?.slice(0, 120) ??\n payload.error?.slice(0, 120),\n };\n case 'CwdChanged':\n return {\n attentionKind: previous?.attentionKind ?? 'none',\n hasAttention: previous?.hasAttention ?? false,\n state: previous?.state ?? 'idle',\n };\n case 'SubagentStart':\n return {\n attentionKind: 'none',\n hasAttention: false,\n state: 'working',\n summary: payload.agent_type\n ? `Subagent ${payload.agent_type}`\n : 'Subagent started',\n };\n case 'SubagentStop':\n return {\n attentionKind: 'none',\n hasAttention: false,\n state: 'idle',\n summary: summarizeText(payload.last_assistant_message),\n };\n case 'SessionEnd':\n return {\n attentionKind: 'none',\n hasAttention: false,\n state: 'idle',\n summary: payload.reason\n ? `Claude session ended: ${payload.reason}`\n : 'Claude session ended',\n };\n case 'Notification':\n return {\n attentionKind: classifyAttention(payload.message, payload.title),\n hasAttention: true,\n state: previous?.state ?? 'idle',\n summary: summarizeText(payload.message ?? payload.title),\n };\n default:\n return {\n attentionKind: previous?.attentionKind ?? 'none',\n hasAttention: previous?.hasAttention ?? false,\n state: previous?.state ?? 'idle',\n };\n }\n}\n\nfunction deriveSummary(\n event: string,\n payload: ClaudeHookPayload\n): string | undefined {\n if (event === 'UserPromptSubmit' && payload.prompt) {\n return summarizeText(payload.prompt);\n }\n if (event === 'SessionStart' && payload.source) {\n return `Claude session ${payload.source}`;\n }\n if (event === 'PermissionRequest' && payload.tool_name) {\n const target = summarizeToolInput(payload.tool_name, payload.tool_input);\n return `Permission required for ${payload.tool_name}${target ? `: ${target}` : ''}`;\n }\n if (event === 'PermissionDenied') {\n return payload.reason?.slice(0, 120);\n }\n if (\n (event === 'PreToolUse' || event === 'PostToolUse') &&\n payload.tool_name\n ) {\n const cmd = (payload.tool_input?.['command'] as string | undefined)?.slice(\n 0,\n 60\n );\n return cmd\n ? `Using ${payload.tool_name}: ${cmd}`\n : `Using ${payload.tool_name}`;\n }\n if (event === 'PostToolUseFailure') {\n return payload.error?.slice(0, 120);\n }\n if (event === 'Stop' || event === 'StopFailure' || event === 'SubagentStop') {\n return summarizeText(payload.last_assistant_message);\n }\n if (event === 'SessionEnd' && payload.reason) {\n return `Claude session ended: ${payload.reason}`;\n }\n return undefined;\n}\n\n// ── Tmux integration ──────────────────────────────────────────────────────────\n\nfunction setTmuxPaneOption(sessionId: string): void {\n const tmuxPane = process.env['TMUX_PANE'];\n if (!tmuxPane) return;\n\n const result = spawnSync(\n 'tmux',\n ['set-option', '-p', '-t', tmuxPane, '@claude-session', sessionId],\n {\n stdio: 'ignore',\n timeout: 2000,\n }\n );\n\n if (result.error || result.status !== 0) {\n // Non-fatal — tmux may not be available or the pane might have closed\n }\n}\n\nfunction clearTmuxPaneOption(): void {\n const tmuxPane = process.env['TMUX_PANE'];\n if (!tmuxPane) return;\n\n spawnSync('tmux', ['set-option', '-up', '-t', tmuxPane, '@claude-session'], {\n stdio: 'ignore',\n timeout: 2000,\n });\n}\n\n// ── File writing ──────────────────────────────────────────────────────────────\n\ninterface HookStateFile {\n eventType: string;\n eventTime: number;\n hookSeq: number;\n state: SessionState;\n hasAttention: boolean;\n attentionKind: AttentionKind;\n summary?: string;\n activeTool?: string;\n toolName?: string;\n toolInputSummary?: string;\n toolResultStatus?: ToolResultStatus;\n permissionMode?: string;\n startSource?: string;\n endReason?: string;\n errorType?: string;\n agentId?: string;\n agentType?: string;\n transcriptPath?: string;\n currentPath?: string;\n lastActivity: number;\n parentSessionId?: string;\n detectionSource: 'claude-hook';\n}\n\nfunction readExistingHookState(\n hooksDir: string,\n sessionId: string\n): HookStateFile | null {\n const path = join(hooksDir, `${sessionId}.json`);\n if (!existsSync(path)) return null;\n try {\n return JSON.parse(readFileSync(path, 'utf8')) as HookStateFile;\n } catch {\n return null;\n }\n}\n\nfunction writeHookFile(\n hooksDir: string,\n sessionId: string,\n data: HookStateFile\n): void {\n ensureHooksDirSafe(hooksDir);\n const targetPath = join(hooksDir, `${sessionId}.json`);\n const tmpPath = join(hooksDir, `${sessionId}.json.tmp`);\n writeFileSync(tmpPath, JSON.stringify(data), 'utf8');\n try {\n renameSync(tmpPath, targetPath);\n } catch {\n writeFileSync(targetPath, JSON.stringify(data), 'utf8');\n }\n}\n\n// ── Main ──────────────────────────────────────────────────────────────────────\n\nasync function main(): Promise<void> {\n let raw = '';\n for await (const chunk of Bun.stdin.stream()) {\n raw += new TextDecoder().decode(chunk);\n }\n\n let payload: ClaudeHookPayload;\n try {\n payload = JSON.parse(raw) as ClaudeHookPayload;\n } catch {\n process.exit(0); // Malformed input — exit cleanly so Claude Code is not disrupted\n }\n\n const event = payload.hook_event_name ?? '';\n const sessionId = payload.session_id;\n if (!sessionId) process.exit(0);\n\n const hooksDir = resolveHooksDir();\n const now = Date.now();\n\n if (event === 'SessionEnd') {\n await tryHttpDelivery({\n attentionKind: 'none',\n detectionSource: 'claude-hook',\n eventTime: now,\n eventType: event,\n hasAttention: false,\n hookSeq: 0,\n sessionId,\n state: 'idle',\n summary: deriveSummary(event, payload),\n ...(payload.reason ? { endReason: payload.reason } : {}),\n ...(payload.transcript_path\n ? { transcriptPath: payload.transcript_path }\n : {}),\n currentPath: payload.cwd,\n lastActivity: now,\n });\n try {\n rmSync(join(hooksDir, `${sessionId}.json`), { force: true });\n } catch {\n // Ignore cleanup issues\n }\n clearTmuxPaneOption();\n process.exit(0);\n }\n\n if (event === 'SubagentStart' || event === 'SubagentStop') {\n const subTranscriptPath =\n payload.agent_transcript_path ?? payload.subagent_transcript_path;\n const subId =\n payload.agent_id ??\n payload.subagent_session_id ??\n (subTranscriptPath ? basename(subTranscriptPath, '.jsonl') : undefined);\n\n if (subId) {\n const previous = readExistingHookState(hooksDir, subId);\n const derived = deriveHookState(event, payload, previous);\n const subBaseSeq = previous?.hookSeq ?? 0;\n const subNowMs = Math.floor(Date.now() / 100);\n const subHookSeq = Math.max(subBaseSeq + 1, subNowMs);\n const subData: HookStateFile = {\n attentionKind: derived.attentionKind,\n detectionSource: 'claude-hook',\n eventTime: now,\n eventType: event,\n hasAttention: derived.hasAttention,\n hookSeq: subHookSeq,\n state: derived.state,\n ...((derived.summary ?? deriveSummary(event, payload))\n ? { summary: derived.summary ?? deriveSummary(event, payload) }\n : {}),\n ...(payload.permission_mode\n ? { permissionMode: payload.permission_mode }\n : {}),\n ...(payload.agent_id ? { agentId: payload.agent_id } : {}),\n ...(payload.agent_type ? { agentType: payload.agent_type } : {}),\n ...(subTranscriptPath ? { transcriptPath: subTranscriptPath } : {}),\n currentPath: payload.cwd,\n lastActivity: now,\n parentSessionId: `claude:${sessionId}`,\n };\n writeHookFile(hooksDir, subId, subData);\n await tryHttpDelivery({ sessionId: subId, ...subData });\n }\n\n if (event === 'SubagentStop') {\n // Update the parent session metadata (summary, lastActivity) but preserve\n // its state — SubagentStop is a sub-agent event, not a parent state change.\n // deriveHookState(\"SubagentStop\") returns \"idle\" which would incorrectly\n // set the parent to idle while it may still be working.\n const parentPrevious = readExistingHookState(hooksDir, sessionId);\n const parentBaseSeq = parentPrevious?.hookSeq ?? 0;\n const parentNowMs = Math.floor(Date.now() / 100);\n const parentHookSeq = Math.max(parentBaseSeq + 1, parentNowMs);\n const parentSummary = deriveSummary('SubagentStop', payload);\n const parentData: HookStateFile = {\n attentionKind: parentPrevious?.attentionKind ?? 'none',\n detectionSource: 'claude-hook',\n eventTime: now,\n eventType: 'SubagentStop',\n hasAttention: parentPrevious?.hasAttention ?? false,\n hookSeq: parentHookSeq,\n state: parentPrevious?.state ?? 'working',\n ...(parentSummary ? { summary: parentSummary } : {}),\n currentPath: payload.new_cwd ?? payload.cwd,\n lastActivity: now,\n };\n writeHookFile(hooksDir, sessionId, parentData);\n await tryHttpDelivery({ sessionId, ...parentData });\n // Ensure tmux pane option is refreshed so the parent session remains linked\n setTmuxPaneOption(sessionId);\n process.exit(0);\n }\n }\n\n // For all other events: write state for the current session\n const previous = readExistingHookState(hooksDir, sessionId);\n const derived = deriveHookState(event, payload, previous);\n const summary =\n derived.summary ?? deriveSummary(event, payload) ?? previous?.summary;\n\n // Use a timestamp-based hookSeq to avoid collisions when multiple hooks\n // fire concurrently. The previous seq is used as a floor to ensure monotonicity.\n const baseSeq = previous?.hookSeq ?? 0;\n const nowMs = Math.floor(Date.now() / 100);\n const hookSeq = Math.max(baseSeq + 1, nowMs);\n\n const hookData: HookStateFile = {\n attentionKind: derived.attentionKind,\n detectionSource: 'claude-hook',\n eventTime: now,\n eventType: event,\n hasAttention: derived.hasAttention,\n hookSeq,\n state: derived.state,\n ...(summary ? { summary } : {}),\n ...(derived.activeTool ? { activeTool: derived.activeTool } : {}),\n ...(derived.toolName ? { toolName: derived.toolName } : {}),\n ...(derived.toolInputSummary\n ? { toolInputSummary: derived.toolInputSummary }\n : {}),\n ...(derived.toolResultStatus\n ? { toolResultStatus: derived.toolResultStatus }\n : {}),\n ...(payload.permission_mode\n ? { permissionMode: payload.permission_mode }\n : {}),\n ...(payload.source ? { startSource: payload.source } : {}),\n ...(payload.reason ? { endReason: payload.reason } : {}),\n ...(payload.error ? { errorType: payload.error } : {}),\n ...(payload.agent_id ? { agentId: payload.agent_id } : {}),\n ...(payload.agent_type ? { agentType: payload.agent_type } : {}),\n ...(payload.transcript_path\n ? { transcriptPath: payload.transcript_path }\n : {}),\n currentPath: payload.new_cwd ?? payload.cwd,\n lastActivity: now,\n };\n\n writeHookFile(hooksDir, sessionId, hookData);\n await tryHttpDelivery({ sessionId, ...hookData });\n\n // Update tmux binding for the main session, especially on SessionStart or resume.\n if (\n event === 'SessionStart' ||\n event === 'UserPromptSubmit' ||\n event === 'PermissionRequest' ||\n event === 'PreToolUse'\n ) {\n setTmuxPaneOption(sessionId);\n }\n\n process.exit(0);\n}\n\nmain().catch(() => process.exit(0));\n"
|
|
7
7
|
],
|
|
8
|
-
"mappings": ";;g9BAAA,
|
|
9
|
-
"debugId": "
|
|
8
|
+
"mappings": ";;g9BAAA,oBAAS,eAAW,gBAAW,kBAAW,aAAc,YACxD,iBAAS,eAAQ,YACjB,eAAS,aA2BF,SAAS,EAAY,CAAC,EAA6B,CACxD,IAAM,EACJ,IACA,QAAQ,CAAI,EACd,GAAI,EAAQ,OAAO,EAEnB,GAAI,EACF,OAAO,EAAa,QAAS,CAAC,CAAI,CAAC,EAErC,OAAO,EAAa,QAAS,CAAC,CAAI,CAAC,EAGrC,SAAS,CAAY,CAAC,EAAiB,EAA+B,CACpE,GAAI,CACF,IAAM,EAAO,IAAI,UAAU,CAAC,EAAS,GAAG,CAAI,EAAG,CAC7C,OAAQ,SACR,OAAQ,MACV,CAAC,EACK,EAAO,IAAI,YAAY,EAAE,OAAO,EAAK,MAAM,EAAE,KAAK,EACxD,OAAO,EAAQ,EAAK,MAAM,OAAO,EAAE,IAAM,EAAQ,KACjD,KAAM,CACN,OAAO,MAQJ,SAAS,EAAS,EAAoB,CAC3C,GAAI,CACF,OAAO,GAAS,EAAE,IAClB,KAAM,CAEN,OAAO,QAAQ,IAAI,MAAQ,QAAQ,IAAI,UAAY,WAgBhD,SAAS,CAAW,CAAC,EAAsB,CAChD,GAAI,EACF,OAAO,EAAK,GAAO,EAAG,WAAW,GAAM,EAEzC,OAAO,EAAK,OAAQ,WAAW,GAAU,KAAK,GAAM,EAGtD,SAAS,CAAgB,CAAC,EAAsB,CAC9C,IAAM,EAAO,GAAU,CAAG,EAC1B,OAAO,EAAK,YAAY,GAAK,EAAK,MAAQ,QAAQ,SAAS,EAetD,SAAS,CAAkB,CAAC,EAAmB,CAGpD,GAFA,EAAU,EAAK,CAAE,KAAM,IAAO,UAAW,EAAK,CAAC,EAE3C,EAAY,OAEhB,GAAI,CAAC,EAAiB,CAAG,GAIvB,GAHA,GAAO,EAAK,CAAE,MAAO,GAAM,UAAW,EAAK,CAAC,EAC5C,EAAU,EAAK,CAAE,KAAM,IAAO,UAAW,EAAK,CAAC,EAE3C,CAAC,EAAiB,CAAG,EACvB,MAAU,MAAM,8CAA8C,GAAK,EAIvE,EAAU,EAAK,GAAK,MAhHT,EAGA,GAGA,GAGA,gBATA,EAAa,QAAQ,WAAa,QAGlC,GAAS,QAAQ,WAAa,SAG9B,GAAW,QAAQ,WAAa,QAGhC,IAAU,IAAe,CACpC,GAAI,CAAC,GAAU,MAAO,GACtB,GAAI,CACF,IAAM,EAAU,GAAa,gBAAiB,MAAM,EACpD,MAAO,iBAAiB,KAAK,CAAO,EACpC,KAAM,CACN,MAAO,MAER,ICEH,IAVA,mBAAS,WAAU,aACnB,qBACE,kBACA,gBACA,aACA,oBACA,WAEF,oBAAS,sBAsET,SAAS,EAAe,EAAW,CACjC,IAAM,EAAY,QAAQ,IAAI,eAC9B,GAAI,GAAa,EAAW,CAAS,EACnC,GAAI,CAKF,IAAM,EAJM,KAAK,MAAM,EAAa,EAAW,MAAM,CAAC,EAInC,OACnB,GAAI,OAAO,GAAS,WAAgB,SAAU,OAAO,EAAO,SAC5D,KAAM,EAIV,OAAO,EAAY,cAAc,EAGnC,SAAS,EAAiB,EAAW,CACnC,IAAM,EAAY,QAAQ,IAAI,eAC9B,GAAI,GAAa,EAAW,CAAS,EACnC,GAAI,CAKF,IAAM,EAJM,KAAK,MAAM,EAAa,EAAW,MAAM,CAAC,EAItC,IAChB,GAAI,OAAO,GAAM,OAAY,SAAU,OAAO,EAAI,KAClD,KAAM,EAIV,MAAO,MAGT,eAAe,CAAe,CAC5B,EACkB,CAClB,GAAI,CACF,IAAM,EAAO,GAAkB,EAO/B,OANY,MAAM,MAAM,oBAAoB,uBAA2B,CACrE,KAAM,KAAK,UAAU,CAAO,EAC5B,QAAS,CAAE,eAAgB,kBAAmB,EAC9C,OAAQ,OACR,OAAQ,YAAY,QAAQ,IAAI,CAClC,CAAC,GACU,GACX,KAAM,CACN,MAAO,IA+BX,SAAS,CAAkB,CACzB,EACA,EACoB,CACpB,GAAI,CAAC,EAAU,OACf,IAAM,EACJ,OAAO,GAAQ,UAAe,SAC1B,EAAM,QAAW,MAAM,EAAG,GAAG,EAC7B,OACN,GAAI,EAAS,OAAO,EACpB,IAAM,EACJ,OAAO,GAAQ,YAAiB,SAAW,EAAM,UAAe,OAClE,GAAI,EAAU,OAAO,EACrB,IAAM,EACJ,OAAO,GAAQ,cAAmB,SAC9B,EAAM,YACN,OACN,GAAI,EAAa,OAAO,EAAY,MAAM,EAAG,GAAG,EAChD,OAGF,SAAS,CAAa,CAAC,EAAmC,CACxD,GAAI,CAAC,EAAM,OACX,IAAM,EAAQ,EAAK,KAAK,EAAE,MAAM;AAAA,CAAI,EAAE,IAAM,GAC5C,OAAO,EAAM,OAAS,EAAI,EAAM,MAAM,EAAG,GAAG,EAAI,OAGlD,SAAS,EAAU,CAAC,EAAwB,CAC1C,GAAI,CAAC,EAAM,MAAO,GAClB,IAAM,EAAU,EAAK,KAAK,EAC1B,MACE,KAAK,KAAK,CAAO,GACjB,yFAAyF,KACvF,CACF,EAWJ,SAAS,EAAiB,CAAC,EAAkB,EAA+B,CAC1E,IAAM,EAAO,GAAG,GAAS,MAAM,GAAW,KAAK,KAAK,EACpD,GAAI,CAAC,EAAM,MAAO,OAClB,GAAI,uDAAuD,KAAK,CAAI,EAClE,MAAO,aACT,GACE,MAAM,KAAK,CAAI,GACf,0EAA0E,KACxE,CACF,EAEA,MAAO,WAET,MAAO,OAGT,SAAS,EAAc,CAAC,EAA0C,CAChE,GAAI,GAAW,EAAQ,sBAAsB,EAAG,MAAO,gBACvD,MAAO,OAWT,SAAS,CAAe,CACtB,EACA,EACA,EACkB,CAClB,IAAM,EAAmB,EACvB,EAAQ,UACR,EAAQ,UACV,EACA,OAAQ,OACD,eACH,MAAO,CACL,cAAe,OACf,aAAc,GACd,MAAO,OACP,QAAS,EAAQ,OACb,kBAAkB,EAAQ,SAC1B,wBACN,MACG,mBACH,MAAO,CACL,cAAe,OACf,aAAc,GACd,MAAO,UACP,QAAS,EAAc,EAAQ,MAAM,GAAK,mBAC5C,MACG,aACH,MAAO,CACL,WAAY,EAAQ,UACpB,cAAe,OACf,aAAc,GACd,MAAO,UACP,QAAS,EAAQ,UACb,SAAS,EAAQ,YAAY,EAAmB,KAAK,IAAqB,KAC1E,OACJ,mBACA,SAAU,EAAQ,UAClB,iBAAkB,SACpB,MACG,oBACH,MAAO,CACL,WAAY,EAAQ,UACpB,cAAe,aACf,aAAc,GACd,MAAO,gBACP,QAAS,EAAQ,UACb,2BAA2B,EAAQ,YAAY,EAAmB,KAAK,IAAqB,KAC5F,sBACJ,mBACA,SAAU,EAAQ,UAClB,iBAAkB,SACpB,MACG,mBACH,MAAO,CACL,WAAY,EAAQ,UACpB,cAAe,OACf,aAAc,GACd,MAAO,UACP,QACE,EAAQ,QAAQ,MAAM,EAAG,GAAG,IAC3B,EAAQ,UACL,yBAAyB,EAAQ,YACjC,qBACN,mBACA,SAAU,EAAQ,UAClB,iBAAkB,QACpB,MACG,cACH,MAAO,CACL,WAAY,EAAQ,UACpB,cAAe,OACf,aAAc,GACd,MAAO,UACP,QAAS,EAAQ,UACb,SAAS,EAAQ,YAAY,EAAmB,KAAK,IAAqB,KAC1E,OACJ,mBACA,SAAU,EAAQ,UAClB,iBACE,OAAO,EAAQ,gBAAkB,UACjC,EAAQ,gBAAkB,MAC1B,aAAe,EAAQ,cAClB,EAAQ,cAA0C,WACnD,GACE,QACA,YACF,WACR,MACG,qBACH,MAAO,CACL,WAAY,EAAQ,UACpB,cAAe,OACf,aAAc,GACd,MAAO,EAAQ,aAAe,OAAS,UACvC,QACE,EAAQ,OAAO,MAAM,EAAG,GAAG,IAC1B,EAAQ,UAAY,UAAU,EAAQ,YAAc,eACvD,mBACA,SAAU,EAAQ,UAClB,iBAAkB,OACpB,MACG,gBACH,MAAO,CACL,cAAe,OACf,aAAc,GACd,MAAO,UACP,QAAS,yBACX,MACG,OAAQ,CACX,IAAM,EAAQ,GAAe,CAAO,EACpC,MAAO,CACL,cAAe,IAAU,gBAAkB,WAAa,OACxD,aAAc,IAAU,gBACxB,QACA,QAAS,EAAc,EAAQ,sBAAsB,CACvD,CACF,KACK,cACH,MAAO,CACL,cAAe,OACf,aAAc,GACd,MAAO,QACP,QACE,EAAc,EAAQ,sBAAsB,GAC5C,EAAQ,eAAe,MAAM,EAAG,GAAG,GACnC,EAAQ,OAAO,MAAM,EAAG,GAAG,CAC/B,MACG,aACH,MAAO,CACL,cAAe,GAAU,eAAiB,OAC1C,aAAc,GAAU,cAAgB,GACxC,MAAO,GAAU,OAAS,MAC5B,MACG,gBACH,MAAO,CACL,cAAe,OACf,aAAc,GACd,MAAO,UACP,QAAS,EAAQ,WACb,YAAY,EAAQ,aACpB,kBACN,MACG,eACH,MAAO,CACL,cAAe,OACf,aAAc,GACd,MAAO,OACP,QAAS,EAAc,EAAQ,sBAAsB,CACvD,MACG,aACH,MAAO,CACL,cAAe,OACf,aAAc,GACd,MAAO,OACP,QAAS,EAAQ,OACb,yBAAyB,EAAQ,SACjC,sBACN,MACG,eACH,MAAO,CACL,cAAe,GAAkB,EAAQ,QAAS,EAAQ,KAAK,EAC/D,aAAc,GACd,MAAO,GAAU,OAAS,OAC1B,QAAS,EAAc,EAAQ,SAAW,EAAQ,KAAK,CACzD,UAEA,MAAO,CACL,cAAe,GAAU,eAAiB,OAC1C,aAAc,GAAU,cAAgB,GACxC,MAAO,GAAU,OAAS,MAC5B,GAIN,SAAS,CAAa,CACpB,EACA,EACoB,CACpB,GAAI,IAAU,oBAAsB,EAAQ,OAC1C,OAAO,EAAc,EAAQ,MAAM,EAErC,GAAI,IAAU,gBAAkB,EAAQ,OACtC,MAAO,kBAAkB,EAAQ,SAEnC,GAAI,IAAU,qBAAuB,EAAQ,UAAW,CACtD,IAAM,EAAS,EAAmB,EAAQ,UAAW,EAAQ,UAAU,EACvE,MAAO,2BAA2B,EAAQ,YAAY,EAAS,KAAK,IAAW,KAEjF,GAAI,IAAU,mBACZ,OAAO,EAAQ,QAAQ,MAAM,EAAG,GAAG,EAErC,IACG,IAAU,cAAgB,IAAU,gBACrC,EAAQ,UACR,CACA,IAAM,EAAO,EAAQ,YAAa,SAAmC,MACnE,EACA,EACF,EACA,OAAO,EACH,SAAS,EAAQ,cAAc,IAC/B,SAAS,EAAQ,YAEvB,GAAI,IAAU,qBACZ,OAAO,EAAQ,OAAO,MAAM,EAAG,GAAG,EAEpC,GAAI,IAAU,QAAU,IAAU,eAAiB,IAAU,eAC3D,OAAO,EAAc,EAAQ,sBAAsB,EAErD,GAAI,IAAU,cAAgB,EAAQ,OACpC,MAAO,yBAAyB,EAAQ,SAE1C,OAKF,SAAS,CAAiB,CAAC,EAAyB,CAClD,IAAM,EAAW,QAAQ,IAAI,UAC7B,GAAI,CAAC,EAAU,OAEf,IAAM,EAAS,EACb,OACA,CAAC,aAAc,KAAM,KAAM,EAAU,kBAAmB,CAAS,EACjE,CACE,MAAO,SACP,QAAS,IACX,CACF,EAEA,GAAI,EAAO,OAAS,EAAO,SAAW,EAAG,EAK3C,SAAS,EAAmB,EAAS,CACnC,IAAM,EAAW,QAAQ,IAAI,UAC7B,GAAI,CAAC,EAAU,OAEf,EAAU,OAAQ,CAAC,aAAc,MAAO,KAAM,EAAU,iBAAiB,EAAG,CAC1E,MAAO,SACP,QAAS,IACX,CAAC,EA8BH,SAAS,CAAqB,CAC5B,EACA,EACsB,CACtB,IAAM,EAAO,EAAK,EAAU,GAAG,QAAgB,EAC/C,GAAI,CAAC,EAAW,CAAI,EAAG,OAAO,KAC9B,GAAI,CACF,OAAO,KAAK,MAAM,EAAa,EAAM,MAAM,CAAC,EAC5C,KAAM,CACN,OAAO,MAIX,SAAS,CAAa,CACpB,EACA,EACA,EACM,CACN,EAAmB,CAAQ,EAC3B,IAAM,EAAa,EAAK,EAAU,GAAG,QAAgB,EAC/C,EAAU,EAAK,EAAU,GAAG,YAAoB,EACtD,EAAc,EAAS,KAAK,UAAU,CAAI,EAAG,MAAM,EACnD,GAAI,CACF,GAAW,EAAS,CAAU,EAC9B,KAAM,CACN,EAAc,EAAY,KAAK,UAAU,CAAI,EAAG,MAAM,GAM1D,eAAe,EAAI,EAAkB,CACnC,IAAI,EAAM,GACV,cAAiB,KAAS,IAAI,MAAM,OAAO,EACzC,GAAO,IAAI,YAAY,EAAE,OAAO,CAAK,EAGvC,IAAI,EACJ,GAAI,CACF,EAAU,KAAK,MAAM,CAAG,EACxB,KAAM,CACN,QAAQ,KAAK,CAAC,EAGhB,IAAM,EAAQ,EAAQ,iBAAmB,GACnC,EAAY,EAAQ,WAC1B,GAAI,CAAC,EAAW,QAAQ,KAAK,CAAC,EAE9B,IAAM,EAAW,GAAgB,EAC3B,EAAM,KAAK,IAAI,EAErB,GAAI,IAAU,aAAc,CAC1B,MAAM,EAAgB,CACpB,cAAe,OACf,gBAAiB,cACjB,UAAW,EACX,UAAW,EACX,aAAc,GACd,QAAS,EACT,YACA,MAAO,OACP,QAAS,EAAc,EAAO,CAAO,KACjC,EAAQ,OAAS,CAAE,UAAW,EAAQ,MAAO,EAAI,CAAC,KAClD,EAAQ,gBACR,CAAE,eAAgB,EAAQ,eAAgB,EAC1C,CAAC,EACL,YAAa,EAAQ,IACrB,aAAc,CAChB,CAAC,EACD,GAAI,CACF,GAAO,EAAK,EAAU,GAAG,QAAgB,EAAG,CAAE,MAAO,EAAK,CAAC,EAC3D,KAAM,EAGR,GAAoB,EACpB,QAAQ,KAAK,CAAC,EAGhB,GAAI,IAAU,iBAAmB,IAAU,eAAgB,CACzD,IAAM,EACJ,EAAQ,uBAAyB,EAAQ,yBACrC,EACJ,EAAQ,UACR,EAAQ,sBACP,EAAoB,GAAS,EAAmB,QAAQ,EAAI,QAE/D,GAAI,EAAO,CACT,IAAM,EAAW,EAAsB,EAAU,CAAK,EAChD,EAAU,EAAgB,EAAO,EAAS,CAAQ,EAClD,EAAa,GAAU,SAAW,EAClC,EAAW,KAAK,MAAM,KAAK,IAAI,EAAI,GAAG,EACtC,EAAa,KAAK,IAAI,EAAa,EAAG,CAAQ,EAC9C,EAAyB,CAC7B,cAAe,EAAQ,cACvB,gBAAiB,cACjB,UAAW,EACX,UAAW,EACX,aAAc,EAAQ,aACtB,QAAS,EACT,MAAO,EAAQ,SACV,EAAQ,SAAW,EAAc,EAAO,CAAO,EAChD,CAAE,QAAS,EAAQ,SAAW,EAAc,EAAO,CAAO,CAAE,EAC5D,CAAC,KACD,EAAQ,gBACR,CAAE,eAAgB,EAAQ,eAAgB,EAC1C,CAAC,KACD,EAAQ,SAAW,CAAE,QAAS,EAAQ,QAAS,EAAI,CAAC,KACpD,EAAQ,WAAa,CAAE,UAAW,EAAQ,UAAW,EAAI,CAAC,KAC1D,EAAoB,CAAE,eAAgB,CAAkB,EAAI,CAAC,EACjE,YAAa,EAAQ,IACrB,aAAc,EACd,gBAAiB,UAAU,GAC7B,EACA,EAAc,EAAU,EAAO,CAAO,EACtC,MAAM,EAAgB,CAAE,UAAW,KAAU,CAAQ,CAAC,EAGxD,GAAI,IAAU,eAAgB,CAK5B,IAAM,EAAiB,EAAsB,EAAU,CAAS,EAC1D,EAAgB,GAAgB,SAAW,EAC3C,EAAc,KAAK,MAAM,KAAK,IAAI,EAAI,GAAG,EACzC,EAAgB,KAAK,IAAI,EAAgB,EAAG,CAAW,EACvD,EAAgB,EAAc,eAAgB,CAAO,EACrD,EAA4B,CAChC,cAAe,GAAgB,eAAiB,OAChD,gBAAiB,cACjB,UAAW,EACX,UAAW,eACX,aAAc,GAAgB,cAAgB,GAC9C,QAAS,EACT,MAAO,GAAgB,OAAS,aAC5B,EAAgB,CAAE,QAAS,CAAc,EAAI,CAAC,EAClD,YAAa,EAAQ,SAAW,EAAQ,IACxC,aAAc,CAChB,EACA,EAAc,EAAU,EAAW,CAAU,EAC7C,MAAM,EAAgB,CAAE,eAAc,CAAW,CAAC,EAElD,EAAkB,CAAS,EAC3B,QAAQ,KAAK,CAAC,GAKlB,IAAM,EAAW,EAAsB,EAAU,CAAS,EACpD,EAAU,EAAgB,EAAO,EAAS,CAAQ,EAClD,EACJ,EAAQ,SAAW,EAAc,EAAO,CAAO,GAAK,GAAU,QAI1D,EAAU,GAAU,SAAW,EAC/B,EAAQ,KAAK,MAAM,KAAK,IAAI,EAAI,GAAG,EACnC,EAAU,KAAK,IAAI,EAAU,EAAG,CAAK,EAErC,EAA0B,CAC9B,cAAe,EAAQ,cACvB,gBAAiB,cACjB,UAAW,EACX,UAAW,EACX,aAAc,EAAQ,aACtB,UACA,MAAO,EAAQ,SACX,EAAU,CAAE,SAAQ,EAAI,CAAC,KACzB,EAAQ,WAAa,CAAE,WAAY,EAAQ,UAAW,EAAI,CAAC,KAC3D,EAAQ,SAAW,CAAE,SAAU,EAAQ,QAAS,EAAI,CAAC,KACrD,EAAQ,iBACR,CAAE,iBAAkB,EAAQ,gBAAiB,EAC7C,CAAC,KACD,EAAQ,iBACR,CAAE,iBAAkB,EAAQ,gBAAiB,EAC7C,CAAC,KACD,EAAQ,gBACR,CAAE,eAAgB,EAAQ,eAAgB,EAC1C,CAAC,KACD,EAAQ,OAAS,CAAE,YAAa,EAAQ,MAAO,EAAI,CAAC,KACpD,EAAQ,OAAS,CAAE,UAAW,EAAQ,MAAO,EAAI,CAAC,KAClD,EAAQ,MAAQ,CAAE,UAAW,EAAQ,KAAM,EAAI,CAAC,KAChD,EAAQ,SAAW,CAAE,QAAS,EAAQ,QAAS,EAAI,CAAC,KACpD,EAAQ,WAAa,CAAE,UAAW,EAAQ,UAAW,EAAI,CAAC,KAC1D,EAAQ,gBACR,CAAE,eAAgB,EAAQ,eAAgB,EAC1C,CAAC,EACL,YAAa,EAAQ,SAAW,EAAQ,IACxC,aAAc,CAChB,EAMA,GAJA,EAAc,EAAU,EAAW,CAAQ,EAC3C,MAAM,EAAgB,CAAE,eAAc,CAAS,CAAC,EAI9C,IAAU,gBACV,IAAU,oBACV,IAAU,qBACV,IAAU,aAEV,EAAkB,CAAS,EAG7B,QAAQ,KAAK,CAAC,EAGhB,GAAK,EAAE,MAAM,IAAM,QAAQ,KAAK,CAAC,CAAC",
|
|
9
|
+
"debugId": "301E94BAA2B7FF9164756E2164756E21",
|
|
10
10
|
"names": []
|
|
11
11
|
}
|
package/dist/hooks/codex.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
// @bun
|
|
3
|
-
var p=Object.create;var{getPrototypeOf:i,defineProperty:b,getOwnPropertyNames:w}=Object;var _=Object.prototype.hasOwnProperty;function j(K){return this[K]}var UG=(K,G,V)=>{var Y=w(G);for(let Z of Y)if(!_.call(K,Z)&&Z!=="default")b(K,Z,{get:j.bind(G,Z),enumerable:!0});if(V){for(let Z of Y)if(!_.call(V,Z)&&Z!=="default")b(V,Z,{get:j.bind(G,Z),enumerable:!0});return V}},t,d,AG=(K,G,V)=>{var Y=K!=null&&typeof K==="object";if(Y){var Z=G?t??=new WeakMap:d??=new WeakMap,Q=Z.get(K);if(Q)return Q}V=K!=null?p(i(K)):{};let $=G||!K||!K.__esModule?b(V,"default",{value:K,enumerable:!0}):V;for(let X of w(K))if(!_.call($,X))b($,X,{get:j.bind(K,X),enumerable:!0});if(Y)Z.set(K,$);return $};var _G=(K,G)=>()=>(G||K((G={exports:{}}).exports,G),G.exports);var o=(K)=>K;function e(K,G){this[K]=o.bind(null,G)}var jG=(K,G)=>{for(var V in G)b(K,V,{get:G[V],enumerable:!0,configurable:!0,set:e.bind(G,V)})};var a=(K,G)=>()=>(K&&(G=K(K=0)),G);var CG=(K)=>Promise.all(K),NG=import.meta.require;import{
|
|
4
|
-
`)[0]??"";return G.length>0?G.slice(0,120):void 0}function BG(K){if(!K)return!1;let G=K.trim();return/^(would you|do you|can you|should i|please confirm|shall i|want me to|implement this plan|implemente this plan|apply this plan|continue with this plan|proceed with this plan)\b/i.test(G)||/^(quieres que|debo|puedo|confirmas|por favor confirma|contin[u\u00FA]o|procedo)\b/i.test(G)||/^\u00BF?(qu[e\u00E9]|cu[a\u00E1]l|cu[a\u00E1]les|c[o\u00F3]mo|d[o\u00F3]nde|cu[a\u00E1]ndo|por qu[e\u00E9])(?:\s|\?|$)/i.test(G)}function m(K){if(K.stop_hook_active===!0)return"working";if(BG(K.last_assistant_message))return"waiting-input";return"idle"}function W(K,G,V){let Y=y(G.tool_name,G.tool_input);switch(K){case"SessionStart":return{
|
|
5
|
-
`);process.exit(0)}let M=v(Z,L),B=M?{
|
|
6
|
-
`);process.exit(0)}if(V==="Stop"&&G.stop_hook_active===!1){let L=W(V,G,X),M=L.summary??z(V,G)??$?.summary,B={
|
|
7
|
-
`),process.exit(0)}let J=W(V,G,X),
|
|
3
|
+
var p=Object.create;var{getPrototypeOf:i,defineProperty:b,getOwnPropertyNames:w}=Object;var _=Object.prototype.hasOwnProperty;function j(K){return this[K]}var UG=(K,G,V)=>{var Y=w(G);for(let Z of Y)if(!_.call(K,Z)&&Z!=="default")b(K,Z,{get:j.bind(G,Z),enumerable:!0});if(V){for(let Z of Y)if(!_.call(V,Z)&&Z!=="default")b(V,Z,{get:j.bind(G,Z),enumerable:!0});return V}},t,d,AG=(K,G,V)=>{var Y=K!=null&&typeof K==="object";if(Y){var Z=G?t??=new WeakMap:d??=new WeakMap,Q=Z.get(K);if(Q)return Q}V=K!=null?p(i(K)):{};let $=G||!K||!K.__esModule?b(V,"default",{value:K,enumerable:!0}):V;for(let X of w(K))if(!_.call($,X))b($,X,{get:j.bind(K,X),enumerable:!0});if(Y)Z.set(K,$);return $};var _G=(K,G)=>()=>(G||K((G={exports:{}}).exports,G),G.exports);var o=(K)=>K;function e(K,G){this[K]=o.bind(null,G)}var jG=(K,G)=>{for(var V in G)b(K,V,{get:G[V],enumerable:!0,configurable:!0,set:e.bind(G,V)})};var a=(K,G)=>()=>(K&&(G=K(K=0)),G);var CG=(K)=>Promise.all(K),NG=import.meta.require;import{chmodSync as GG,lstatSync as KG,mkdirSync as D,readFileSync as VG,rmSync as YG}from"fs";import{tmpdir as ZG,userInfo as $G}from"os";import{join as x}from"path";function HG(K){let G=Bun.which?.(K);if(G)return G;if(C)return k("where",[K]);return k("which",[K])}function k(K,G){try{let V=Bun.spawnSync([K,...G],{stderr:"ignore",stdout:"pipe"}),Y=new TextDecoder().decode(V.stdout).trim();return Y?Y.split(/\r?\n/)[0]??Y:null}catch{return null}}function LG(){try{return $G().uid}catch{return process.env.USER??process.env.USERNAME??"default"}}function S(K){if(C)return x(ZG(),`scrylog-${K}`);return x("/tmp",`scrylog-${LG()}-${K}`)}function I(K){let G=KG(K);return G.isDirectory()&&G.uid===process.getuid?.()}function c(K){if(D(K,{mode:448,recursive:!0}),C)return;if(!I(K)){if(YG(K,{force:!0,recursive:!0}),D(K,{mode:448,recursive:!0}),!I(K))throw Error(`Refusing to use untrusted hooks directory: ${K}`)}GG(K,448)}var C,qG,JG,TG;var h=a(()=>{C=process.platform==="win32",qG=process.platform==="darwin",JG=process.platform==="linux",TG=(()=>{if(!JG)return!1;try{let K=VG("/proc/version","utf8");return/microsoft|wsl/i.test(K)}catch{return!1}})()});h();import{existsSync as q,readFileSync as T,renameSync as MG,rmSync as QG,writeFileSync as u}from"fs";import{join as U}from"path";import{spawnSync as XG}from"child_process";function RG(){let K=process.env.SCRYLOG_CONFIG;if(K&&q(K))try{let V=JSON.parse(T(K,"utf8")).codex;if(typeof V?.hooksDir==="string")return V.hooksDir}catch{}return S("codex-hooks")}function bG(){let K=process.env.SCRYLOG_CONFIG;if(K&&q(K))try{let V=JSON.parse(T(K,"utf8")).api;if(typeof V?.port==="number")return V.port}catch{}return 7788}async function N(K){try{let G=bG();return(await fetch(`http://127.0.0.1:${G}/api/webhook/codex`,{body:JSON.stringify(K),headers:{"Content-Type":"application/json"},method:"POST",signal:AbortSignal.timeout(1500)})).ok}catch{return!1}}function y(K,G){if(!K)return;let V=typeof G?.command==="string"?G.command.slice(0,120):void 0;if(V)return V;let Y=typeof G?.file_path==="string"?G.file_path:void 0;if(Y)return Y;let Z=typeof G?.description==="string"?G.description:void 0;if(Z)return Z.slice(0,120);return}function R(K){if(!K)return;let G=K.trim().split(`
|
|
4
|
+
`)[0]??"";return G.length>0?G.slice(0,120):void 0}function BG(K){if(!K)return!1;let G=K.trim();return/^(would you|do you|can you|should i|please confirm|shall i|want me to|implement this plan|implemente this plan|apply this plan|continue with this plan|proceed with this plan)\b/i.test(G)||/^(quieres que|debo|puedo|confirmas|por favor confirma|contin[u\u00FA]o|procedo)\b/i.test(G)||/^\u00BF?(qu[e\u00E9]|cu[a\u00E1]l|cu[a\u00E1]les|c[o\u00F3]mo|d[o\u00F3]nde|cu[a\u00E1]ndo|por qu[e\u00E9])(?:\s|\?|$)/i.test(G)}function m(K){if(K.stop_hook_active===!0)return"working";if(BG(K.last_assistant_message))return"waiting-input";return"idle"}function W(K,G,V){let Y=y(G.tool_name,G.tool_input);switch(K){case"SessionStart":return{attentionKind:"none",hasAttention:!1,state:"idle",summary:G.source?`Codex session ${G.source}`:"Codex session started"};case"UserPromptSubmit":return{attentionKind:"none",hasAttention:!1,state:"working",summary:R(G.prompt)??"Codex is working"};case"PreToolUse":return{activeTool:G.tool_name,attentionKind:"none",hasAttention:!1,state:"working",summary:G.tool_name?`Using ${G.tool_name}${Y?`: ${Y}`:""}`:void 0,toolInputSummary:Y,toolName:G.tool_name,toolResultStatus:"started"};case"PermissionRequest":return{activeTool:G.tool_name,attentionKind:"permission",hasAttention:!0,state:"waiting-input",summary:G.tool_name?`Permission required for ${G.tool_name}${Y?`: ${Y}`:""}`:"Permission required",toolInputSummary:Y,toolName:G.tool_name,toolResultStatus:"blocked"};case"PostToolUse":{if(G.tool_name==="request_user_input"){let Z=typeof G.tool_input?.question==="string"?G.tool_input.question.slice(0,120):void 0;return{activeTool:G.tool_name,attentionKind:"question",hasAttention:!0,state:"waiting-input",summary:Z?`Waiting for input: ${Z}`:"Waiting for user input",toolInputSummary:Z,toolName:G.tool_name,toolResultStatus:"completed"}}return{activeTool:G.tool_name,attentionKind:"none",hasAttention:!1,state:"working",summary:G.tool_name?`Using ${G.tool_name}${Y?`: ${Y}`:""}`:void 0,toolInputSummary:Y,toolName:G.tool_name,toolResultStatus:typeof G.tool_response==="object"&&G.tool_response!==null&&"is_error"in G.tool_response?G.tool_response.is_error===!0?"error":"completed":"completed"}}case"SubagentStart":return{attentionKind:"none",hasAttention:!1,state:"working",summary:G.agent_type?`Codex sub-agent ${G.agent_type}`:"Codex sub-agent"};case"SubagentStop":{let Z=m(G);return{attentionKind:Z==="waiting-input"?"question":"none",hasAttention:Z==="waiting-input",state:Z,summary:R(G.last_assistant_message)??(G.agent_type?`Codex sub-agent ${G.agent_type}`:"Codex sub-agent")}}case"Stop":{let Z=m(G);return{attentionKind:Z==="waiting-input"?"question":"none",hasAttention:Z==="waiting-input",state:Z,summary:R(G.last_assistant_message)}}default:return{attentionKind:V?.attentionKind??"none",hasAttention:V?.hasAttention??!1,state:V?.state??"idle"}}}function z(K,G){if(K==="UserPromptSubmit"&&G.prompt)return R(G.prompt);if(K==="SessionStart"&&G.source)return`Codex session ${G.source}`;if(K==="PermissionRequest"&&G.tool_name){let V=y(G.tool_name,G.tool_input);return`Permission required for ${G.tool_name}${V?`: ${V}`:""}`}if((K==="PreToolUse"||K==="PostToolUse")&&G.tool_name){if(K==="PostToolUse"&&G.tool_name==="request_user_input"){let Y=typeof G.tool_input?.question==="string"?G.tool_input.question.slice(0,120):void 0;return Y?`Waiting for input: ${Y}`:"Waiting for user input"}let V=G.tool_input?.command?.slice(0,60);return V?`Using ${G.tool_name}: ${V}`:`Using ${G.tool_name}`}if(K==="Stop")return R(G.last_assistant_message);if(K==="SubagentStart"||K==="SubagentStop")return R(G.last_assistant_message)??(G.agent_type?`Codex sub-agent ${G.agent_type}`:"Codex sub-agent");return}function F(K){let G=process.env.TMUX_PANE;if(!G)return;let V=XG("tmux",["set-option","-p","-t",G,"@codex-session",K],{stdio:"ignore",timeout:2000});if(V.error||V.status!==0);}function v(K,G){let V=U(K,`${G}.json`);if(!q(V))return null;try{return JSON.parse(T(V,"utf8"))}catch{return null}}function O(K,G,V){c(K);let Y=U(K,`${G}.json`),Z=U(K,`${G}.json.tmp`);u(Z,JSON.stringify(V),"utf8");try{MG(Z,Y)}catch{u(Y,JSON.stringify(V),"utf8")}}async function EG(){let K="";for await(let L of Bun.stdin.stream())K+=new TextDecoder().decode(L);let G;try{G=JSON.parse(K)}catch{process.exit(0)}let V=G.hook_event_name??"",Y=G.session_id;if(!Y)process.exit(0);let Z=RG(),Q=Date.now(),$=v(Z,Y),X=$?{attentionKind:$.attentionKind,hasAttention:$.hasAttention,state:$.state}:null,l=$?.hookSeq??0,r=Math.floor(Date.now()/100),A=Math.max(l+1,r);if(V==="SubagentStart"||V==="SubagentStop"){let L=G.agent_id;if(!L){if(V==="SubagentStop")process.stdout.write(`{}
|
|
5
|
+
`);process.exit(0)}let M=v(Z,L),B=M?{attentionKind:M.attentionKind,hasAttention:M.hasAttention,state:M.state}:null,s=M?.hookSeq??0,n=Math.max(s+1,A),E=W(V,G,B),P=E.summary??z(V,G)??M?.summary,g={attentionKind:E.attentionKind,detectionSource:"codex-hook",eventTime:Q,eventType:V,hasAttention:E.hasAttention,hookSeq:n,lastActivity:Q,state:E.state,...P?{summary:P}:{},...G.permission_mode?{permissionMode:G.permission_mode}:{},...G.model?{model:G.model}:{},transcriptPath:G.agent_transcript_path??M?.transcriptPath??"",...G.turn_id?{turnId:G.turn_id}:{},...G.agent_id?{agentId:G.agent_id}:{},...G.agent_type?{agentType:G.agent_type}:{},currentPath:G.cwd,parentSessionId:`codex:${Y}`};if(O(Z,L,g),await N({sessionId:L,...g}),F(Y),V==="SubagentStop")process.stdout.write(`{}
|
|
6
|
+
`);process.exit(0)}if(V==="Stop"&&G.stop_hook_active===!1){let L=W(V,G,X),M=L.summary??z(V,G)??$?.summary,B={attentionKind:L.attentionKind,detectionSource:"codex-hook",eventTime:Q,eventType:V,hasAttention:L.hasAttention,hookSeq:A,state:L.state,...M?{summary:M}:{},...G.transcript_path?{transcriptPath:G.transcript_path}:{},currentPath:G.cwd,lastActivity:Q};if(await N({sessionId:Y,...B}),L.state==="waiting-input")O(Z,Y,B),F(Y);else try{QG(U(Z,`${Y}.json`),{force:!0})}catch{}process.stdout.write(`{}
|
|
7
|
+
`),process.exit(0)}let J=W(V,G,X),H=J.summary??z(V,G)??$?.summary,f={attentionKind:J.attentionKind,detectionSource:"codex-hook",eventTime:Q,eventType:V,hasAttention:J.hasAttention,hookSeq:A,state:J.state,...H?{summary:H}:{},...J.activeTool?{activeTool:J.activeTool}:{},...J.toolName?{toolName:J.toolName}:{},...J.toolInputSummary?{toolInputSummary:J.toolInputSummary}:{},...J.toolResultStatus?{toolResultStatus:J.toolResultStatus}:{},...G.permission_mode?{permissionMode:G.permission_mode}:{},...G.source?{startSource:G.source}:{},...G.model?{model:G.model}:{},...G.transcript_path?{transcriptPath:G.transcript_path}:{},...G.turn_id?{turnId:G.turn_id}:{},currentPath:G.cwd,lastActivity:Q};if(O(Z,Y,f),await N({sessionId:Y,...f}),V==="SessionStart"||V==="UserPromptSubmit"||V==="PreToolUse"||V==="PermissionRequest")F(Y);if(V==="Stop")process.stdout.write(`{}
|
|
8
8
|
`);process.exit(0)}EG().catch(()=>process.exit(0));
|
|
9
9
|
|
|
10
|
-
//# debugId=
|
|
10
|
+
//# debugId=308AC51C15DF7A3764756E2164756E21
|
package/dist/hooks/codex.js.map
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/shared/platform.ts", "../src/hooks/codex.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"import {
|
|
6
|
-
"#!/usr/bin/env bun\n/**\n * scrylog-codex-hook — Codex CLI lifecycle hook handler.\n *\n * Codex CLI calls this binary at each lifecycle event (SessionStart, PreToolUse,\n * PermissionRequest, PostToolUse, UserPromptSubmit, SubagentStart,\n * SubagentStop, Stop) with a JSON payload on stdin.\n * This script writes a state file for the CodexCollector to pick up and,\n * when running inside tmux, sets the @codex-session pane option so the\n * TmuxCollector can link the pane to the Codex session.\n *\n * Configure in ~/.codex/hooks.json via:\n * scrylog codex hooks install\n */\n\nimport { writeFileSync, renameSync, readFileSync, existsSync, rmSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { spawnSync } from \"node:child_process\";\nimport { getHooksDir, ensureHooksDirSafe } from \"../shared/platform\";\nimport type { SessionState } from \"../shared/types\";\n\ntype AttentionKind = \"permission\" | \"question\" | \"turn\" | \"none\";\ntype ToolResultStatus = \"started\" | \"completed\" | \"error\" | \"blocked\" | \"denied\" | \"unknown\";\n\n// ── Hook payload ──────────────────────────────────────────────────────────────\n\ninterface CodexHookPayload {\n session_id: string;\n transcript_path?: string;\n cwd?: string;\n permission_mode?: string;\n hook_event_name?: string;\n model?: string;\n prompt?: string;\n stop_hook_active?: boolean;\n last_assistant_message?: string;\n source?: string;\n turn_id?: string;\n // Subagents\n agent_id?: string;\n agent_type?: string;\n agent_transcript_path?: string;\n // PreToolUse / PermissionRequest / PostToolUse\n tool_name?: string;\n tool_input?: Record<string, unknown>;\n tool_response?: unknown;\n tool_use_id?: string;\n tool_input_description?: string;\n}\n\ninterface DerivedHookState {\n state: SessionState;\n hasAttention: boolean;\n attentionKind: AttentionKind;\n activeTool?: string;\n toolName?: string;\n toolInputSummary?: string;\n toolResultStatus?: ToolResultStatus;\n summary?: string;\n}\n\n// ── Config resolution ─────────────────────────────────────────────────────────\n\nfunction resolveHooksDir(): string {\n const envConfig = process.env[\"SCRYLOG_CONFIG\"];\n if (envConfig && existsSync(envConfig)) {\n try {\n const raw = JSON.parse(readFileSync(envConfig, \"utf8\")) as Record<string, unknown>;\n const codex = raw[\"codex\"] as Record<string, unknown> | undefined;\n if (typeof codex?.[\"hooksDir\"] === \"string\") return codex[\"hooksDir\"];\n } catch {\n // fall through to default\n }\n }\n return getHooksDir(\"codex-hooks\");\n}\n\nfunction resolveDaemonPort(): number {\n const envConfig = process.env[\"SCRYLOG_CONFIG\"];\n if (envConfig && existsSync(envConfig)) {\n try {\n const raw = JSON.parse(readFileSync(envConfig, \"utf8\")) as Record<string, unknown>;\n const api = raw[\"api\"] as Record<string, unknown> | undefined;\n if (typeof api?.[\"port\"] === \"number\") return api[\"port\"] as number;\n } catch {\n // fall through to default\n }\n }\n return 7788;\n}\n\nasync function tryHttpDelivery(payload: HookStateFile & { sessionId: string }): Promise<boolean> {\n try {\n const port = resolveDaemonPort();\n const res = await fetch(`http://127.0.0.1:${port}/api/webhook/codex`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(payload),\n signal: AbortSignal.timeout(1500),\n });\n return res.ok;\n } catch {\n return false;\n }\n}\n\n// ── Helpers ───────────────────────────────────────────────────────────────────\n\nfunction summarizeToolInput(toolName?: string, input?: Record<string, unknown>): string | undefined {\n if (!toolName) return undefined;\n const command = typeof input?.[\"command\"] === \"string\" ? input[\"command\"].slice(0, 120) : undefined;\n if (command) return command;\n const filePath = typeof input?.[\"file_path\"] === \"string\" ? input[\"file_path\"] : undefined;\n if (filePath) return filePath;\n const description = typeof input?.[\"description\"] === \"string\" ? input[\"description\"] : undefined;\n if (description) return description.slice(0, 120);\n return undefined;\n}\n\nfunction summarizeText(text?: string): string | undefined {\n if (!text) return undefined;\n const first = text.trim().split(\"\\n\")[0] ?? \"\";\n return first.length > 0 ? first.slice(0, 120) : undefined;\n}\n\nfunction isQuestion(text?: string): boolean {\n if (!text) return false;\n const trimmed = text.trim();\n return /^(would you|do you|can you|should i|please confirm|shall i|want me to|implement this plan|implemente this plan|apply this plan|continue with this plan|proceed with this plan)\\b/i.test(trimmed)\n || /^(quieres que|debo|puedo|confirmas|por favor confirma|contin[uú]o|procedo)\\b/i.test(trimmed)\n || /^¿?(qu[eé]|cu[aá]l|cu[aá]les|c[oó]mo|d[oó]nde|cu[aá]ndo|por qu[eé])(?:\\s|\\?|$)/i.test(trimmed);\n}\n\nfunction deriveStopState(payload: CodexHookPayload): SessionState {\n if (payload.stop_hook_active === true) return \"working\";\n if (isQuestion(payload.last_assistant_message)) return \"waiting-input\";\n return \"idle\";\n}\n\n// ── State derivation ──────────────────────────────────────────────────────────\n\nfunction deriveHookState(event: string, payload: CodexHookPayload, previous?: PreviousState | null): DerivedHookState {\n const toolInputSummary = summarizeToolInput(payload.tool_name, payload.tool_input);\n switch (event) {\n case \"SessionStart\":\n return {\n state: \"idle\",\n hasAttention: false,\n attentionKind: \"none\",\n summary: payload.source ? `Codex session ${payload.source}` : \"Codex session started\",\n };\n case \"UserPromptSubmit\":\n return {\n state: \"working\",\n hasAttention: false,\n attentionKind: \"none\",\n summary: summarizeText(payload.prompt) ?? \"Codex is working\",\n };\n case \"PreToolUse\":\n return {\n state: \"working\",\n hasAttention: false,\n attentionKind: \"none\",\n activeTool: payload.tool_name,\n toolName: payload.tool_name,\n toolInputSummary,\n toolResultStatus: \"started\",\n summary: payload.tool_name ? `Using ${payload.tool_name}${toolInputSummary ? `: ${toolInputSummary}` : \"\"}` : undefined,\n };\n case \"PermissionRequest\":\n return {\n state: \"waiting-input\",\n hasAttention: true,\n attentionKind: \"permission\",\n activeTool: payload.tool_name,\n toolName: payload.tool_name,\n toolInputSummary,\n toolResultStatus: \"blocked\",\n summary: payload.tool_name ? `Permission required for ${payload.tool_name}${toolInputSummary ? `: ${toolInputSummary}` : \"\"}` : \"Permission required\",\n };\n case \"PostToolUse\": {\n // request_user_input means the agent asked a question and is waiting for\n // the user to respond — this is waiting-input, not working.\n if (payload.tool_name === \"request_user_input\") {\n const question = typeof payload.tool_input?.[\"question\"] === \"string\"\n ? (payload.tool_input[\"question\"] as string).slice(0, 120)\n : undefined;\n return {\n state: \"waiting-input\",\n hasAttention: true,\n attentionKind: \"question\",\n activeTool: payload.tool_name,\n toolName: payload.tool_name,\n toolInputSummary: question,\n toolResultStatus: \"completed\",\n summary: question ? `Waiting for input: ${question}` : \"Waiting for user input\",\n };\n }\n return {\n state: \"working\",\n hasAttention: false,\n attentionKind: \"none\",\n activeTool: payload.tool_name,\n toolName: payload.tool_name,\n toolInputSummary,\n toolResultStatus: typeof payload.tool_response === \"object\" && payload.tool_response !== null && \"is_error\" in (payload.tool_response as Record<string, unknown>)\n ? ((payload.tool_response as Record<string, unknown>)[\"is_error\"] === true ? \"error\" : \"completed\")\n : \"completed\",\n summary: payload.tool_name ? `Using ${payload.tool_name}${toolInputSummary ? `: ${toolInputSummary}` : \"\"}` : undefined,\n };\n }\n case \"SubagentStart\":\n return {\n state: \"working\",\n hasAttention: false,\n attentionKind: \"none\",\n summary: payload.agent_type ? `Codex sub-agent ${payload.agent_type}` : \"Codex sub-agent\",\n };\n case \"SubagentStop\": {\n const state = deriveStopState(payload);\n return {\n state,\n hasAttention: state === \"waiting-input\",\n attentionKind: state === \"waiting-input\" ? \"question\" : \"none\",\n summary: summarizeText(payload.last_assistant_message) ?? (payload.agent_type ? `Codex sub-agent ${payload.agent_type}` : \"Codex sub-agent\"),\n };\n }\n case \"Stop\": {\n const state = deriveStopState(payload);\n return {\n state,\n hasAttention: state === \"waiting-input\",\n attentionKind: state === \"waiting-input\" ? \"question\" : \"none\",\n summary: summarizeText(payload.last_assistant_message),\n };\n }\n default:\n return {\n state: previous?.state ?? \"idle\",\n hasAttention: previous?.hasAttention ?? false,\n attentionKind: previous?.attentionKind ?? \"none\",\n };\n }\n}\n\nfunction deriveSummary(event: string, payload: CodexHookPayload): string | undefined {\n if (event === \"UserPromptSubmit\" && payload.prompt) {\n return summarizeText(payload.prompt);\n }\n if (event === \"SessionStart\" && payload.source) {\n return `Codex session ${payload.source}`;\n }\n if (event === \"PermissionRequest\" && payload.tool_name) {\n const target = summarizeToolInput(payload.tool_name, payload.tool_input);\n return `Permission required for ${payload.tool_name}${target ? `: ${target}` : \"\"}`;\n }\n if ((event === \"PreToolUse\" || event === \"PostToolUse\") && payload.tool_name) {\n if (event === \"PostToolUse\" && payload.tool_name === \"request_user_input\") {\n const question = typeof payload.tool_input?.[\"question\"] === \"string\"\n ? (payload.tool_input[\"question\"] as string).slice(0, 120)\n : undefined;\n return question ? `Waiting for input: ${question}` : \"Waiting for user input\";\n }\n const cmd = (payload.tool_input?.[\"command\"] as string | undefined)?.slice(0, 60);\n return cmd ? `Using ${payload.tool_name}: ${cmd}` : `Using ${payload.tool_name}`;\n }\n if (event === \"Stop\") {\n return summarizeText(payload.last_assistant_message);\n }\n if (event === \"SubagentStart\" || event === \"SubagentStop\") {\n return summarizeText(payload.last_assistant_message) ?? (payload.agent_type ? `Codex sub-agent ${payload.agent_type}` : \"Codex sub-agent\");\n }\n return undefined;\n}\n\n// ── Tmux integration ──────────────────────────────────────────────────────────\n\nfunction setTmuxPaneOption(sessionId: string): void {\n const tmuxPane = process.env[\"TMUX_PANE\"];\n if (!tmuxPane) return;\n\n const result = spawnSync(\"tmux\", [\"set-option\", \"-p\", \"-t\", tmuxPane, \"@codex-session\", sessionId], {\n stdio: \"ignore\",\n timeout: 2000,\n });\n\n if (result.error || result.status !== 0) {\n // Non-fatal — tmux may not be available or the pane might have closed\n }\n}\n\nfunction clearTmuxPaneOption(): void {\n const tmuxPane = process.env[\"TMUX_PANE\"];\n if (!tmuxPane) return;\n\n spawnSync(\"tmux\", [\"set-option\", \"-up\", \"-t\", tmuxPane, \"@codex-session\"], {\n stdio: \"ignore\",\n timeout: 2000,\n });\n}\n\n// ── File writing ──────────────────────────────────────────────────────────────\n\ninterface PreviousState {\n state?: SessionState;\n hasAttention?: boolean;\n attentionKind?: AttentionKind;\n}\n\ninterface HookStateFile {\n eventType: string;\n eventTime: number;\n hookSeq: number;\n state: SessionState;\n hasAttention: boolean;\n attentionKind: AttentionKind;\n summary?: string;\n activeTool?: string;\n toolName?: string;\n toolInputSummary?: string;\n toolResultStatus?: ToolResultStatus;\n permissionMode?: string;\n startSource?: string;\n model?: string;\n endReason?: string;\n transcriptPath?: string;\n currentPath?: string;\n turnId?: string;\n agentId?: string;\n agentType?: string;\n lastActivity: number;\n parentSessionId?: string;\n detectionSource: \"codex-hook\";\n}\n\nfunction readExistingHookState(hooksDir: string, sessionId: string): HookStateFile | null {\n const path = join(hooksDir, `${sessionId}.json`);\n if (!existsSync(path)) return null;\n try {\n return JSON.parse(readFileSync(path, \"utf8\")) as HookStateFile;\n } catch {\n return null;\n }\n}\n\nfunction writeHookFile(hooksDir: string, sessionId: string, data: HookStateFile): void {\n ensureHooksDirSafe(hooksDir);\n const targetPath = join(hooksDir, `${sessionId}.json`);\n const tmpPath = join(hooksDir, `${sessionId}.json.tmp`);\n writeFileSync(tmpPath, JSON.stringify(data), \"utf8\");\n try {\n renameSync(tmpPath, targetPath);\n } catch {\n writeFileSync(targetPath, JSON.stringify(data), \"utf8\");\n }\n}\n\n// ── Main ──────────────────────────────────────────────────────────────────────\n\nasync function main(): Promise<void> {\n let raw = \"\";\n for await (const chunk of Bun.stdin.stream()) {\n raw += new TextDecoder().decode(chunk);\n }\n\n let payload: CodexHookPayload;\n try {\n payload = JSON.parse(raw) as CodexHookPayload;\n } catch {\n process.exit(0); // Malformed input — exit cleanly so Codex CLI is not disrupted\n }\n\n const event = payload.hook_event_name ?? \"\";\n const sessionId = payload.session_id;\n if (!sessionId) process.exit(0);\n\n const hooksDir = resolveHooksDir();\n const now = Date.now();\n const previous = readExistingHookState(hooksDir, sessionId);\n const previousState: PreviousState | null = previous\n ? { state: previous.state, hasAttention: previous.hasAttention, attentionKind: previous.attentionKind }\n : null;\n const baseSeq = previous?.hookSeq ?? 0;\n const nowMs = Math.floor(Date.now() / 100);\n const hookSeq = Math.max(baseSeq + 1, nowMs);\n\n if (event === \"SubagentStart\" || event === \"SubagentStop\") {\n const childSessionId = payload.agent_id;\n if (!childSessionId) {\n if (event === \"SubagentStop\") process.stdout.write(\"{}\\n\");\n process.exit(0);\n }\n\n const childPrevious = readExistingHookState(hooksDir, childSessionId);\n const childPreviousState: PreviousState | null = childPrevious\n ? { state: childPrevious.state, hasAttention: childPrevious.hasAttention, attentionKind: childPrevious.attentionKind }\n : null;\n const childBaseSeq = childPrevious?.hookSeq ?? 0;\n const childHookSeq = Math.max(childBaseSeq + 1, hookSeq);\n const derived = deriveHookState(event, payload, childPreviousState);\n const summary = derived.summary ?? deriveSummary(event, payload) ?? childPrevious?.summary;\n const hookData: HookStateFile = {\n eventType: event,\n eventTime: now,\n hookSeq: childHookSeq,\n state: derived.state,\n hasAttention: derived.hasAttention,\n attentionKind: derived.attentionKind,\n ...(summary ? { summary } : {}),\n ...(payload.permission_mode ? { permissionMode: payload.permission_mode } : {}),\n ...(payload.model ? { model: payload.model } : {}),\n transcriptPath: payload.agent_transcript_path ?? childPrevious?.transcriptPath ?? \"\",\n ...(payload.turn_id ? { turnId: payload.turn_id } : {}),\n ...(payload.agent_id ? { agentId: payload.agent_id } : {}),\n ...(payload.agent_type ? { agentType: payload.agent_type } : {}),\n currentPath: payload.cwd,\n parentSessionId: `codex:${sessionId}`,\n lastActivity: now,\n detectionSource: \"codex-hook\",\n };\n\n writeHookFile(hooksDir, childSessionId, hookData);\n await tryHttpDelivery({ sessionId: childSessionId, ...hookData });\n setTmuxPaneOption(sessionId);\n\n if (event === \"SubagentStop\") {\n process.stdout.write(\"{}\\n\");\n }\n process.exit(0);\n }\n\n // Stop with stop_hook_active=false signals the end of a turn, not the session itself.\n // We delete the state file but leave the @codex-session pane option so the pane\n // remains linked to the Codex session between turns.\n if (event === \"Stop\" && payload.stop_hook_active === false) {\n const derived = deriveHookState(event, payload, previousState);\n const summary = derived.summary ?? deriveSummary(event, payload) ?? previous?.summary;\n const hookData: HookStateFile = {\n eventType: event,\n eventTime: now,\n hookSeq,\n state: derived.state,\n hasAttention: derived.hasAttention,\n attentionKind: derived.attentionKind,\n ...(summary ? { summary } : {}),\n ...(payload.transcript_path ? { transcriptPath: payload.transcript_path } : {}),\n currentPath: payload.cwd,\n lastActivity: now,\n detectionSource: \"codex-hook\",\n };\n\n await tryHttpDelivery({\n sessionId,\n ...hookData,\n });\n\n if (derived.state === \"waiting-input\") {\n writeHookFile(hooksDir, sessionId, hookData);\n setTmuxPaneOption(sessionId);\n } else {\n try {\n rmSync(join(hooksDir, `${sessionId}.json`), { force: true });\n } catch {\n // Ignore cleanup issues\n }\n }\n // Codex expects JSON on stdout for Stop events\n process.stdout.write(\"{}\\n\");\n process.exit(0);\n }\n\n // For all other events: write state for the current session\n const derived = deriveHookState(event, payload, previousState);\n const summary = derived.summary ?? deriveSummary(event, payload) ?? previous?.summary;\n\n // Use a timestamp-based hookSeq to avoid collisions when multiple hooks\n // fire concurrently. The previous seq is used as a floor to ensure monotonicity.\n\n const hookData: HookStateFile = {\n eventType: event,\n eventTime: now,\n hookSeq,\n state: derived.state,\n hasAttention: derived.hasAttention,\n attentionKind: derived.attentionKind,\n ...(summary ? { summary } : {}),\n ...(derived.activeTool ? { activeTool: derived.activeTool } : {}),\n ...(derived.toolName ? { toolName: derived.toolName } : {}),\n ...(derived.toolInputSummary ? { toolInputSummary: derived.toolInputSummary } : {}),\n ...(derived.toolResultStatus ? { toolResultStatus: derived.toolResultStatus } : {}),\n ...(payload.permission_mode ? { permissionMode: payload.permission_mode } : {}),\n ...(payload.source ? { startSource: payload.source } : {}),\n ...(payload.model ? { model: payload.model } : {}),\n ...(payload.transcript_path ? { transcriptPath: payload.transcript_path } : {}),\n ...(payload.turn_id ? { turnId: payload.turn_id } : {}),\n currentPath: payload.cwd,\n lastActivity: now,\n detectionSource: \"codex-hook\",\n };\n\n writeHookFile(hooksDir, sessionId, hookData);\n await tryHttpDelivery({ sessionId, ...hookData });\n\n // Update tmux binding on session start, prompt, and tool use events\n if (event === \"SessionStart\" || event === \"UserPromptSubmit\" || event === \"PreToolUse\" || event === \"PermissionRequest\") {\n setTmuxPaneOption(sessionId);\n }\n\n // Codex expects JSON on stdout for Stop events\n if (event === \"Stop\") {\n process.stdout.write(\"{}\\n\");\n }\n\n process.exit(0);\n}\n\nmain().catch(() => process.exit(0));\n"
|
|
5
|
+
"import { chmodSync, lstatSync, mkdirSync, readFileSync, rmSync } from 'node:fs';\nimport { tmpdir, userInfo } from 'node:os';\nimport { join } from 'node:path';\n\n/** True on Windows where Unix sockets are not available */\nexport const IS_WINDOWS = process.platform === 'win32';\n\n/** True on macOS */\nexport const IS_MAC = process.platform === 'darwin';\n\n/** True on Linux (including WSL) */\nexport const IS_LINUX = process.platform === 'linux';\n\n/** True when running inside Windows Subsystem for Linux */\nexport const IS_WSL = ((): boolean => {\n if (!IS_LINUX) return false;\n try {\n const version = readFileSync('/proc/version', 'utf8');\n return /microsoft|wsl/i.test(version);\n } catch {\n return false;\n }\n})();\n\n/**\n * Resolve the path to an executable using the platform-appropriate command.\n * Prefers Bun.which when available, falling back to `which` on Unix and `where` on Windows.\n * @param name - The name of the executable to resolve.\n */\nexport function whichCommand(name: string): string | null {\n const viaBun = (\n Bun as typeof Bun & { which?: (name: string) => string | null }\n ).which?.(name);\n if (viaBun) return viaBun;\n\n if (IS_WINDOWS) {\n return spawnAndRead('where', [name]);\n }\n return spawnAndRead('which', [name]);\n}\n\nfunction spawnAndRead(command: string, args: string[]): string | null {\n try {\n const proc = Bun.spawnSync([command, ...args], {\n stderr: 'ignore',\n stdout: 'pipe',\n });\n const text = new TextDecoder().decode(proc.stdout).trim();\n return text ? (text.split(/\\r?\\n/)[0] ?? text) : null;\n } catch {\n return null;\n }\n}\n\n/**\n * Return the current user's id, used to scope shared /tmp paths so multiple\n * users on the same host don't conflict or collide on the same path.\n */\nexport function getUserId(): number | string {\n try {\n return userInfo().uid;\n } catch {\n // Fallback: use a hash of the username\n return process.env.USER ?? process.env.USERNAME ?? 'default';\n }\n}\n\n/**\n * Return a stable hooks directory path for the given hook name.\n * On Unix we intentionally avoid os.tmpdir() because macOS per-session temp paths\n * would break rendezvous between independently-launched daemon and hook binaries.\n * The path is scoped by uid so multiple users on the same host don't conflict.\n * On Windows we use os.tmpdir() because %TEMP% is stable per user session and\n * already per-user.\n *\n * This function is pure (no I/O). Callers that need to actually create the\n * directory should use `ensureHooksDirSafe`.\n * @param name - The name of the hook, used to create a unique directory for it.\n */\nexport function getHooksDir(name: string): string {\n if (IS_WINDOWS) {\n return join(tmpdir(), `scrylog-${name}`);\n }\n return join('/tmp', `scrylog-${getUserId()}-${name}`);\n}\n\nfunction isOwnedDirectory(dir: string): boolean {\n const stat = lstatSync(dir);\n return stat.isDirectory() && stat.uid === process.getuid?.();\n}\n\n/**\n * Ensure `dir` exists and, on Unix, is a real directory (not a symlink)\n * owned by the current user, tightening its mode to 0700. A pre-existing\n * path that isn't a directory we own — e.g. a symlink or a directory\n * planted by another local user on a shared host — is untrusted: it is\n * removed and recreated rather than written into. A directory we already\n * own is tightened in place (not recreated) so existing session state isn't\n * discarded. Ownership/mode checks are skipped on Windows, where\n * `getHooksDir` already resolves to a per-user temp directory.\n * @param dir - The path to the hooks directory to ensure is safe.\n * @returns\n */\nexport function ensureHooksDirSafe(dir: string): void {\n mkdirSync(dir, { mode: 0o700, recursive: true });\n\n if (IS_WINDOWS) return;\n\n if (!isOwnedDirectory(dir)) {\n rmSync(dir, { force: true, recursive: true });\n mkdirSync(dir, { mode: 0o700, recursive: true });\n\n if (!isOwnedDirectory(dir)) {\n throw new Error(`Refusing to use untrusted hooks directory: ${dir}`);\n }\n }\n\n chmodSync(dir, 0o700);\n}\n",
|
|
6
|
+
"#!/usr/bin/env bun\n/**\n * scrylog-codex-hook — Codex CLI lifecycle hook handler.\n *\n * Codex CLI calls this binary at each lifecycle event (SessionStart, PreToolUse,\n * PermissionRequest, PostToolUse, UserPromptSubmit, SubagentStart,\n * SubagentStop, Stop) with a JSON payload on stdin.\n * This script writes a state file for the CodexCollector to pick up and,\n * when running inside tmux, sets the \\@codex-session pane option so the\n * TmuxCollector can link the pane to the Codex session.\n *\n * Configure in ~/.codex/hooks.json via:\n * scrylog codex hooks install\n */\n\nimport {\n existsSync,\n readFileSync,\n renameSync,\n rmSync,\n writeFileSync,\n} from 'node:fs';\nimport { join } from 'node:path';\nimport { spawnSync } from 'node:child_process';\n\nimport { ensureHooksDirSafe, getHooksDir } from '../shared/platform';\nimport type { SessionState } from '../shared/types';\n\ntype AttentionKind = 'permission' | 'question' | 'turn' | 'none';\ntype ToolResultStatus =\n | 'started'\n | 'completed'\n | 'error'\n | 'blocked'\n | 'denied'\n | 'unknown';\n\n// ── Hook payload ──────────────────────────────────────────────────────────────\n\ninterface CodexHookPayload {\n session_id: string;\n transcript_path?: string;\n cwd?: string;\n permission_mode?: string;\n hook_event_name?: string;\n model?: string;\n prompt?: string;\n stop_hook_active?: boolean;\n last_assistant_message?: string;\n source?: string;\n turn_id?: string;\n // Subagents\n agent_id?: string;\n agent_type?: string;\n agent_transcript_path?: string;\n // PreToolUse / PermissionRequest / PostToolUse\n tool_name?: string;\n tool_input?: Record<string, unknown>;\n tool_response?: unknown;\n tool_use_id?: string;\n tool_input_description?: string;\n}\n\ninterface DerivedHookState {\n state: SessionState;\n hasAttention: boolean;\n attentionKind: AttentionKind;\n activeTool?: string;\n toolName?: string;\n toolInputSummary?: string;\n toolResultStatus?: ToolResultStatus;\n summary?: string;\n}\n\n// ── Config resolution ─────────────────────────────────────────────────────────\n\nfunction resolveHooksDir(): string {\n const envConfig = process.env['SCRYLOG_CONFIG'];\n if (envConfig && existsSync(envConfig)) {\n try {\n const raw = JSON.parse(readFileSync(envConfig, 'utf8')) as Record<\n string,\n unknown\n >;\n const codex = raw['codex'] as Record<string, unknown> | undefined;\n if (typeof codex?.['hooksDir'] === 'string') return codex['hooksDir'];\n } catch {\n // fall through to default\n }\n }\n return getHooksDir('codex-hooks');\n}\n\nfunction resolveDaemonPort(): number {\n const envConfig = process.env['SCRYLOG_CONFIG'];\n if (envConfig && existsSync(envConfig)) {\n try {\n const raw = JSON.parse(readFileSync(envConfig, 'utf8')) as Record<\n string,\n unknown\n >;\n const api = raw['api'] as Record<string, unknown> | undefined;\n if (typeof api?.['port'] === 'number') return api['port'] as number;\n } catch {\n // fall through to default\n }\n }\n return 7788;\n}\n\nasync function tryHttpDelivery(\n payload: HookStateFile & { sessionId: string }\n): Promise<boolean> {\n try {\n const port = resolveDaemonPort();\n const res = await fetch(`http://127.0.0.1:${port}/api/webhook/codex`, {\n body: JSON.stringify(payload),\n headers: { 'Content-Type': 'application/json' },\n method: 'POST',\n signal: AbortSignal.timeout(1500),\n });\n return res.ok;\n } catch {\n return false;\n }\n}\n\n// ── Helpers ───────────────────────────────────────────────────────────────────\n\nfunction summarizeToolInput(\n toolName?: string,\n input?: Record<string, unknown>\n): string | undefined {\n if (!toolName) return undefined;\n const command =\n typeof input?.['command'] === 'string'\n ? input['command'].slice(0, 120)\n : undefined;\n if (command) return command;\n const filePath =\n typeof input?.['file_path'] === 'string' ? input['file_path'] : undefined;\n if (filePath) return filePath;\n const description =\n typeof input?.['description'] === 'string'\n ? input['description']\n : undefined;\n if (description) return description.slice(0, 120);\n return undefined;\n}\n\nfunction summarizeText(text?: string): string | undefined {\n if (!text) return undefined;\n const first = text.trim().split('\\n')[0] ?? '';\n return first.length > 0 ? first.slice(0, 120) : undefined;\n}\n\nfunction isQuestion(text?: string): boolean {\n if (!text) return false;\n const trimmed = text.trim();\n return (\n // eslint-disable-next-line max-len\n /^(would you|do you|can you|should i|please confirm|shall i|want me to|implement this plan|implemente this plan|apply this plan|continue with this plan|proceed with this plan)\\b/i.test(\n trimmed\n ) ||\n /^(quieres que|debo|puedo|confirmas|por favor confirma|contin[uú]o|procedo)\\b/i.test(\n trimmed\n ) ||\n /^¿?(qu[eé]|cu[aá]l|cu[aá]les|c[oó]mo|d[oó]nde|cu[aá]ndo|por qu[eé])(?:\\s|\\?|$)/i.test(\n trimmed\n )\n );\n}\n\nfunction deriveStopState(payload: CodexHookPayload): SessionState {\n if (payload.stop_hook_active === true) return 'working';\n if (isQuestion(payload.last_assistant_message)) return 'waiting-input';\n return 'idle';\n}\n\n// ── State derivation ──────────────────────────────────────────────────────────\n\nfunction deriveHookState(\n event: string,\n payload: CodexHookPayload,\n previous?: PreviousState | null\n): DerivedHookState {\n const toolInputSummary = summarizeToolInput(\n payload.tool_name,\n payload.tool_input\n );\n switch (event) {\n case 'SessionStart':\n return {\n attentionKind: 'none',\n hasAttention: false,\n state: 'idle',\n summary: payload.source\n ? `Codex session ${payload.source}`\n : 'Codex session started',\n };\n case 'UserPromptSubmit':\n return {\n attentionKind: 'none',\n hasAttention: false,\n state: 'working',\n summary: summarizeText(payload.prompt) ?? 'Codex is working',\n };\n case 'PreToolUse':\n return {\n activeTool: payload.tool_name,\n attentionKind: 'none',\n hasAttention: false,\n state: 'working',\n summary: payload.tool_name\n ? `Using ${payload.tool_name}${toolInputSummary ? `: ${toolInputSummary}` : ''}`\n : undefined,\n toolInputSummary,\n toolName: payload.tool_name,\n toolResultStatus: 'started',\n };\n case 'PermissionRequest':\n return {\n activeTool: payload.tool_name,\n attentionKind: 'permission',\n hasAttention: true,\n state: 'waiting-input',\n summary: payload.tool_name\n ? `Permission required for ${payload.tool_name}${toolInputSummary ? `: ${toolInputSummary}` : ''}`\n : 'Permission required',\n toolInputSummary,\n toolName: payload.tool_name,\n toolResultStatus: 'blocked',\n };\n case 'PostToolUse': {\n // request_user_input means the agent asked a question and is waiting for\n // the user to respond — this is waiting-input, not working.\n if (payload.tool_name === 'request_user_input') {\n const question =\n typeof payload.tool_input?.['question'] === 'string'\n ? (payload.tool_input['question'] as string).slice(0, 120)\n : undefined;\n return {\n activeTool: payload.tool_name,\n attentionKind: 'question',\n hasAttention: true,\n state: 'waiting-input',\n summary: question\n ? `Waiting for input: ${question}`\n : 'Waiting for user input',\n toolInputSummary: question,\n toolName: payload.tool_name,\n toolResultStatus: 'completed',\n };\n }\n return {\n activeTool: payload.tool_name,\n attentionKind: 'none',\n hasAttention: false,\n state: 'working',\n summary: payload.tool_name\n ? `Using ${payload.tool_name}${toolInputSummary ? `: ${toolInputSummary}` : ''}`\n : undefined,\n toolInputSummary,\n toolName: payload.tool_name,\n toolResultStatus:\n typeof payload.tool_response === 'object' &&\n payload.tool_response !== null &&\n 'is_error' in (payload.tool_response as Record<string, unknown>)\n ? (payload.tool_response as Record<string, unknown>)['is_error'] ===\n true\n ? 'error'\n : 'completed'\n : 'completed',\n };\n }\n case 'SubagentStart':\n return {\n attentionKind: 'none',\n hasAttention: false,\n state: 'working',\n summary: payload.agent_type\n ? `Codex sub-agent ${payload.agent_type}`\n : 'Codex sub-agent',\n };\n case 'SubagentStop': {\n const state = deriveStopState(payload);\n return {\n attentionKind: state === 'waiting-input' ? 'question' : 'none',\n hasAttention: state === 'waiting-input',\n state,\n summary:\n summarizeText(payload.last_assistant_message) ??\n (payload.agent_type\n ? `Codex sub-agent ${payload.agent_type}`\n : 'Codex sub-agent'),\n };\n }\n case 'Stop': {\n const state = deriveStopState(payload);\n return {\n attentionKind: state === 'waiting-input' ? 'question' : 'none',\n hasAttention: state === 'waiting-input',\n state,\n summary: summarizeText(payload.last_assistant_message),\n };\n }\n default:\n return {\n attentionKind: previous?.attentionKind ?? 'none',\n hasAttention: previous?.hasAttention ?? false,\n state: previous?.state ?? 'idle',\n };\n }\n}\n\nfunction deriveSummary(\n event: string,\n payload: CodexHookPayload\n): string | undefined {\n if (event === 'UserPromptSubmit' && payload.prompt) {\n return summarizeText(payload.prompt);\n }\n if (event === 'SessionStart' && payload.source) {\n return `Codex session ${payload.source}`;\n }\n if (event === 'PermissionRequest' && payload.tool_name) {\n const target = summarizeToolInput(payload.tool_name, payload.tool_input);\n return `Permission required for ${payload.tool_name}${target ? `: ${target}` : ''}`;\n }\n if (\n (event === 'PreToolUse' || event === 'PostToolUse') &&\n payload.tool_name\n ) {\n if (event === 'PostToolUse' && payload.tool_name === 'request_user_input') {\n const question =\n typeof payload.tool_input?.['question'] === 'string'\n ? (payload.tool_input['question'] as string).slice(0, 120)\n : undefined;\n return question\n ? `Waiting for input: ${question}`\n : 'Waiting for user input';\n }\n const cmd = (payload.tool_input?.['command'] as string | undefined)?.slice(\n 0,\n 60\n );\n return cmd\n ? `Using ${payload.tool_name}: ${cmd}`\n : `Using ${payload.tool_name}`;\n }\n if (event === 'Stop') {\n return summarizeText(payload.last_assistant_message);\n }\n if (event === 'SubagentStart' || event === 'SubagentStop') {\n return (\n summarizeText(payload.last_assistant_message) ??\n (payload.agent_type\n ? `Codex sub-agent ${payload.agent_type}`\n : 'Codex sub-agent')\n );\n }\n return undefined;\n}\n\n// ── Tmux integration ──────────────────────────────────────────────────────────\n\nfunction setTmuxPaneOption(sessionId: string): void {\n const tmuxPane = process.env['TMUX_PANE'];\n if (!tmuxPane) return;\n\n const result = spawnSync(\n 'tmux',\n ['set-option', '-p', '-t', tmuxPane, '@codex-session', sessionId],\n {\n stdio: 'ignore',\n timeout: 2000,\n }\n );\n\n if (result.error || result.status !== 0) {\n // Non-fatal — tmux may not be available or the pane might have closed\n }\n}\n\n// oxlint-disable-next-line no-unused-vars\nfunction clearTmuxPaneOption(): void {\n const tmuxPane = process.env['TMUX_PANE'];\n if (!tmuxPane) return;\n\n spawnSync('tmux', ['set-option', '-up', '-t', tmuxPane, '@codex-session'], {\n stdio: 'ignore',\n timeout: 2000,\n });\n}\n\n// ── File writing ──────────────────────────────────────────────────────────────\n\ninterface PreviousState {\n state?: SessionState;\n hasAttention?: boolean;\n attentionKind?: AttentionKind;\n}\n\ninterface HookStateFile {\n eventType: string;\n eventTime: number;\n hookSeq: number;\n state: SessionState;\n hasAttention: boolean;\n attentionKind: AttentionKind;\n summary?: string;\n activeTool?: string;\n toolName?: string;\n toolInputSummary?: string;\n toolResultStatus?: ToolResultStatus;\n permissionMode?: string;\n startSource?: string;\n model?: string;\n endReason?: string;\n transcriptPath?: string;\n currentPath?: string;\n turnId?: string;\n agentId?: string;\n agentType?: string;\n lastActivity: number;\n parentSessionId?: string;\n detectionSource: 'codex-hook';\n}\n\nfunction readExistingHookState(\n hooksDir: string,\n sessionId: string\n): HookStateFile | null {\n const path = join(hooksDir, `${sessionId}.json`);\n if (!existsSync(path)) return null;\n try {\n return JSON.parse(readFileSync(path, 'utf8')) as HookStateFile;\n } catch {\n return null;\n }\n}\n\nfunction writeHookFile(\n hooksDir: string,\n sessionId: string,\n data: HookStateFile\n): void {\n ensureHooksDirSafe(hooksDir);\n const targetPath = join(hooksDir, `${sessionId}.json`);\n const tmpPath = join(hooksDir, `${sessionId}.json.tmp`);\n writeFileSync(tmpPath, JSON.stringify(data), 'utf8');\n try {\n renameSync(tmpPath, targetPath);\n } catch {\n writeFileSync(targetPath, JSON.stringify(data), 'utf8');\n }\n}\n\n// ── Main ──────────────────────────────────────────────────────────────────────\n\nasync function main(): Promise<void> {\n let raw = '';\n for await (const chunk of Bun.stdin.stream()) {\n raw += new TextDecoder().decode(chunk);\n }\n\n let payload: CodexHookPayload;\n try {\n payload = JSON.parse(raw) as CodexHookPayload;\n } catch {\n process.exit(0); // Malformed input — exit cleanly so Codex CLI is not disrupted\n }\n\n const event = payload.hook_event_name ?? '';\n const sessionId = payload.session_id;\n if (!sessionId) process.exit(0);\n\n const hooksDir = resolveHooksDir();\n const now = Date.now();\n const previous = readExistingHookState(hooksDir, sessionId);\n const previousState: PreviousState | null = previous\n ? {\n attentionKind: previous.attentionKind,\n hasAttention: previous.hasAttention,\n state: previous.state,\n }\n : null;\n const baseSeq = previous?.hookSeq ?? 0;\n const nowMs = Math.floor(Date.now() / 100);\n const hookSeq = Math.max(baseSeq + 1, nowMs);\n\n if (event === 'SubagentStart' || event === 'SubagentStop') {\n const childSessionId = payload.agent_id;\n if (!childSessionId) {\n if (event === 'SubagentStop') process.stdout.write('{}\\n');\n process.exit(0);\n }\n\n const childPrevious = readExistingHookState(hooksDir, childSessionId);\n const childPreviousState: PreviousState | null = childPrevious\n ? {\n attentionKind: childPrevious.attentionKind,\n hasAttention: childPrevious.hasAttention,\n state: childPrevious.state,\n }\n : null;\n const childBaseSeq = childPrevious?.hookSeq ?? 0;\n const childHookSeq = Math.max(childBaseSeq + 1, hookSeq);\n const derived = deriveHookState(event, payload, childPreviousState);\n const summary =\n derived.summary ??\n deriveSummary(event, payload) ??\n childPrevious?.summary;\n const hookData: HookStateFile = {\n attentionKind: derived.attentionKind,\n detectionSource: 'codex-hook',\n eventTime: now,\n eventType: event,\n hasAttention: derived.hasAttention,\n hookSeq: childHookSeq,\n lastActivity: now,\n state: derived.state,\n ...(summary ? { summary } : {}),\n ...(payload.permission_mode\n ? { permissionMode: payload.permission_mode }\n : {}),\n ...(payload.model ? { model: payload.model } : {}),\n transcriptPath:\n payload.agent_transcript_path ?? childPrevious?.transcriptPath ?? '',\n ...(payload.turn_id ? { turnId: payload.turn_id } : {}),\n ...(payload.agent_id ? { agentId: payload.agent_id } : {}),\n ...(payload.agent_type ? { agentType: payload.agent_type } : {}),\n currentPath: payload.cwd,\n parentSessionId: `codex:${sessionId}`,\n };\n\n writeHookFile(hooksDir, childSessionId, hookData);\n await tryHttpDelivery({ sessionId: childSessionId, ...hookData });\n setTmuxPaneOption(sessionId);\n\n if (event === 'SubagentStop') {\n process.stdout.write('{}\\n');\n }\n process.exit(0);\n }\n\n // Stop with stop_hook_active=false signals the end of a turn, not the session itself.\n // We delete the state file but leave the @codex-session pane option so the pane\n // remains linked to the Codex session between turns.\n if (event === 'Stop' && payload.stop_hook_active === false) {\n const derived = deriveHookState(event, payload, previousState);\n const summary =\n derived.summary ?? deriveSummary(event, payload) ?? previous?.summary;\n const hookData: HookStateFile = {\n attentionKind: derived.attentionKind,\n detectionSource: 'codex-hook',\n eventTime: now,\n eventType: event,\n hasAttention: derived.hasAttention,\n hookSeq,\n state: derived.state,\n ...(summary ? { summary } : {}),\n ...(payload.transcript_path\n ? { transcriptPath: payload.transcript_path }\n : {}),\n currentPath: payload.cwd,\n lastActivity: now,\n };\n\n await tryHttpDelivery({\n sessionId,\n ...hookData,\n });\n\n if (derived.state === 'waiting-input') {\n writeHookFile(hooksDir, sessionId, hookData);\n setTmuxPaneOption(sessionId);\n } else {\n try {\n rmSync(join(hooksDir, `${sessionId}.json`), { force: true });\n } catch {\n // Ignore cleanup issues\n }\n }\n // Codex expects JSON on stdout for Stop events\n process.stdout.write('{}\\n');\n process.exit(0);\n }\n\n // For all other events: write state for the current session\n const derived = deriveHookState(event, payload, previousState);\n const summary =\n derived.summary ?? deriveSummary(event, payload) ?? previous?.summary;\n\n // Use a timestamp-based hookSeq to avoid collisions when multiple hooks\n // fire concurrently. The previous seq is used as a floor to ensure monotonicity.\n\n const hookData: HookStateFile = {\n attentionKind: derived.attentionKind,\n detectionSource: 'codex-hook',\n eventTime: now,\n eventType: event,\n hasAttention: derived.hasAttention,\n hookSeq,\n state: derived.state,\n ...(summary ? { summary } : {}),\n ...(derived.activeTool ? { activeTool: derived.activeTool } : {}),\n ...(derived.toolName ? { toolName: derived.toolName } : {}),\n ...(derived.toolInputSummary\n ? { toolInputSummary: derived.toolInputSummary }\n : {}),\n ...(derived.toolResultStatus\n ? { toolResultStatus: derived.toolResultStatus }\n : {}),\n ...(payload.permission_mode\n ? { permissionMode: payload.permission_mode }\n : {}),\n ...(payload.source ? { startSource: payload.source } : {}),\n ...(payload.model ? { model: payload.model } : {}),\n ...(payload.transcript_path\n ? { transcriptPath: payload.transcript_path }\n : {}),\n ...(payload.turn_id ? { turnId: payload.turn_id } : {}),\n currentPath: payload.cwd,\n lastActivity: now,\n };\n\n writeHookFile(hooksDir, sessionId, hookData);\n await tryHttpDelivery({ sessionId, ...hookData });\n\n // Update tmux binding on session start, prompt, and tool use events\n if (\n event === 'SessionStart' ||\n event === 'UserPromptSubmit' ||\n event === 'PreToolUse' ||\n event === 'PermissionRequest'\n ) {\n setTmuxPaneOption(sessionId);\n }\n\n // Codex expects JSON on stdout for Stop events\n if (event === 'Stop') {\n process.stdout.write('{}\\n');\n }\n\n process.exit(0);\n}\n\nmain().catch(() => process.exit(0));\n"
|
|
7
7
|
],
|
|
8
|
-
"mappings": ";;g9BAAA,
|
|
9
|
-
"debugId": "
|
|
8
|
+
"mappings": "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",
|
|
9
|
+
"debugId": "308AC51C15DF7A3764756E2164756E21",
|
|
10
10
|
"names": []
|
|
11
11
|
}
|